简介这是一套面向高校计算机相关专业毕业设计的完整项目资料主题为基于Python实现的漏洞扫描系统适合正在准备毕设、需要可运行源码与配套文档的学生及自学者参考。资源包共380个文件压缩后约84.69MB涵盖26个py源码文件、30个pyc编译文件、53个js脚本、26个css样式、15个tmpl模板以及大量gif、jpg、png等界面素材另含sql数据库脚本、doc说明文档与pptx演示文稿并附带nikto相关配置与插件文件结构完整、层次分明。系统功能覆盖用户登录、扫描首页、端口扫描模块与扫描列表模块文档部分则从网络安全概述、安全漏洞与扫描技术等理论基础讲起延伸至系统设计目标、总体架构与可行性分析再到测试环境搭建与各界面实现展示形成从理论到落地的完整链路。目前已有3163人学习下载可帮助读者快速理解漏洞扫描系统的设计思路与实现方式并在此基础上完成自己的毕设开发与文档撰写。1. 从一份毕业设计说起Python 漏洞扫描系统到底在扫什么很多人第一次接触「基于 Python 的漏洞扫描系统」是在毕业设计选题表上看到它觉得名字唬人、能写进简历真动手才发现不知道从哪下手。它本质上是一套用 Python 写的自动化检测工具给定一个目标地址或网段程序主动发起请求比对已知漏洞特征最后把结果连同证据一起落进数据库再配一份说明文档讲清楚怎么部署、怎么用。它解决的是「人工一台台翻配置、查版本」的低效问题适合安全入门者、运维自检场景以及需要一套可演示、可复现、能写进论文的完整工程的人。源码、数据库、说明文档这三件套恰好对应「能跑、能存、能讲」三个落地门槛缺一个都撑不起答辩。2. 系统架构与扫描引擎选型为什么不是随便拼几个脚本2.1 扫描器的三层结构调度、探测、存储一套能拿得出手的漏洞扫描系统绝不是把几个requests.get堆在一个文件里。常见做法是拆成三层调度层负责读取目标列表、控制并发、分配任务探测层是真正干活的插件集合每个插件对应一类漏洞检测逻辑存储层把扫描任务、目标、漏洞条目、原始响应分别落表。这样拆的好处是加一个新漏洞检测只需新增一个插件文件不用动主流程论文里也好画架构图。调度层我一般用concurrent.futures的线程池因为漏洞扫描绝大多数时间耗在网络 IO 上线程模型足够没必要上多进程把内存吃满。探测层用「插件注册」的方式每个插件是一个函数或类带name、match、verify三个属性主程序启动时扫描插件目录自动加载。存储层用 SQLite单文件、零配置毕业设计场景下完全够用也方便把.db文件直接打包进交付物。提示不要一上来就追求「支持上万插件」。毕业设计的评分点在于结构清晰、能跑通、有数据而不是插件数量。2.2 为什么选 SQLite 而不是 MySQL热词里sqllite数据库、mysql数据库常用命令、数据库增删改查都出现了说明很多人纠结选哪个。我的血泪经验是毕业设计优先 SQLite。原因有三。第一交付即用评审老师拿到压缩包解压就能跑不用先装数据库服务、配账号密码。第二Python 标准库自带sqlite3不引入额外依赖python安装完就能用。第三单文件便于随源码一起提交符合「源码 数据库 说明文档」的交付形态。MySQL 的优势在并发写入和多用户但漏洞扫描系统通常是单机、单用户、串行或小并发写入SQLite 的写锁根本不会成为瓶颈。真要用 MySQL也不是不行但说明文档里必须写清楚建库建表脚本和连接配置否则换台机器就跑不起来这是答辩现场最容易翻车的地方。2.3 建库建表四张核心表的最小可用设计数据库设计不用花哨四张表就能撑起整个系统。下面是我常用的建表脚本字段都留了注释方便直接抄。-- 扫描任务表一次扫描对应一条记录 CREATE TABLE scan_task ( id INTEGER PRIMARY KEY AUTOINCREMENT, target TEXT NOT NULL, -- 扫描目标域名或IP status TEXT DEFAULT pending, -- pending/running/done/failed start_time TEXT, -- 开始时间ISO格式字符串 end_time TEXT, -- 结束时间 total_found INTEGER DEFAULT 0 -- 本次发现的漏洞总数 ); -- 漏洞定义表插件元信息落库便于前端展示和统计 CREATE TABLE vuln_def ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 漏洞名称如敏感目录泄露 level TEXT NOT NULL, -- low/medium/high plugin TEXT NOT NULL, -- 对应插件文件名 description TEXT -- 漏洞说明写进报告用 ); -- 扫描结果表每条命中的漏洞一行 CREATE TABLE scan_result ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, -- 关联 scan_task.id vuln_id INTEGER NOT NULL, -- 关联 vuln_def.id url TEXT NOT NULL, -- 命中的具体URL evidence TEXT, -- 证据如响应片段 found_time TEXT, -- 发现时间 FOREIGN KEY (task_id) REFERENCES scan_task(id), FOREIGN KEY (vuln_id) REFERENCES vuln_def(id) ); -- 原始响应表存关键请求响应方便复现和写论文 CREATE TABLE raw_response ( id INTEGER PRIMARY KEY AUTOINCREMENT, result_id INTEGER NOT NULL, -- 关联 scan_result.id request TEXT, -- 请求报文 response TEXT, -- 响应报文可截断 FOREIGN KEY (result_id) REFERENCES scan_result(id) );逻辑说明scan_task记录每次扫描的生命周期status字段让程序能断点续扫vuln_def把漏洞元信息和插件解耦改描述不用动代码scan_result是核心结果表evidence字段一定要存答辩时老师问「你怎么证明这个漏洞存在」直接调出证据比空口解释强一百倍raw_response可选但强烈建议留写论文的「实验结果」章节时它就是素材库。参数说明level用字符串而不是数字是为了可读性统计时用GROUP BY level即可时间统一用 ISO 字符串而非时间戳方便直接看外键约束在 SQLite 里默认不开启需要在连接后执行PRAGMA foreign_keys ON;这点很多人不知道导致删任务时留下孤儿结果。2.4 插件式扫描引擎的最小实现下面这段是探测层的骨架展示插件如何注册、如何被调度。它不是完整系统但把最关键的「插件发现 并发执行 结果落库」串起来了。import os import sqlite3 import importlib.util from concurrent.futures import ThreadPoolExecutor, as_completed PLUGIN_DIR plugins def load_plugins(): 遍历插件目录动态加载每个 .py 文件里的 check 函数 plugins [] for fname in os.listdir(PLUGIN_DIR): if not fname.endswith(.py) or fname.startswith(_): continue path os.path.join(PLUGIN_DIR, fname) spec importlib.util.spec_from_file_location(fname[:-3], path) mod importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) # 约定每个插件必须暴露 name / level / check 三个属性 if hasattr(mod, check) and hasattr(mod, name): plugins.append(mod) return plugins def run_scan(task_id, target, plugins, db_pathscan.db): 对单个目标并发跑所有插件命中结果写入数据库 conn sqlite3.connect(db_path) conn.execute(PRAGMA foreign_keys ON;) cur conn.cursor() cur.execute(UPDATE scan_task SET statusrunning WHERE id?, (task_id,)) conn.commit() hits [] with ThreadPoolExecutor(max_workers8) as pool: futures {pool.submit(p.check, target): p for p in plugins} for fut in as_completed(futures): plugin futures[fut] try: result fut.result(timeout10) # 单插件超时保护 except Exception as e: print(f[!] {plugin.name} 执行异常: {e}) continue if result: # 插件返回非空表示命中 hits.append((plugin, result)) for plugin, result in hits: cur.execute( INSERT INTO scan_result(task_id, vuln_id, url, evidence, found_time) VALUES (?,?,?,?,datetime(now)), (task_id, plugin.vuln_id, result[url], result[evidence]) ) cur.execute(UPDATE scan_task SET statusdone, total_found? WHERE id?, (len(hits), task_id)) conn.commit() conn.close() return len(hits)逻辑说明load_plugins用importlib动态加载避免每加一个插件就改主程序run_scan用线程池并发as_completed保证谁先返回谁先处理每个插件单独try/except并设超时防止某个插件卡死拖垮整轮扫描——这是实战里最常见的稳定性问题。结果落库前先更新任务状态为running结束后更新为done并写入命中数这样即使程序中途崩溃数据库里也能看出哪些任务没跑完。参数说明max_workers8是经验值目标少可以调大目标多或对方有防护就调小避免触发限流timeout10是单插件超时网络差的环境可以放宽到 20vuln_id需要插件在加载时从vuln_def表查得或硬编码建议在插件里写vuln_id 1这种显式声明别在运行时反复查库。3. 从零跑通第一个扫描插件目录探测的完整落地3.1 环境准备与依赖安装先把环境搭起来。热词里python安装教程、python官网下载、python安装numpy库的方法出现频率很高说明不少人是第一次配 Python 环境。漏洞扫描系统本身不需要 numpy但如果你后续想加机器学习做流量分类那再说。当前阶段只需要标准库加requests。# 建议 Python 3.8 及以上3.8 是很多教程和库的兼容基线 python --version # 创建虚拟环境避免污染全局 python -m venv venv # Linux / macOS source venv/bin/activate # Windows venv\Scripts\activate # 安装唯一的外部依赖 pip install requests逻辑说明虚拟环境是必须的毕业设计交付时在说明文档里写清楚「先建虚拟环境再装依赖」能避免评审老师机器上版本冲突。requests用于发 HTTP 请求标准库的urllib也能用但requests的 API 更友好处理超时和重定向更省心。参数说明python -m venv venv里的第二个venv是目录名可以改成.venv隐藏起来激活命令因系统而异说明文档里两种都要写。3.2 写一个目录探测插件目录探测是最适合入门的插件给一个目标尝试访问一批常见敏感路径根据状态码和响应特征判断是否存在。下面这个插件可以直接放进plugins/目录。import requests name 敏感目录探测 level medium vuln_id 1 # 对应 vuln_def 表里的记录 # 常见敏感路径实际项目可扩充 PATHS [/admin, /backup, /.git/config, /phpinfo.php, /config.php] def check(target): 返回 None 表示未命中返回 dict 表示命中 if not target.startswith(http): target http:// target for path in PATHS: url target.rstrip(/) path try: resp requests.get(url, timeout5, allow_redirectsFalse) except requests.RequestException: continue # 200 且响应体不是通用错误页才认为命中 if resp.status_code 200 and len(resp.text) 0: return { url: url, evidence: fstatus{resp.status_code}, len{len(resp.text)} } return None逻辑说明allow_redirectsFalse很关键很多站点会把不存在的路径 302 到首页如果跟随重定向所有路径都会「命中」结果全是误报。判断条件用状态码加响应长度双重校验比只看状态码稳。返回的evidence里带上状态码和长度写进数据库后就是可追溯的证据。参数说明timeout5是单请求超时目录多的时候整体耗时会累加可以配合线程池PATHS列表建议按目标类型分组比如 Java 站点加/actuatorPHP 站点加/phpinfo.php别一股脑全塞进去否则误报率飙升。3.3 把插件跑起来并验证结果写完之后要验证不能写完就交。下面这段是手动触发一次扫描并打印结果的最小脚本。import sqlite3 from engine import load_plugins, run_scan # 1. 插入一条任务 conn sqlite3.connect(scan.db) cur conn.cursor() cur.execute(INSERT INTO scan_task(target, status) VALUES (?, pending), (http://testphp.vulnweb.com,)) task_id cur.lastrowid conn.commit() conn.close() # 2. 加载插件并执行 plugins load_plugins() print(f已加载插件: {[p.name for p in plugins]}) found run_scan(task_id, http://testphp.vulnweb.com, plugins) print(f任务 {task_id} 完成命中 {found} 条) # 3. 回查结果 conn sqlite3.connect(scan.db) for row in conn.execute( SELECT url, evidence FROM scan_result WHERE task_id?, (task_id,)): print(row) conn.close()逻辑说明先插任务拿到task_id再跑扫描最后回查结果形成闭环。lastrowid是 SQLite 拿自增主键的标准方式。打印插件列表能确认动态加载是否生效如果列表为空八成是插件文件没暴露check或name。参数说明测试目标建议用公开的靶场环境别拿真实站点练手这是底线。run_scan的db_path默认scan.db如果数据库文件不在当前目录要显式传路径。4. 避坑与排查那些让扫描系统当场翻车的地方4.1 现象所有路径都命中结果全是误报原因请求跟随了重定向站点把 404 页面 302 到首页首页返回 200于是每个路径都被判定为存在。解决requests.get加allow_redirectsFalse并且对响应体做特征过滤比如排除包含「页面不存在」「404」字样的响应。更稳的做法是先请求一个必然不存在的随机路径记录它的状态码和长度作为基线后续命中必须与基线不同。4.2 现象扫描跑一半卡死任务状态永远停在 running原因某个插件发起的请求没有设超时或者对方服务器保持连接不返回线程被永久占用。解决所有网络请求必须带timeout插件执行外层再套一层future.result(timeout10)兜底。数据库里statusrunning的任务程序启动时可以扫描一遍超过一定时间未完成的标记为failed这就是「后悔药」。4.3 现象换台机器就报 no such table原因数据库文件没随源码一起提交或者程序启动时没有自动建表。解决在程序入口加一段初始化逻辑检测scan.db是否存在不存在就执行建表脚本。建表脚本单独放一个schema.sql文件用executescript执行说明文档里注明「首次运行自动建库」。4.4 现象中文漏洞名称写进数据库变成乱码原因SQLite 默认编码没问题但 Windows 终端输出或文件读写时用了错误编码。解决Python 3 的sqlite3默认 UTF-8问题通常出在读取插件文件或写日志时。统一在文件操作里显式指定encodingutf-8打印到 Windows 终端乱码不影响数据库内容别被终端骗了。4.5 现象并发调大后目标站点拒绝服务或本机端口耗尽原因线程数过高短时间内发起大量连接触发对方限流或本机 TIME_WAIT 堆积。解决max_workers控制在 8 到 16 之间插件内部请求之间加小延时或者用信号量限制单目标并发。扫描是自检工具不是压力测试别把参数调到自己都收不了场。5. 让系统更像一个「作品」报告生成与可复现验证到这一步系统能扫、能存、能查但离「毕业设计作品」还差一口气——缺一份能直接放进论文附录的扫描报告。我的习惯是加一个报告生成模块从数据库读结果输出 Markdown 或 HTML。下面这段从scan_result联表查出漏洞详情并生成 Markdown 表格。import sqlite3 def gen_report(task_id, db_pathscan.db, outreport.md): conn sqlite3.connect(db_path) rows conn.execute( SELECT v.name, v.level, r.url, r.evidence, r.found_time FROM scan_result r JOIN vuln_def v ON r.vuln_id v.id WHERE r.task_id ? ORDER BY v.level DESC , (task_id,)).fetchall() conn.close() with open(out, w, encodingutf-8) as f: f.write(f# 扫描报告 (任务 {task_id})\n\n) f.write(f共发现 {len(rows)} 条漏洞\n\n) f.write(| 漏洞名称 | 等级 | URL | 证据 | 发现时间 |\n) f.write(|---|---|---|---|---|\n) for name, level, url, evidence, t in rows: f.write(f| {name} | {level} | {url} | {evidence} | {t} |\n) print(f报告已生成: {out}) gen_report(1)逻辑说明联表查询把漏洞名称和等级从vuln_def带出来按等级降序排列高危在前。输出 Markdown 表格直接粘进论文或转成 PDF 都行。encodingutf-8必须写否则中文标题在部分系统上会出问题。参数说明task_id对应一次扫描报告按任务隔离方便对比不同目标的扫描结果。out参数可以改成带时间戳的文件名避免多次生成互相覆盖。验证方法上我一般会做两件事。第一用公开靶场跑一遍把命中的 URL 手动访问确认确保不是误报。第二故意把某个插件的判断条件改错看报告里是否出现异常条目以此验证整条链路没有「假装成功」。这两步做完答辩时被问到「你怎么保证结果可信」就有实打实的依据。最后说个习惯每加一个新插件先单独写个if __name__ __main__的小测试跑通再挂进主流程。我早期图快插件写完直接扔进目录跑全量结果一个插件的异常把整轮扫描带崩排查了半天才发现是返回值类型不对。从那以后插件独立可测成了我的硬规矩。希望帮到你。本文还有配套的精品资源点击获取