公司动态
Flask构建Web漏洞扫描系统:从信息搜集到漏洞检测
简介这是一套面向计算机、通信、人工智能等专业本科生的毕业设计级Web安全实践项目基于Python Flask框架实现轻量级Web漏洞扫描系统聚焦信息搜集类漏洞识别与自动化探测适用于课程设计、大作业及毕设开发参考。资源包共156个文件涵盖43个核心Python模块含主控逻辑、爬虫引擎、端口扫描、子域名枚举等、19个CSS与18个JS前端资源集成Bootstrap、Font Awesome及Dark主题样式以及HTML页面、SQL建表脚本、Dockerfile容器化配置等整体压缩包仅2.88MB结构清晰、模块解耦度高。已有33人下载学习项目经答辩评审获98分附完整可运行源码与详细使用文档包含环境部署说明、功能调用示例及常见报错解决方案特别适合具备基础Web开发与网络安全知识的学习者进行二次开发与功能扩展。 站在毕业设计的视角这个基于Python Flask的Web漏洞扫描系统是我当年花了不少心思做完的一个项目。它本质上就是一个带Web界面的漏洞扫描工具分为信息搜集和漏洞扫描两大块信息搜集负责探测目标的域名解析、开放端口、服务指纹漏洞扫描则针对常见Web漏洞做自动化检测最后把结果存在数据库里再通过Flask提供的页面展示出来。整篇文档我会把从设计思路到具体实现再到踩坑记录从头到尾拆开讲清楚对这个方向感兴趣的学弟学妹可以直接拿去参考。1. 项目整体设计与模块拆解1.1 为什么选Python Flask而不是Django做Web漏洞扫描系统你第一个要决定的事就是后端框架。当时我手里有两个选择Flask和Django。Django功能全自带Admin后台、ORM、表单处理看起来省事但它的设计哲学是把一套完整架子塞给你很多模块这个项目根本用不到。而且Django的中间件和URL分发机制相对重在写这种以异步任务和接口调用为主的小工具时反而觉得束手束脚。Flask就简单多了它只做核心的请求分发和路由映射其他东西靠扩展加。我可以用它快速挂出一个API接口配合前端页面展示结果同时后台任务逻辑不受框架结构限制完全按照自己的思路写。还有一个现实原因毕业设计答辩时老师会问你框架选型的理由。你要是说“因为Django功能多所以用了Django”老师会继续追问具体多在哪、你用了哪几个模块、这些模块在你的系统里承担了什么角色。但Flask你可以答得很清楚路由灵活、上下文管理清晰、适合构建RESTful接口、开发周期短这些都是实际在这个项目里受益的点答起来底气足。1.2 系统架构和模块划分整个系统的逻辑可以分成三条线前端展示层、后端业务层、底层扫描引擎。前端展示层是Flask渲染的模板页面包括任务提交页、扫描结果页、报告详情页。后端业务层负责接收用户请求、校验目标URL格式、调用扫描引擎、把结果写入数据库。底层扫描引擎是核心里面又拆成信息搜集和漏洞扫描两个子模块。信息搜集模块做的事情包括获取目标URL的域名对应的IP地址、检测开放端口、识别Web服务器类型和版本、解析Robots协议、爬取站点目录结构。这些信息是为后续漏洞扫描做铺垫的因为知道了目标是什么框架、什么端口开放你才能选合适的检测规则提高扫描效率和准确率。漏洞扫描模块则是一堆检测插件每个插件针对某一种漏洞类型设计。比如SQL注入检测插件会向目标API添加特定的请求参数并分析响应特征XSS检测插件会构造payload并验证页面回显目录遍历检测插件则会尝试常见敏感路径。这些插件共同组成一个规则引擎扫描任务就是规则引擎遍历插件的过程。数据库我选的是SQLite原因很直接这个项目不需要分布式数据库也没有高并发写入需求SQLite单文件部署方便毕设展示的时候直接把项目拷过去就能跑。如果你要扩展成多用户版本再切MySQL也不难Flask的SQLAlchemy ORM层可以降低切换成本。2. 核心细节解析与实操要点2.1 信息搜集模块的关键实现信息搜集的第一个任务是获取IP。用socket.gethostbyname可以把域名解析成IPv4地址这是最基础的操作完全没有技术含量但需要注意一个坑如果你传入的URL里带协议头比如https://example.com需要先用urlparse把netloc提取出来再传给gethostbyname。我第一版代码没做这个过滤传入完整URL直接解析结果抛异常报错排查了一会儿才反应过来。端口扫描的实现方式有两种思路。一种是用socket逐端口connect测试简单直接但速度慢而且容易被目标防火墙发现。另一种是集成python-nmap库调用Nmap引擎做SYN扫描速度快、结果准确。我在毕业设计里用了python-nmap因为扫描结果可以拿到服务版本信息比如端口80是Apache还是Nginx这个信息对后续漏洞匹配很有用。不过要注意python-nmap只是Nmap的命令行封装本机必须先装好Nmap并配置到环境变量否则库会报找不到nmap可执行文件的错。Web指纹识别这块我用了两个思路结合。一个是解析服务器返回的Server头比如Server: nginx/1.18.0这个直接就能用另一个是匹配页面特征比如检测到WordPress就知道目标用的是PHP和MySQL。特征匹配靠一个预定义的字典里面存了常见CMS和框架的指纹特征做的时候需要维护这个字典工作量不小但这是Web信息搜集最有技术含量的部分。2.2 漏洞扫描插件的设计思路扫描插件设计要考虑两个维度一个是通用性一个是可扩展性。通用性指插件本身不依赖特定目标拿到URL和参数就能跑。可扩展性指新增漏洞类型时不用改主框架只需要按约定格式新增一个插件文件。我把插件基类定义在plugin_base.py里基类包含两个方法run()方法接收target、params、cookie等参数返回扫描结果report()方法负责生成报告片段。每个漏洞检测插件继承基类并实现这两个方法。这样写的好处是分工明确主调度器只负责遍历插件列表并调用run()不关心具体漏洞检测逻辑每个插件独立维护自己的检测代码互不干扰。SQL注入插件的核心逻辑并不复杂在目标URL的参数值后面拼接一个单引号请求后看响应里有没有数据库报错特征比如“SQL syntax error”或“mysql_fetch_array()”等字符串。再进一步用布尔盲注的方式给参数加一条恒真条件例如 and 11和恒假条件and 12分别请求比较两个响应内容的差异。如果有明显差异就说明参数可能参与了SQL语句拼接存在注入风险。这里有个细节比较响应差异时要剔除掉动态变化的内容比如页面里的时间戳否则容易误判。XSS检测插件的思路是反射型XSS检测把payload注入参数后请求看响应里是否原样返回payload。比如payload是 如果响应HTML里出现了完整的这段脚本字符串就说明参数值没有经过转义就被输出到页面存在反射型XSS。实际检测时我用的payload是自定义的随机字符串外面套一层HTML标签比如 payload 这样即使脚本被拦截只要标签原样返回就能确认漏洞存在误报率更低。2.3 Flask与扫描任务的异步处理Flask默认是同步处理请求的如果扫描任务直接写在路由函数里用户发起扫描请求后页面会一直转圈直到所有扫描完成后才返回。一个扫描任务可能要跑几分钟这样用户体验极差而且浏览器很容易超时断开连接。所以我用了线程池来处理扫描任务用户提交扫描请求后主线程把任务丢给后台线程池然后立刻返回一个“扫描已启动”的提示前端页面通过轮询接口获取扫描状态。这里有个地方容易被新手忽略Flask的线程安全。Flask的request对象是线程本地的后台线程里不能直接访问request上下文。我的做法是在路由函数里把目标URL等参数提取出来封装成一个任务对象再把这个对象传给后台线程。后台线程完全不依赖Flask上下文只是纯Python逻辑执行扫描这样就不存在线程安全问题。3. 实操过程与核心环节实现3.1 项目目录结构和代码组织我这里给出一份可以直接照搬的目录结构web_scanner/ ├── app.py # Flask入口初始化应用和路由 ├── config.py # 全局配置如数据库路径、线程池大小 ├── database.py # 数据库连接和模型定义 ├── plugins/ │ ├── __init__.py │ ├── base_plugin.py # 插件基类 │ ├── sql_injection.py # SQL注入检测插件 │ ├── xss_detection.py # XSS检测插件 │ └── directory_traversal.py # 目录遍历检测插件 ├── scanner/ │ ├── __init__.py │ ├── info_gather.py # 信息搜集模块 │ ├── scanner_engine.py # 扫描引擎调度插件 │ └── task_manager.py # 后台线程池任务管理 ├── templates/ │ ├── index.html # 提交扫描页面 │ ├── result.html # 扫描结果页面 │ └── report.html # 报告详情页面 └── requirements.txt这个目录结构的好处是清晰插件和扫描引擎分离模板和逻辑分离。答辩的时候老师问起来你可以说采用了模块化设计每个模块功能单一符合低耦合高内聚的原则。3.2 核心代码实现与参数解析先从配置开始看。config.py里我定义了这么几个参数import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATABASE_PATH os.path.join(BASE_DIR, scanner.db) THREAD_POOL_SIZE 3 SCAN_TIMEOUT 10 USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36数据库路径用os.path拼接是为了避免在Windows开发、Linux部署时路径分隔符问题。线程池大小设成3是因为免费的单机扫描任务不宜开太多并发太大会漏报目标服务器宕机或限制请求速率太小扫描效率又太低。数据库模型在database.py里我建了两张表task表保存扫描任务信息包括目标URL、扫描状态、创建时间result表保存每个漏洞的检测结果包括任务ID外键、漏洞类型、风险等级、url地址、漏洞描述。这样设计查询方便在结果页按任务ID查对应的所有漏洞记录一行代码搞定。from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from datetime import datetime import config engine create_engine(fsqlite:///{config.DATABASE_PATH}, echoFalse) Base declarative_base() Session sessionmaker(bindengine) class Task(Base): __tablename__ task id Column(Integer, primary_keyTrue, autoincrementTrue) url Column(String(500), nullableFalse) status Column(String(20), defaultpending) created_at Column(DateTime, defaultdatetime.now) results relationship(Result, backreftask, cascadeall, delete-orphan) class Result(Base): __tablename__ result id Column(Integer, primary_keyTrue, autoincrementTrue) task_id Column(Integer, ForeignKey(task.id)) vuln_type Column(String(50)) risk_level Column(String(10)) url Column(String(500)) description Column(Text)information_gather.py里有一个函数专门负责收集信息这里以获取服务器信息为例import requests from urllib.parse import urlparse import config def gather_server_info(url): parsed urlparse(url) scheme parsed.scheme if parsed.scheme else http netloc parsed.netloc if parsed.netloc else parsed.path target f{scheme}://{netloc} try: resp requests.get(target, timeoutconfig.SCAN_TIMEOUT, headers{User-Agent: config.USER_AGENT}) server_header resp.headers.get(Server, unknown) powered_by resp.headers.get(X-Powered-By, unknown) return { url: target, status_code: resp.status_code, server: server_header, powered_by: powered_by } except requests.exceptions.RequestException as e: return {error: str(e)}注意这里用urllib.parse解析URL时处理了没有协议头的情况这个我在第一版里漏掉了。实际上用户在页面输入的时候很可能直接输入example.com忘记带https://我就在提交任务时做了格式化def normalize_url(url): if not url.startswith(http): url http:// url return url.rstrip(/)尾部去斜杠也很重要否则同一个URL可能会被识别成两个不同的目标。扫描引擎scanner_engine.py里的核心调度逻辑是这样的from plugins.sql_injection import SQLInjectionPlugin from plugins.xss_detection import XSSDetectionPlugin from plugins.directory_traversal import DirectoryTraversalPlugin PLUGINS [ SQLInjectionPlugin(), XSSDetectionPlugin(), DirectoryTraversalPlugin() ] def run_scan(task_id, target_url): for plugin in PLUGINS: result plugin.run(target_url) if result: store_result(task_id, result)这里没有做复杂的插件加载机制直接用列表维护。如果后续新增插件在列表里加一行就行。对一个毕设来说够用了。3.3 Flask路由设计与前端页面交互Flask路由挂在app.py里我一共写了四个主要的接口from flask import Flask, render_template, request, jsonify from database import Session, Task, Result from scanner.task_manager import submit_scan, get_task_status app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/scan, methods[POST]) def start_scan(): url request.form.get(url, ) if not url: return jsonify({error: url不能为空}), 400 task_id submit_scan(url) return jsonify({task_id: task_id, status: pending}) app.route(/status/int:task_id) def scan_status(task_id): return jsonify(get_task_status(task_id)) app.route(/result/int:task_id) def show_result(task_id): session Session() task session.query(Task).filter_by(idtask_id).first() results session.query(Result).filter_by(task_idtask_id).all() session.close() return render_template(result.html, tasktask, resultsresults)前端页面用一个轮询脚本每3秒请求一次/status接口判断扫描状态。这个轮询接口返回的内容包括扫描进度比如当前检测到第几个插件、总共几个插件方便显示进度条。关于状态管理我在Task表里维护了status字段取值是pending、running、completed、failed。任务管理器启动线程前把状态改成running扫描完成后改成completed异常则改成failed。前端根据状态切换页面展示。4. 常见问题与排查技巧实录4.1 扫描任务卡死无响应怎么处理我遇到过的最大一个坑是扫描任务卡死。用户提交了一个目标URL线程池里开始跑然后状态一直是running但是页面上的进度条一动不动。排查后发现是底层socket连接卡住了目标服务器对某些端口不响应也不拒绝导致connect超时。默认socket超时时间非常长可能好几分钟才抛异常但用户等不了那么久。解决办法是在所有网络请求里显式设置timeout包括socket、requests库请求、Nmap扫描的参数。scanner_engine.py里设置了一个全局的SCAN_TIMEOUT配置默认10秒所有请求继承这个超时设置。如果某个端口扫描卡住超过10秒直接跳过该端口不影响整体进度。另外线程池中必须设置线程的daemon属性为True这样主进程退出时子线程不会阻塞程序退出。不然扫描中途关闭Flask应用终端会一直挂着提示线程未结束。4.2 依赖安装与版本兼容问题很多学弟学妹卡在第一步就是装依赖。requirements.txt里的版本一定要锁死不能写大于号。比如Flask直接用2.2.2requests用2.28.1python-nmap用0.7.1。不同版本之间API会变特别是Flask 2.3之后有些地方改了默认行为如果用的老代码可能不兼容所以在文档里给出明确版本号能省很多麻烦。还有一个坑是python-nmap 0.7.1与最新版Nmap 7.94配合没问题但如果用户没有安装Nmap这个库导入时不报错执行扫描时才报nmap not found的异常。我在信息搜集模块里加了前置检查如果检测不到nmap可执行文件端口扫描功能自动降级为socket扫描同时给前端提示避免整个任务崩掉。4.3 误报和漏报问题处理经验漏洞扫描最容易被答辩老师挑战的就是误报和漏报。完全没有误报是不现实的但我们可以控制误报率。我在每个插件返回结果时增加了一个score字段代表置信度。比如SQL注入插件发现响应里有数据库报错片段时置信度设为90仅发现页面差异明显但无报错特征时置信度设为60。结果页上只显示置信度大于等于60的漏洞。这个方法帮我在答辩时加分不少。老师问“你怎么保证结果可信”我就可以回答我们利用多种检测特征交叉验证并且用置信度过滤低可信结果。实际操作时也确实有效比如有些页面本身就有动态时间戳用布尔盲注法比较响应时会把这种干扰算进去通过置信度阈值能滤掉一部分干扰项。漏报问题就难处理一些因为不可能把所有漏洞都覆盖到。我的策略是文档里明确写了本系统的检测范围目前支持SQL注入、XSS、目录遍历三大类其他漏洞类型后续可扩展。诚实说明系统的局限性比掩盖问题的效果要好得多。4.4 数据库并发读写的小坑Flask的SQLAlchemy默认session不是线程安全的我在后台线程里写数据库时遇到了偶尔报错“SQLite objects created in a thread can only be used in that same thread”。之前没有加线程锁多个扫描插件同时往Result表插入数据时SQLite会因为跨线程访问报错。解决方法是每个线程创建独立的session用完即关。不要在多个线程间共用同一个session对象。我在task_manager.py里让每个扫描插件内部使用自己的Session()实例确保数据库连接只在当前线程内使用。同时SQLite文件可以被多个线程同时读但写操作需要串行化所以我给写入操作加了一个threading.Lock保证同一时间只有一个线程在写数据库。5. 实际使用效果与指标展示5.1 测试环境与性能表现我在本地搭建了一个测试环境用DVWADamn Vulnerable Web Application作为靶机这个环境本身就是用来做漏洞测试的包含SQL注入、XSS、文件包含等多种漏洞非常适合验收扫描系统。扫描流程走下来信息搜集阶段耗时在5到10秒之间包括DNS解析、端口扫描只扫常见端口、服务器指纹识别。漏洞扫描阶段三个插件依次执行SQL注入检测最耗时因为要对每个参数尝试多组payloadXSS检测速度较快目录遍历检测因为有了一批字典文件平均每个路径请求约耗时100毫秒加上10秒超时控制整体扫描时间控制在1分钟内。扫描结果能正确识别出DVWA里的SQL注入漏洞和反射型XSS目录遍历也能发现常见后台路径。对正常站点比如一个静态企业官网扫描时误报数量比较少大概每隔三四个站点会出现一条低置信度提示。5.2 系统安全性注意事项毕业设计项目交付的时候一定要在文档里写明系统的使用边界。这个系统的目的是帮助Web开发者发现自身网站的漏洞做安全加固用不是用来攻击别人的。我在Flask前端页面上增加了一个授权声明用户提交扫描任务时必须勾选“我确认已获得目标授权”才能继续。虽然这只是形式上的约束但作为教育项目这个设计是必要的也能体现你的安全意识。另外一个技术上的安全点是目标URL参数输入需要做合法性校验。防止有人通过提交恶意拼接的URL对扫描器发起命令注入或SSRF攻击。我用了urlparse解析目标检查scheme只能是http或httpsnetloc必须包含点号或localhost否则直接拒绝任务。6. 我个人的实操体会与建议做这个项目最大的体会是与其一开始就追求功能全面不如先把一条主链路跑通。我的开发顺序是先写好一个最简单的Flask页面能提交URL、在后端打印目标信息、再返回结果然后把信息搜集模块耦合进来再逐步加上漏洞检测插件。每一步都能看到实际运行效果调试起来有方向感。对于正在做类似毕设的同学我建议至少给它预留两周时间进行联调和写文档。这个项目本身代码量不大但坑多你永远猜不到是缺Nmap还是端口被防火墙挡了还是Flask版本问题。调试的时间往往比写代码的时间长提前做好心理准备。另外文档一定要写清楚环境搭建步骤包括Python版本最好是3.8到3.10、系统依赖Windows上要装Nmap并配置环境变量Linux上要用apt安装nmap、以及pip安装命令。我见过很多人项目源码能跑通但换台电脑就挂掉多半是环境变量没配好。最后一个小技巧为了增加项目亮点我还加了一个导出报告的功能扫描完成后可以下载一份HTML格式的扫描报告包含目标基本信息和每个漏洞的描述及修复建议。这个功能不复杂但在答辩演示时效果很好老师会觉得你的项目有闭环意识。如果你时间充裕可以考虑把前端页面做得更精致一些用卡片式布局展示漏洞列表再配合不同的颜色标识风险等级整体效果会提升一大截。本文还有配套的精品资源点击获取