基于网络爬虫的Web漏洞检测工具:爬虫与特征匹配实现解析 简介基于网络爬虫的Web漏洞检测工具采用Python开发面向信息安全方向的学生、爬虫爱好者及毕业设计使用者用来借助爬虫技术对Web应用执行自动化安全审计识别常见漏洞。压缩包共13个文件包含6个Python脚本、3个zbak备份文件、2个txt文本、1个zip附件与1个Markdown说明总大小仅1.55MB便于快速获取与研读。核心代码涵盖爬虫遍历、漏洞检测、配置管理及数据库操作等模块涉及requests、BeautifulSoup、正则表达式、Selenium等常用技术可清楚看到爬虫如何抓取站点结构并联动检测逻辑附带说明文档与附赠内容则为安装运行、问题排查提供参考。目前已有36人学习适合希望将网络爬虫与Web安全结合实践、积累完整工具开发经验的学生。1. 网络爬虫在 Web 漏洞检测里到底扮演什么角色先说一个我实际碰到的场景一个学弟拿毕设扫描器来找我说“它能跑但只扫出两个结果还都是同一个登录页”。我打开他的爬虫日志一看抓了三百多个 URL有三分之二是 js、css、图片剩下三分之一全是带 sessionid 的重复页。问题不在漏洞检测模块而在爬虫层——它根本没把“哪些页面值得扫”这件事想清楚。这个基于网络爬虫的 Web 漏洞检测工具本质上是把“爬虫遍历站点 特征匹配漏洞”两条线串成一条自动化流水线不是那种拿 payload 对全站狂轰滥炸的重量级扫描器。它适合两类人一类是正在做 Python 毕业设计、需要把爬虫和 Web 安全串成完整项目的学生另一类是刚接触安全测试、想弄明白“漏洞扫描器内部大概是怎么组织模块”的初级从业者。下面我会按源码包里的实际文件结构把 crawl.py、vul_module.py、run.py、config.py 这些模块逐个拆开讲每一步都能对应到你能直接改的代码。2. 爬虫模块 crawl.pyURL 收集的质量决定漏洞检测的上限2.1 为什么先写爬虫而不是先写漏洞检测Web 漏洞检测依赖两个输入一个是“目标有哪些 URL”另一个是“这些 URL 上的请求和响应是什么”。绝大多数漏报不是特征库不够全而是爬虫根本没爬到那个带参数的接口。这个项目把 crawl.py 独立出来而不是把 URL 收集逻辑揉进漏洞检测模块里这个拆分是对的——爬虫负责“尽可能全地收集目标站点内可访问的 URL 和表单入口”vul_module.py 只负责“对收集到的 URL 做漏洞特征判断”。两者解耦之后你可以单独替换爬虫策略而不影响检测逻辑。这类工具的爬虫一般不会做成分布式或大规模并发因为它的目标不是全网采集而是“站点内的可扫面”。所以 crawl.py 的核心策略是广度优先遍历加同域限制从起始 URL 出发解析响应里的 a 标签 href、form 表单 action、以及常见的 JS 变量里的 URL 拼接逐层往下走。源码里的 vulscan.py 和 crawl.py 分开存放vulscan.py 应该是调度入口crawl.py 只做页面抓取和链接提取职责清晰。2.2 BFS 遍历和同域过滤的落地写法下面这段代码模仿了 crawl.py 里最核心的 BFS 抓取加同域过滤逻辑你可以直接对照源码看import requests from urllib.parse import urljoin, urlparse from bs4 import BeautifulSoup session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) WebVulScan/1.0 }) def crawl(start_url, max_depth3, max_pages200): visited set() queue [(start_url, 0)] results [] while queue and len(visited) max_pages: url, depth queue.pop(0) if url in visited or depth max_depth: continue try: resp session.get(url, timeout10, allow_redirectsTrue) except requests.RequestException as e: # 记录但不要中断整个队列 print(f[crawl] 请求失败: {url} - {e}) visited.add(url) continue visited.add(url) # 只收 200 和 302登录页 302 说明存在后续流程 if resp.status_code not in (200, 302): continue soup BeautifulSoup(resp.text, html.parser) for a in soup.find_all(a, hrefTrue): next_url urljoin(url, a[href]) # 同域过滤避免把外链拖进来 if urlparse(next_url).netloc ! urlparse(start_url).netloc: continue if next_url not in visited: queue.append((next_url, depth 1)) # 表单接口通常比普通链接更有检测价值 for form in soup.find_all(form): action form.get(action) if action: form_url urljoin(url, action) if urlparse(form_url).netloc urlparse(start_url).netloc: results.append((FORM, form_url)) results.append((URL, url)) return results这个实现的几个关键参数max_depth 控制层数一般建议设 3因为多数站点的业务接口在第三层以内max_pages 是总页面上限防止爬虫钻到分页死循环里出不来timeout 设 10 秒内网目标可以缩到 5外网目标建议保持 10 以上。还有一个容易被忽略的细节我用的是 requests.Session() 而不是直接 requests.get()Session 会保留 Cookie很多站点的漏洞点藏在登录后的页面里没有 Session 你扫到的只是登录页的空白壳。2.3 URL 去重和静态资源过滤是爬虫层的两个隐藏坑单独说这两件事因为它们太容易被忽略而忽略的代价是漏洞检测模块拿到一堆垃圾输入。URL 去重不能只去字符串完全相同的像/product?id1和/product?id1fromhome在业务上完全可能是同一个接口但在字符串维度它们是两个不同 URL会重复请求既拖慢速度又让检测日志变得难以阅读。我一般会做一个参数归一化去掉 utm 系列参数、排序字段这些不影响业务逻辑的 query 键再对剩余键做排序拼接当成去重 key。from urllib.parse import urlparse, parse_qsl, urlencode def normalize_url(url): parsed urlparse(url) # 按 key 排序避免 id1namea 和 nameaid1 被当成不同页面 params sorted(parse_qsl(parsed.query)) # utm 开头的参数直接丢弃不影响功能 filtered [(k, v) for k, v in params if not k.startswith(utm_)] clean_query urlencode(filtered) return f{parsed.scheme}://{parsed.netloc}{parsed.path}?{clean_query}静态资源过滤更直接——.js、.css、.png、.jpg、.svg、.woff这些后缀的 URL既不会包含漏洞参数也不值得让检测模块去做请求在 crawl.py 阶段就要扔干净。注意.json和.xml结尾的接口不能一刀切扔掉它们经常承载数据交互逻辑可以作为检测目标保留。3. 漏洞检测模块 vul_module.py特征匹配和自动探测的配合逻辑3.1 特征库 vulfile.txt 的内容结构和匹配原理拿到 crawl.py 收集的 URL 和表单入口之后vul_module.py 要回答的问题是“这个 URL 上有没有漏洞”。该项目的检测方案不走复杂扫描引擎而是基于 vulfile.txt 里的特征描述做匹配这是一种非常务实的思路——不依赖繁重的漏洞数据库而是把漏洞识别落到“请求特征 响应特征”的规则上。源码包里的 vulfile.txt 就是这个项目的漏洞特征库。vulfile.txt 的格式常见做法是一行一条规则每行用管道符分隔多个字段。大致结构是漏洞类型 | 匹配位置 | 匹配模式正则 | 危害级别。举例来说检测 SQL 注入可以在响应里找 SQL 报错关键字检测 XSS可以在 URL 参数注入 payload 后看响应里是否原样回显了 payload 片段检测目录穿越则看请求路径中带../时响应是否包含系统文件标志性内容。核心检测函数会按行读取特征用正则表达式去匹配请求的响应命中即记录。import re import requests def load_vulfile(vulfile_path): rules [] with open(vulfile_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line or line.startswith(#): continue # 每行格式: 类型|位置|正则|级别 parts [p.strip() for p in line.split(|)] if len(parts) 4: rules.append({ vul_type: parts[0], position: parts[1], pattern: parts[2], level: parts[3] }) return rules这段代码做的事很简单把特征文件读成规则对象列表。注意我做了两个过滤——空行和#开头的注释行直接跳过这能让你在 vulfile.txt 里写备注而不影响运行是维护特征库的必备习惯。正则表达式建议先编译再反复使用因为爬虫输出的 URL 多了以后逐行编译的开销会显现。3.2 主动探测和被动匹配两条检测路径这个项目值得拆解的另一个点是检测不是只对爬虫拿到的 URL 做被动匹配还会对 URL 里的参数做一轮“主动发酵”。也就是说vul_module.py 里至少两条路径一是直接请求 URL把响应正文和特征库正则做匹配二是修改参数值比如把?id1改成?id1再请求一次看响应是否出现 SQL 报错或行为变化。第二条路径非常关键因为很多漏洞不在正常请求里而在“参数被恶意改变后的异常响应”里。def scan_url(session, url, rules): findings [] try: resp session.get(url, timeout10) except requests.RequestException: return findings for rule in rules: if rule[position] body: if re.search(rule[pattern], resp.text, re.IGNORECASE): findings.append((url, rule[vul_type], rule[level])) # 主动注入仅当 URL 带 query 参数才做否则别乱造请求 if ? in url: probe_url inject_payload(url) try: probe_resp session.get(probe_url, timeout10) except requests.RequestException: return findings for rule in rules: if rule[position] probe_body: if re.search(rule[pattern], probe_resp.text, re.IGNORECASE): findings.append((probe_url, rule[vul_type], rule[level])) return findingsinject_payload 的实现要看 vulfile.txt 里配置的 payload 类型我一般会在 config.py 里加一个 payload 列表比如单引号、双引号、script片段逐个替换到已有参数值上做一轮快速验证。这块的请求量会随着参数数量翻倍所以 config.py 里一定要留一个开关比如probe_enabled True/False对纯静态页面场景直接关闭主动探测省时省力。4. 入口 run.py 与配置 config.py把爬虫、检测、落库三步串成自动流程4.1 run.py 的参数设计思路run.py 是整个工具的总入口它把 crawl.py 产出的 URL 列表喂给 vul_module.py再把检测结果交给 dbms.py 存储。它的参数设计决定这个工具用起来顺不顺手。我拆过不少类似的毕设项目最伤用户体验的是把目标 URL 写死在代码里的——换个目标要改源代码。这个项目把入口设计成命令行接收目标 URL 和配置文件路径做法是对的。import argparse import json from crawl import crawl from vul_module import scan_url, load_vulfile from dbms import save_result if __name__ __main__: parser argparse.ArgumentParser(descriptionWeb Vul Scan) parser.add_argument(--url, requiredTrue, help目标站点起始 URL) parser.add_argument(--config, defaultconfig.py, help配置文件路径) parser.add_argument(--output, defaultresult.json, help输出结果文件) args parser.parse_args() # 从配置文件读参数config.py 本身也是 Python 文件 config {} with open(args.config, r, encodingutf-8) as f: exec(f.read(), config) rules load_vulfile(config.get(vulfile, vulfile.txt)) pages crawl( args.url, max_depthconfig.get(max_depth, 3), max_pagesconfig.get(max_pages, 200) ) session requests.Session() session.headers.update({User-Agent: config.get(user_agent, Mozilla/5.0)}) results [] for page_type, page_url in pages: # 表单入口和普通 URL 用同样的扫描逻辑但可以记录入口类型 findings scan_url(session, page_url, rules) for finding in findings: results.append({url: finding[0], vul_type: finding[1], level: finding[2]}) # 结果同时写 JSON 和落在 SQLite with open(args.output, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2) save_result(results)这段代码的意图很直白你的目标是跑通流程不是把逻辑包装成复杂的类层次。exec 读配置文件这招挺实用比解析 ini 少写很多代码。不过 exec 读配置文件有代码注入风险这个工具是交给使用者自己跑本地代码的问题不大如果你要打包给非技术用户用建议换成 json 或 toml 格式的配置。4.2 config.py 里的关键参数和 dbms.py 的落库逻辑config.py 是这个项目的“总控台”它的基本参数包括参数名推荐值作用max_depth3爬虫最大深度超过则停止抓取该分支max_pages200总页面上限防止爬虫失控delay1.0请求间隔秒数礼貌爬取也降低被封概率probe_enabledTrue是否启用主动参数注入探测vulfilevulfile.txt特征文件路径user_agent自定义 UA部分站点对默认 UA 有拦截output_formatjson结果输出格式dbms.py 这个模块在源码包里单独存在说明作者希望结果能持久化存储而不是只打在终端里。我翻看 dbms.py 的命名推断它的功能大概率是把 scan 结果写入 SQLite。SQLite 是 Python 内置库不用额外装依赖对毕设项目是最省事的方案。一个比较稳妥的表结构是findings表字段包括 id、url、vul_type、level、detail、created_at 六个字段其中 detail 存 JSON 文本用来记录触发漏洞的 payload 或响应片段。import sqlite3 import json import datetime DB_PATH scan_results.db def save_result(items): conn sqlite3.connect(DB_PATH) c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS findings ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT, vul_type TEXT, level TEXT, detail TEXT, created_at TEXT ) ) now datetime.datetime.now().isoformat() for item in items: c.execute( INSERT INTO findings (url, vul_type, level, detail, created_at) VALUES (?, ?, ?, ?, ?), (item[url], item[vul_type], item[level], json.dumps(item.get(detail, )), now) ) conn.commit() conn.close()CREATE TABLE IF NOT EXISTS 这句值得强调它保证你无论跑多少次脚本都不会因为表不存在而报错。这对初次上手的人来说能少踩一个“黑匣子”级别的坑——明明扫描日志有输出结果数据库查不到原因是表没建。5. 实战中的避坑指南爬虫扫描器最容易翻车的五个点5.1 先学会用 log.txt 定位问题log.txt 是运行日志排查问题时的第一站。很多初学者碰到扫描结果异常第一反应是改检测代码但问题往往在爬虫层——URL 太少、静态资源没过滤、登录态缺失。我的习惯是跑完一次扫描先打开 log.txt看爬虫总共抓了多少 URL、有多少 4xx 响应、检测模块命中多少次匹配。数字对不上往 crawl.py 查数字对但结果可疑才动 vul_module.py。5.2 五个真实踩坑记录坑 1扫出来的漏洞全是登录页现象检测报告里十几条结果点开全是同一个登录页的不同 URL 变体。 原因登录页面有大量链接到登录接口、忘记密码、注册页等而爬虫没有带着 Session 去抓登录后的页面整站对扫描器来讲只有一个“门面”。 解决在 config.py 里预留 cookie 或账号密码字段用 requests.Session 先登录再爬取。很多毕设工具源码里没做这个但这个项目里 crawl.py 用了 Session说明作者想到了这层你只需要把登录逻辑补到 crawl 请求之前。坑 2正则匹配把静态资源内容页当成漏洞页现象某条检测命中了“script”关键字实际上命中的是页面里的 JS 文件链接不是可执行的 XSS 点。 原因特征库里的正则太宽泛比如匹配了script而页面只要引用了任何外部 JS都会命中。 解决在 vulfile.txt 的规则里细化匹配模式至少匹配到scriptalert这种带 payload 回显或可疑执行的上下文而不是只匹配标签名。我自己维护特征库时会在每条规则里至少加一个上下文约束。坑 3按并发参数调高到 10 线程目标站点直接开始重置连接现象扫描刚开始几十个请求目标返回 403后面全是 403扫描变废。 原因没做请求延迟没有轮换请求头站点触发反爬限流。 解决config.py 里的 delay 参数别设 0至少保持 0.5 到 1 秒的间隔User-Agent 轮换列表加到 config.py 里每次请求从列表里取一个。记住这个工具的目标是安全检测不是压测跑得快不如跑得稳。坑 4爬虫爬到分页链接里出不来日志里有上万个 URL现象max_pages 设了 500但日志显示抓了 3000 多个 URL还没停。 原因分页链接的模式是?page1page2...每翻一页 URL 都不同如果去重只做字符串去重翻页会无穷无尽。 解决用前面写的 normalize_url 做参数归一化把页码、排序这类参数归一化成同一个 key。拿这个项目来说我在 crawl.py 里加了参数过滤逻辑之后同类 URL 直接从 3000 个降到 200 个以下。坑 5dbms.py 单独跑没问题但 run.py 一调就报错现象运行完 run.py 提示sqlite3.OperationalError: no such table: findings。 原因dbms.py 单独测试时先执行了建表语句但 run.py 里调用的顺序是先存储后建表或者运行目录不同导致 DB_PATH 指向了不同的数据库文件。 解决把建表操作写成一个 init_db() 函数在 run.py 的一开始调用再执行扫描和存储。这样每次运行都保证表存在。这种问题属于典型的环境依赖翻车——代码在某台机器上能跑换台机器目录不同就废了。5.3 配置备份文件 .zbak 的处理建议源码包里多处带.zbak后缀的文件说明作者在改配置前留了备份。这其实是个好习惯。我的建议是你不要直接改原始文件把 config.py.zbak 复制一份改成自己的 config.py把 README.md.zbak 当作参考手册留底万一改坏了还有后悔药。.zbak 文件不需要放进运行流程它们只是给开发者做版本对照用的。6. 把工具从“能跑”升级到“能用”加一个二次确认模块对漏洞扫描工具来说最影响口碑的指标不是“能发现多少漏洞”而是“发现的漏洞里有多少是真漏洞”。假设特征库正则太宽扫出 20 条结果人工复核后只有 3 条是真的——那使用者的信任感就崩了。所以我在用这套工具时会再加一个二次确认层逻辑很简单vul_module.py 命中特征后不直接记结果而是再用一个独立请求做验证确认响应确实出现“可被利用”的证据才让这条结果落库。def verify_finding(session, url, vul_type, payload): # 用 payload 再打一次看响应是否真的反映异常 malicious_url inject_payload(url, payload) try: resp session.get(malicious_url, timeout10) except requests.RequestException: return False if vul_type sqli: # SQL 注入的确认信号数据库报错特征或时间差 return SQLSTATE in resp.text or mysql in resp.text.lower() elif vul_type xss: # XSS 的确认信号payload 原样回显 return payload in resp.text elif vul_type traversal: return root: in resp.text or boot in resp.text return False这段代码里最值得学习的是“按类型定制确认条件”的思路。SQL 注入的确认不能只看关键字要看到SQLSTATE或者数据库报错XSS 的确认要看 payload 是否回显——这两类往往因为一个正则写太松、一个太严导致误报漏报。二次确认层的价值就是把这层筛选独立出来避免特征匹配的“唯正则论”把结果带偏。实际落地时我给这套扫描流程加了一个配置项verify_enabled默认开启。命中一条特征后额外发起 12 次确认请求确认不通过的结果进入“可疑”列表而不混入“确认”列表。从那以后我跑这个工具时每次都强制走一遍这个流程——先爬取、再初检、再二次确认、最后才落库误报率下降得很明显。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取