基于Python和Flask的Web漏洞扫描系统设计与实现

发布时间:2026/8/29 3:43:28
基于Python和Flask的Web漏洞扫描系统设计与实现 简介在网络安全体系建设中Web漏洞扫描是渗透测试与安全评估的关键环节。通过对目标应用进行信息搜集、端口扫描、指纹识别等测绘工作并结合SQL注入、XSS等漏洞检测逻辑可有效暴露潜在风险。基于Python与Flask构建的扫描系统利用线程池实现并发探测借助SQLite持久化结果并通过可视化界面呈现报告为安全工程师提供了自动化、可复现的检测工具。无论是在毕业设计还是实际安全测试场景此类系统都兼具教学与工程价值。本文完整解析从信息搜集到漏洞扫描的系统架构与实现细节帮助读者快速搭建属于自己的Web漏洞扫描器。 去年我做毕业设计那会儿导师看到“Web漏洞扫描”这个方向直接点头说这题既能在算法上看出功夫又接地气但真正动手才发现网上能跑通的源码十有八九是残缺品——要么只做了端口扫描就敢叫“扫描系统”要么把SQLMap的壳套了一层就交差。我最后选择基于Python和Flask从零搭一套完整的Web漏洞扫描系统核心拆成“信息搜集”和“漏洞扫描”两大部分折腾了三个月拿到优秀答辩。这篇文章就按我实际开发的顺序把整个系统的设计思路、核心模块实现、踩过的坑一次性讲透给正在做同类课题的同学一个完整可复现的参考。1. 为什么选这个题目一个能实战的毕设有多难找做毕设最怕什么不是代码写不出来是题目看起来有东西做起来全是空壳。市面上很多“Web漏洞扫描系统”的毕设源码点开一看要么是调了现成的工具要么只是静态扫描几个固定页面根本谈不上“系统”。我选这个题的核心逻辑很简单信息搜集和漏洞扫描是渗透测试流程里最有代表性的两个阶段把它们用Flask包成一套带网页界面的工具既有技术深度又有完整的产品形态演示的时候也好看。Python在这个场景下的优势不用多说。requests、BeautifulSoup、dnspython这些库简直是天生为Web安全准备的写探测逻辑的时候不用重复造轮子。Flask则是把扫描能力包装成“可操作产品”的最轻方案——不需要像Django那样搞得重路由、模板、静态文件几件事就能搭出完整的任务管理页面而且异步、SQLite这些配套方案都很成熟。选型的时候我对比过Django但毕设项目里Flask的轻量正好匹配“分模块渐进开发”的节奏先把核心扫描引擎写好再往上套Web层。整个系统的模块划分我在前期就定了下来后面开发基本没改过信息搜集模块子域名枚举、端口扫描、HTTP指纹识别、目录探测。漏洞扫描模块SQL注入检测、XSS检测、敏感文件探测。任务调度模块基于线程池的任务分发、状态跟踪。数据存储模块SQLite存取扫描目标和结果。展示模块Flask页面、任务管理、结果报表。模块化设计带来的好处在开发中段才体现出来——信息搜集和漏洞扫描本来是两个独立的Python程序我用Flask把它们串起来的时候只需要设计好数据接口两个引擎各自跑各自的互不干扰。2. 系统骨架Flask如何把扫描能力变成产品很多同学写毕设代码写了一大坨发现最后根本没法给别人演示。Flask的核心价值就是给底层扫描引擎套上一层“人能操作的壳”。我的项目结构和常见的Flask应用有些区别为了让扫描引擎和Web层解耦我把代码按业务边界分成了这样scanner/ ├── app.py # Flask 主入口 ├── core/ │ ├── info_collect.py # 信息搜集引擎 │ ├── vuln_scan.py # 漏洞扫描引擎 │ ├── task_manager.py # 任务调度 │ └── db.py # 数据库操作 ├── templates/ # 页面模板 ├── static/ # 静态资源 └── requirements.txt核心设计有几个关键决策值得展开说。首先是任务异步处理。扫描是个耗时操作如果直接在Flask路由里同步跑请求一进来页面就卡住用户毫无体验。我用了Python自带的concurrent.futures.ThreadPoolExecutor做线程池把扫描任务丢进后台执行前端通过轮询接口获取进度。这里我没有用Celery因为毕设场景不需要分布式任务队列线程池足够而且部署简单不引入额外依赖。from flask import Flask, render_template, request, jsonify from concurrent.futures import ThreadPoolExecutor from core.info_collect import collect_info from core.vuln_scan import scan_vulns from core.task_manager import add_task, get_task_status app Flask(__name__) executor ThreadPoolExecutor(max_workers4) app.route(/scan, methods[POST]) def start_scan(): target request.json.get(target) task_id add_task(target) executor.submit(execute_scan, task_id, target) return jsonify({task_id: task_id, status: running}) def execute_scan(task_id, target): # 按顺序执行信息搜集和漏洞扫描 info_result collect_info(target) vuln_result scan_vulns(target) save_result(task_id, info_result, vuln_result) update_task_status(task_id, completed)这段代码是整个Web层的主线后面所有功能都是围绕它展开的。有些毕设代码把扫描逻辑直接写在路由里页面一请求就卡住好几秒体验极差。所以我强烈建议用任务ID把扫描过程和页面请求解耦这也是答辩时“系统设计”的一个亮点。数据库设计同样关键。我用SQLite就够了三张表搞定tasks表存扫描任务的元信息比如目标地址、状态、创建时间。info_results表存信息搜集的产出比如开放端口、子域名、指纹信息。vuln_results表存漏洞信息包括漏洞类型、URL、参数、描述。数据库这块别过度设计毕设规模上SQLite完全撑得住演示时还能直接把数据库文件拷走显得规范。3. 信息搜集模块扫描的第一步不是漏洞是测绘信息搜集是漏洞扫描的前置步骤这块做得扎实漏洞扫描才有意义。我管它叫“测绘”而不是“搜集”是因为这一步的本质是把目标Web应用的暴露面全部摸清楚——有哪些子域名、开了哪些端口、跑着什么服务、用了什么框架。3.1 子域名枚举的实现思路子域名枚举我用了两种方式结合一是基于字典的暴力解析直接用dnspython逐个尝试常见的子域名前缀二是从证书透明度日志网站拉取已知子域名。字典暴破纯粹是拼资源和字典质量我准备了一个包含1000条常见前缀的字典比如admin、api、test、dev、mail等解析到相同IP的可能直接跳过。import dns.resolver def subdomain_enum(domain, wordlist): found [] for sub in wordlist: target f{sub}.{domain} try: answers dns.resolver.resolve(target, A) ips [answer.address for answer in answers] found.append({domain: target, ips: ips}) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): continue return found证书透明度日志的方式则是从crt.sh上拉取JSON数据解析出所有匹配*.domain.com的条目。两种方式合并去重之后覆盖面比单一方式要好不少。这里有个细节是DNS解析的超时设置不超时可能导致整个扫描线程卡住我用dns.resolver.Resolver自定义了resolver.timeout 3。3.2 端口扫描与Banner识别端口扫描模块我利用socket实现核心是并发连接测试。常用的TCP连接扫描原理简单——尝试与目标端口建立TCP连接能连上就是开放的。但毕设里要注意效率单线程扫1000个端口会慢到让人怀疑人生。我用线程池并发来扫描每个端口一个线程同时限制并发数是200避免触发目标防火墙的封禁。import socket from concurrent.futures import ThreadPoolExecutor COMMON_PORTS [21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1433, 1521, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 8888, 9200] def port_scan(host): open_ports [] def _check(port): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) if sock.connect_ex((host, port)) 0: banner grab_banner(host, port) open_ports.append({port: port, service: guess_service(port), banner: banner}) sock.close() with ThreadPoolExecutor(max_workers200) as executor: executor.map(_check, COMMON_PORTS) return open_portsBanner识别就是连接上服务之后主动发送一个探测数据包比如向HTTP端口发送HEAD / HTTP/1.1请求读取返回的头部信息。很多服务会直接暴露版本号比如Apache/2.4.41这些信息在漏洞匹配阶段很有用。3.3 指纹识别与敏感目录探测指纹识别的原理比较简单——通过响应头里的Server字段、X-Powered-By字段以及HTML源码中的特定关键字判断Web应用类型和版本。我维护了一个简单的正则规则表比如检测到wp-content就判定为WordPress检测到generator contentDrupal就判定为Drupal。敏感目录探测是信息搜集模块里比较有意思的部分。原理是发HTTP请求逐一访问常见路径比如.git、.gitignore、admin.php、backup.zip、phpinfo.php、web.config等然后根据响应状态码和页面大小做判定——200说明文件存在403说明目录存在但有权限限制。请求的时候注意设置合理的UA头和超时否则有些WAF会把我们拦截掉导致误判。这个模块我单独做成了可配置的字典放在外部文本文件里方便扩展。答辩老师问“你的系统能检测哪些敏感文件”时直接打开字典文件给他看既有说服力又体现工程化思维。4. 漏洞扫描引擎检测规则背后的逻辑漏洞扫描是系统的核心卖点也是区分“玩具”和“可用工具”的关键。我挑了三个最典型、更容易在演示中出效果的漏洞类型SQL注入、XSS、敏感信息泄露。每个类型的检测逻辑都必须说清楚原理答辩的时候老师一定会追问。4.1 SQL注入检测的实现SQL注入检测我用了两种互补的检测策略基于报错和基于布尔盲注。基于报错的思路是向目标参数注入会触发数据库报错的特殊字符比如单引号、双引号然后检查响应页面里是否出现了SQL语法错误的特征比如SQL syntax、mysql_fetch、You have an error in your SQL syntax等。这种方法简单直接但只有在报错信息没有被过滤时有效。布尔盲注的思路则更加可靠——在参数值后拼接条件表达式比如?id1 AND 11和?id1 AND 12如果两次响应页面长度、标题或关键内容有明显差异说明我们注入的条件真的影响到了SQL查询结果这就是注入点。def check_sql_injection(url, param): payload_true 1 AND 11 payload_false 1 AND 12 normal_url f{url}?{param}1 true_url f{url}?{param}{payload_true} false_url f{url}?{param}{payload_false} normal_resp requests.get(normal_url, timeout5) true_resp requests.get(true_url, timeout5) false_resp requests.get(false_url, timeout5) if len(true_resp.text) ! len(normal_resp.text) and len(false_resp.text) len(normal_resp.text): return True return False实际检测时这两种方法不是二选一而是同时跑只要命中其中一个就算疑似漏洞然后通过误报验证环节去掉明显不准的结果。很多系统的误报率居高不下问题就出在单一检测方法上。4.2 XSS检测的反射逻辑XSS检测的核心是判断注入的payload是否会被原样“反射”到响应页面中。我在系统里维护了一批XSS测试payload比如scriptalert(document.cookie)/script、img srcx onerroralert(1)、svg/onloadalert(1)等。检测流程是将payload编码到URL参数中向目标发出请求检查响应内容里是否包含payload的关键特征。这里有个非常关键的细节——响应页面返回的payload不一定和请求时完全一样可能被HTML实体编码成lt;scriptgt;这种情况下用字符串包含做判断就会漏报。所以我在判断前先把响应内容做一次HTML实体解码再查找payload特征。import html def check_xss(url, param): payload scriptalert(xss)/script encoded_payload urllib.parse.quote(payload) test_url f{url}?{param}{encoded_payload} resp requests.get(test_url, timeout5) decoded_text html.unescape(resp.text) return payload in decoded_text这个方法虽然简单但对反射型XSS的检出率相当高。在写报告时我还会记录触发URL、payload、响应中的命中片段方便用户验证。4.3 常见漏洞检测的扩展除上面两类我还实现了几个容易展示的检测项敏感文件探测访问常见的备份文件、配置文件路径比如/.git/head、/config.php.bak根据响应状态和内容判断是否泄露。目录列表检测请求目录路径检查响应中是否包含Index of /或Directory listing for这类目录列表特征。过期SSL证书检测检查HTTPS证书的有效期证书过期或快要过期也属于安全隐患。不安全的HTTP方法检测发送OPTIONS请求查看Allow头如果允许PUT、DELETE这类危险方法就标记告警。漏洞检测这块不要贪多求全选几个有代表性且实现结果稳定的就足够了。我当时试过加一堆检测项结果很多规则在测试靶场上本来就不触发演示效果反而变差了。做一个精度高、能复现的扫描器远胜过做一个大而全但满是误报的扫描器。5. 数据持久化与结果展示从“扫出来”到“看得懂”扫描引擎再厉害结果只打印在控制台里不懂的人根本没法用。Flask的这一层本质上就是把扫描结果变成“人话”。5.1 数据库设计与任务状态管理我用SQLite核心是两张业务表和一张任务表。任务表的设计比较重要因为前端要发起扫描、轮询进度没有任务状态管理就没法做异步展示。CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, target VARCHAR(255) NOT NULL, status VARCHAR(20) DEFAULT pending, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS info_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, result_type VARCHAR(50), content TEXT, FOREIGN KEY(task_id) REFERENCES tasks(id) ); CREATE TABLE IF NOT EXISTS vuln_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, vuln_type VARCHAR(50), url VARCHAR(500), param VARCHAR(255), detail TEXT, level VARCHAR(10), FOREIGN KEY(task_id) REFERENCES tasks(id) );状态管理我用了最简单的字符串状态机pending - running - completed/failed。前端定时每2秒请求一次/api/task/task_id拿到状态后控制页面刷新。这里有个体验细节状态返回completed后前端要跳转或者刷新到结果页不要在同一页面无限轮询否则数据量大了页面会卡。5.2 前端页面与报告导出前端我直接用Flask内置的Jinja2模板Bootstrap不引入Vue这类框架因为毕设演示场景下本地跑没必要增加复杂度。页面分四个首页输入扫描目标选择扫描类型只做信息搜集还是全量扫描。任务列表页展示所有历史扫描任务点击进入详情。信息搜集结果页以卡片形式展示子域名、端口、指纹、目录列表。漏洞结果页以表格形式列出所有漏洞按严重程度排序附修复建议。报告导出功能是个加分项。我用Python原生的csv模块把漏洞列表导出成CSV文件同时用Jinja2渲染一份HTML报告模板用户可以直接打印成PDF。答辩时演示“导入导出”这个功能老师们都会觉得完整度高。6. 上线前必须踩的坑并发、编码、误报排查这部分是全文真正的干货所在。任何没跑过真实扫描的代码看着再顺都有可能翻车。我把自己开发中遇到的几个典型问题逐一列出来每一个我都亲手修过。6.1 线程并发下的扫描干扰线程池并发跑扫描任务最大的问题是线程安全。当时我用了一个全局列表存储扫描结果结果并发跑多线程时列表被多个线程同时写入数据出现错乱扫描结果张冠李戴。后来排查半天发现是Python列表的append操作在GIL下虽然单个操作安全但“读取-处理-写入”这个复合操作不是原子的多线程同时执行就会出问题。解决办法有两个一是加线程锁二是用queue.Queue替代列表。我推荐后者因为队列本身就是为线程安全设计的从队列里取结果再批量汇总比加锁更不容易出错。6.2 编码问题与页面解析Web世界里编码地狱是绕不开的。有些网站返回的是gbk编码有些是utf-8有些甚至不声明编码。直接requests.get(url).text有时候会解析成乱码指纹识别和XSS检测都依赖页面内容乱码直接导致误报和漏报。def fetch_page(url, timeout5): resp requests.get(url, timeouttimeout) encoding resp.encoding or utf-8 try: page_text resp.content.decode(encoding, errorsreplace) except LookupError: page_text resp.text return page_text注意errorsreplace这个参数能让无法解码的字节替换成占位符而不是直接抛异常保证扫描流程不中断。这是我从一次扫描到一半崩溃的教训里换来的经验。6.3 误报与漏报的平衡扫描器做得不准比不扫更糟。我调误报调了很久最后的做法是“两级判定”。敏感文件探测这种项不能只看状态码还要看响应头Content-Length——如果返回的是框架自带的404页面即使状态码是200也要判定为不存在XSS检测则要求HTML解码后payload特征出现且不在JS代码块中防止脚本里本来就有我们的payload却被误判为反射。漏报问题则相反。有些网站WAF会对特殊字符过滤导致我们的注入payload被拦截并返回403这时候记录“目标可能存在WAF”比硬着头皮报漏洞更有价值。我在系统里加了一个探测WAF的简单规则一旦发现403状态码增多且页面中出现waf、denied等字样就提示用户“可能存在防护设备扫描结果可能不完整”。这种诚实反映局限性的设计答辩老师反而会觉得是加分项。7. 部署、演示与答辩的心得项目做完只是第一步怎么把它漂漂亮亮地展示出来也是毕设得分的关键。这一节我给正在做同一方向的学弟学妹几条掏心窝子的建议。7.1 环境部署清单部署环境我建议用虚拟环境管理依赖requirements.txt锁定版本。这里有个坑是dnspython在不同版本上的API有变化锁版本可以避免别人复制代码跑不起来。启动命令一行搞定pip install -r requirements.txt python app.py如果要在本机局域网内演示让老师用手机或另一台电脑访问记得启动时指定host0.0.0.0否则只能本机访问那就尴尬了。if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)提示演示时一定把debugTrue关掉。Flask的debug模式会在出错时弹出交互式调试器演示翻车时那个页面一出来答辩氛围会非常尴尬。7.2 演示和答辩的技巧演示不要真的拿网上某个不知名站点开扫合规和效果都不可控。我当时在本地DVWA靶场和自建的测试站点上跑扫描结果稳定、页面响应快演示节奏完全可控。答辩讲解时我的顺序是先讲系统整体架构用任务流程图说明信息搜集和漏洞扫描的衔接关系然后现场演示一遍从输入目标到产出报告的完整过程最后挑一个具体的扫描结果讲这个漏洞是怎么被检测出来的把检测逻辑和代码对应起来。老师最常问的问题是“如何避免对未知站点的扫描给他人造成危害”这个问题的标准思路是系统设计了授权确认机制扫描前要求用户确认已获得目标授权并通过robots协议过滤部分目录。我的系统里加了一个简单的授权弹窗扫描目标输入框下方强制勾选“我已获得授权”才能开始扫描。别小看这个设计它其实是在传递安全意识和职业伦理在毕设评审中很加分。关于扩展方向我当时时间不够没有加登录页面和用户系统如果你们有时间可以加上用户登录和扫描记录按用户隔离这样系统会更完整也能体现一定的安全意识。另外如果想把原理讲得更深可以把检测规则改成基于请求变异引擎的模糊测试那样在技术层面的复杂度会再上一个台阶但工作量也要翻倍。最后说点实际的这套系统的源码结构和设计文档是配套好的哪怕你现在刚入门Python只要把Flask的基本路由和模板弄明白按这个思路实现一遍完成度绝对不低。关键是别只抄代码要把每个模块为什么这么写弄懂答辩时才能扛住追问。本文还有配套的精品资源点击获取