Flask构建Web漏洞扫描系统:从信息搜集到漏洞检测
2026/8/31 3:08:35 网站建设 项目流程

简介:这是一套面向计算机、通信、人工智能等专业本科生的毕业设计级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 1=1)和恒假条件(and 1=2),分别请求,比较两个响应内容的差异。如果有明显差异,就说明参数可能参与了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(f'sqlite:///{config.DATABASE_PATH}', echo=False) Base = declarative_base() Session = sessionmaker(bind=engine) class Task(Base): __tablename__ = 'task' id = Column(Integer, primary_key=True, autoincrement=True) url = Column(String(500), nullable=False) status = Column(String(20), default='pending') created_at = Column(DateTime, default=datetime.now) results = relationship('Result', backref='task', cascade='all, delete-orphan') class Result(Base): __tablename__ = 'result' id = Column(Integer, primary_key=True, autoincrement=True) 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, timeout=config.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(id=task_id).first() results = session.query(Result).filter_by(task_id=task_id).all() session.close() return render_template('result.html', task=task, results=results)

前端页面用一个轮询脚本,每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.2,requests用2.28.1,python-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 测试环境与性能表现

我在本地搭建了一个测试环境,用DVWA(Damn Vulnerable Web Application)作为靶机,这个环境本身就是用来做漏洞测试的,包含SQL注入、XSS、文件包含等多种漏洞,非常适合验收扫描系统。

扫描流程走下来,信息搜集阶段耗时在5到10秒之间,包括DNS解析、端口扫描(只扫常见端口)、服务器指纹识别。漏洞扫描阶段,三个插件依次执行,SQL注入检测最耗时,因为要对每个参数尝试多组payload,XSS检测速度较快,目录遍历检测因为有了一批字典文件,平均每个路径请求约耗时100毫秒,加上10秒超时控制,整体扫描时间控制在1分钟内。

扫描结果能正确识别出DVWA里的SQL注入漏洞和反射型XSS,目录遍历也能发现常见后台路径。对正常站点(比如一个静态企业官网)扫描时,误报数量比较少,大概每隔三四个站点会出现一条低置信度提示。

5.2 系统安全性注意事项

毕业设计项目交付的时候,一定要在文档里写明系统的使用边界。这个系统的目的是帮助Web开发者发现自身网站的漏洞,做安全加固用,不是用来攻击别人的。我在Flask前端页面上增加了一个授权声明,用户提交扫描任务时必须勾选“我确认已获得目标授权”才能继续。虽然这只是形式上的约束,但作为教育项目,这个设计是必要的,也能体现你的安全意识。

另外一个技术上的安全点是:目标URL参数输入需要做合法性校验。防止有人通过提交恶意拼接的URL,对扫描器发起命令注入或SSRF攻击。我用了urlparse解析目标,检查scheme只能是http或https,netloc必须包含点号或localhost,否则直接拒绝任务。

6. 我个人的实操体会与建议

做这个项目最大的体会是:与其一开始就追求功能全面,不如先把一条主链路跑通。我的开发顺序是先写好一个最简单的Flask页面,能提交URL、在后端打印目标信息、再返回结果;然后把信息搜集模块耦合进来;再逐步加上漏洞检测插件。每一步都能看到实际运行效果,调试起来有方向感。

对于正在做类似毕设的同学,我建议至少给它预留两周时间进行联调和写文档。这个项目本身代码量不大,但坑多,你永远猜不到是缺Nmap还是端口被防火墙挡了还是Flask版本问题。调试的时间往往比写代码的时间长,提前做好心理准备。

另外,文档一定要写清楚环境搭建步骤,包括Python版本(最好是3.8到3.10)、系统依赖(Windows上要装Nmap并配置环境变量,Linux上要用apt安装nmap)、以及pip安装命令。我见过很多人项目源码能跑通但换台电脑就挂掉,多半是环境变量没配好。

最后一个小技巧:为了增加项目亮点,我还加了一个导出报告的功能,扫描完成后可以下载一份HTML格式的扫描报告,包含目标基本信息和每个漏洞的描述及修复建议。这个功能不复杂,但在答辩演示时效果很好,老师会觉得你的项目有闭环意识。如果你时间充裕,可以考虑把前端页面做得更精致一些,用卡片式布局展示漏洞列表,再配合不同的颜色标识风险等级,整体效果会提升一大截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询