基于Python网络爬虫的Web漏洞检测工具原理与实现解析
发布时间:2026/10/1 23:00:57来源:尧图网络
简介这是一份基于Python爬虫技术的Web漏洞检测工具源码包面向计算机专业学生、毕业设计开发者以及Web安全初学者旨在帮助读者理解如何利用爬虫遍历网站并识别常见安全漏洞。项目整合requests、BeautifulSoup、正则表达式、Selenium等爬取与解析技术代码模块划分清晰可直接作为网络安全课程设计或毕业设计的基础框架。压缩包共13个文件整体体积约1.55MB6个Python源码文件覆盖爬虫抓取、漏洞检测模块、数据库管理、主控运行等流程3个zbak配置备份文件可用于排错恢复和版本对比2个文本文件记录漏洞字典与运行日志另有1个Markdown说明文档和1个附赠内容包结构紧凑且便于分角色阅读。目前已有36人学习/下载。通过完整源码和配套说明读者能快速搭建运行环境观察爬虫如何解析页面、构造请求并输出扫描结果参照配置与日志理解常见Web漏洞的检测逻辑附赠内容包含图片与教程有助于以可视化方式掌握项目运作。资源整体轻量适合本地实验和二次开发。1. 爬虫驱动的Web漏洞检测这套Python源码能不能直接上手用拆开「基于网络爬虫的Web漏洞检测工具」这个源码包第一感觉是结构比想象中干净crawl.py、vulscan.py、dbms.py、config.py四个核心文件加一份README没有花架子。它的逻辑一句话能讲明白——网络爬虫负责把目标站点的URL和页面内容尽量抓全漏洞检测模块再去匹配特征库里的XSS、SQL注入、敏感信息等模式命中就写进SQLite存档。这套路子的实际意义在于站点链接一多手工点页面测漏洞既不现实也不完整而爬虫能二十秒把首页、文章页、搜索页全逛一遍检测模块再接力筛查十分钟跑完以前半小时的活。适合正在做毕业设计、想从爬虫往Web安全方向串知识的人也适合想研究「网络爬虫原理怎么在安全场景落地」的开发者。能不能直接拿来用能但前提是你把每个文件读到能背出关键参数的程度否则跑出来的报告根本不值得信。2. 网络爬虫原理落地crawl.py遍历、去重与请求伪装2.1 广度优先与URL去重先把站点的链接掏干净我拆任何爬虫项目有个习惯先找crawl.py因为整份工具的探索能力全看它。这份代码的核心是经典的广度优先遍历从一个种子URL出发放进待抓队列每取一个URL就发请求、解析HTML里的a标签和表单action提取出新的链接再入队同时用一个visited集合记录已经访问过的地址防止死循环。BFS在漏洞扫描场景是合理选择——目标站点的拓扑结构你事先不知道广度优先保证先覆盖浅层页面能以最快速度拿到站点入口、导航页和目录页。对比深度优先后者容易一头扎进某个文章详情页的内部死胡同等爬回来时队列里已经堆了几百个无关链接。# crawl.py 简化核心逻辑BFS遍历 URL规范化去重 import requests from bs4 import BeautifulSoup from urllib.parse import urlsplit, urlunsplit, urljoin, urlparse class Crawler: def __init__(self, seed_url, max_depth3, timeout10): self.seed_url seed_url self.queue [seed_url] self.visited set() self.max_depth max_depth self.timeout timeout self.session requests.Session() def normalize_url(self, url): URL去重前先规范化去掉锚点、统一小写、处理结尾斜杠 scheme, netloc, path, query, fragment urlsplit(url) path path.rstrip(/) or / return urlunsplit((scheme.lower(), netloc.lower(), path, query, )) def extract_links(self, html, base_url): 解析页面里的a标签拼接完整URL同时滤掉外域链接 links [] soup BeautifulSoup(html, html.parser) for a in soup.find_all(a, hrefTrue): full_url urljoin(base_url, a[href]) if urlparse(full_url).netloc urlparse(base_url).netloc: links.append(self.normalize_url(full_url)) return links def crawl(self): 从队列取URL逐层遍历直到队列耗尽或达到深度上限 while self.queue: url self.queue.pop(0) url self.normalize_url(url) if url in self.visited: continue self.visited.add(url) try: resp self.session.get(url, timeoutself.timeout) if resp.status_code 200: for link in self.extract_links(resp.text, url): if link not in self.visited: self.queue.append(link) except requests.RequestException as e: print(f请求失败: {url} - {e})逻辑说明normalize_url这一步很容易被新手漏掉但它恰恰是去重的关键。同一个http://site.com/a#section1和http://site.com/a#section2如果不剥掉锚点会被当成两个URL重复抓取路径末尾的斜杠同理/about和/about/在很多服务器上指向同一个页面。不规范化就去重爬虫会把资源浪费在重复请求上扫描效率直降一半。参数说明max_depth控制遍历深度对课设场景3层足够——首页、列表页、详情页各占一层再深就是个人中心、后台这类对扫描器意义不大的区域。pop(0)是拿列表模拟队列的写法URL量在几百这个量级时完全够用但到了几千条会明显变慢届时要换成collections.deque。2.2 请求头伪装与退避重试别让requests一眼被认出来看到crawl.py里预留了HEADERS的位置但没有默认值时我就知道这是把「填不填、怎么填」的责任丢给了使用者。很多人第一次跑扫描器requests带着默认的User-Agent就出门了——python-requests/2.x这种标识哪怕对方站点没上什么高级防护一眼也能认出是脚本在访问第二个请求就该被拒了。# 请求头配置把爬虫伪装成正常浏览器访问 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: http://127.0.0.1:8080/, } def fetch_with_retry(session, url, retries3, timeout5): 带重试的GET请求超时、限流都让它等等再说 for attempt in range(retries): try: resp session.get(url, headersHEADERS, timeouttimeout) if resp.status_code 200: return resp elif resp.status_code in (403, 429): # 被限流就递增等待别硬刚 time.sleep(2 * (attempt 1)) else: return resp except requests.RequestException: if attempt retries - 1: raise time.sleep(1) return None逻辑说明sleep(2 * (attempt 1))是我在安全类爬虫里常用的简易退避策略——403代表服务器识别出你是脚本429代表请求频率触发了限流阈值这两种情况都不适合继续猛打等几秒再试反而有希望。普通爬虫可以无视状态码继续抓漏洞扫描不行你需要在对方的WAF还没把你拉进黑名单之前让整个扫描过程尽可能「像人」。参数说明retries3对课设够用我扫外网授权站点习惯设5同时把sleep改成随机区间time.sleep(random.uniform(0.5, 1.5))避免固定间隔被特征检测。Referer别照抄示例里的本地地址拿一个目标站点同域名的历史来源页填进去伪装度会高一个档。如果目标站点需要登录态还要在session里手动塞Cookie——F12从浏览器复制当前会话的Cookie字典或者用requests.utils.cookiejar_from_dict转成cookies参数。提示HEADERS里别只改User-Agent就完事Accept-Language缺失会让某些严格校验的服务端直接拒绝因为正常浏览器一定会带这个头。3. Web漏洞检测核心特征库匹配与payload实证3.1 vulfile.txt特征库与vulscan.py匹配机制拆开vulscan.py最想确认的是它凭什么判断一个页面「有问题」读完之后答案是朴素的——特征库也就是vulfile.txt。这个文件每一行是一条漏洞特征最典型的格式是三段式漏洞类型、正则表达式、风险等级用竖线分隔。检测逻辑就是拿爬虫抓回来的响应内容对每一条正则做re.search命中就记为一条漏洞记录。# vulscan.py 简化核心逻辑加载特征库 正则匹配 import re def load_vuln_rules(filepathvulfile.txt): 读取漏洞特征库返回[(漏洞类型, 正则pattern, 风险等级)] rules [] with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if not line or line.startswith(#): continue parts line.split(|) if len(parts) 3: rules.append((parts[0], parts[1], parts[2])) return rules def scan_page(url, html, rules): 对单个页面做特征匹配命中就返回漏洞记录 findings [] for vuln_type, pattern, risk in rules: try: if re.search(pattern, html, re.IGNORECASE): findings.append({ url: url, vuln_type: vuln_type, risk: risk, matched: pattern, }) except re.error as e: print(f规则正则编译失败: {pattern}, 错误: {e}) return findings逻辑说明re.IGNORECASE在扫描场景是必备的——同一个页面里script可能出现在Script、SCRIPT各种写法里不忽略大小写就会漏报。编码方面我统一用utf-8读特征库如果原作者的vulfile.txt是按GBK存的打开会直接乱码甚至抛异常扫描结果就全废了。参数说明规则格式类型|正则|等级是我拆包时根据vulfile.txt推断的默认结构你解压后如果格式有出入以实际文件为准。risk分high/medium/low三档输出报告时按等级降序排列人工复核时优先盯高风险条目。这套逻辑的本质是黑名单匹配天然有局限——它只能发现你特征库里写过的漏洞模式遇到变种或0day就是睁眼瞎这是所有基于特征的扫描器的共性天花板不丢人但你要心里有数。3.2 从静态匹配到实证探测XSS与SQL注入需要动态验证静态匹配有个致命问题它只验证「响应内容长得像漏洞」不验证「这个漏洞真的能被触发」。一个搜索页面把用户的输入原样回显到结果里这确实是XSS的温床但能不能真打进去取决于回显位置的上下文——反射在script标签内和反射在onclick属性里payload写法完全不同静态匹配根本区分不了。# 动态验证把payload塞进URL参数再检查响应里有没有回显 import urllib.parse XSS_PAYLOADS [ scriptalert(vul)/script, img srcx onerroralert(1), ] SQLI_PAYLOADS [ , 1 OR 11, OR 11 --, ] def active_verify(session, base_url, param_name, payload_list): 替换目标参数值发送请求回显标记出现则视为疑似漏洞 results [] parsed urllib.parse.urlparse(base_url) params urllib.parse.parse_qs(parsed.query) for payload in payload_list: test_params dict(params) test_params[param_name] [payload] test_url urllib.parse.urlunparse( parsed._replace(queryurllib.parse.urlencode(test_params, doseqTrue)) ) resp session.get(test_url, timeout5) if payload in resp.text: results.append((param_name, payload, 疑似存在)) return results逻辑说明这里把「看到特征就报漏洞」升级成「发送payload再验证回显」——payload真的出现在响应里至少证明参数值被服务端接收并反射了出来这是后续利用的前提。主动验证比纯静态匹配多一次实际请求代价是扫描时间变长、触发WAF的概率变大所以我把timeout压到5秒并且只在静态匹配命中的页面上做这一步。参数说明img srcx onerroralert(1)比script好用因为它的标签不依赖闭合条件即使用户输入被截断、引号被转义onerror事件依然有可能触发。SQL注入里的单引号是用来试探报错的——响应中出现数据库错误堆栈基本就能确认存在注入入口1 OR 11则是验证布尔型注入的更完整payload。注意这套验证只确认「存在回显」不确认「能实际利用」中间还隔着编码、过滤、WAF这几道关卡最终结论要人工复核后下。4. 数据落地与参数控制dbms.py存储与config.py调优4.1 SQLite建表与去重约束扫描结果怎么存才不乱扫描器跑完不能只在控制台打几行print就完事得把结果落盘下次还能查。dbms.py在这个项目里承担的是数据层角色建表、写库、查询、去重。选SQLite是合理决定——Python标准库自带sqlite3模块不用装MySQL客户端单文件存储答辩演示时拷一个.db文件就能带走零部署成本。-- scan_results 表核心字段与去重约束 CREATE TABLE IF NOT EXISTS scan_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, scan_time TEXT NOT NULL, url TEXT NOT NULL, param TEXT, vuln_type TEXT NOT NULL, risk_level TEXT DEFAULT medium, detail TEXT, UNIQUE(url, param, vuln_type) );逻辑说明UNIQUE(url, param, vuln_type)是整个去重体系的灵魂。爬虫在遍历过程中极可能重复访问同一页面没有这个约束一个XSS漏洞会被记录十几条扫描报告变成一坨重复列表人工复核时根本分不清哪些是真实漏洞哪些是噪音。配套写法是下一条插入用INSERT OR IGNORE重复记录会被数据库直接忽略掉。# dbms.py 简化核心逻辑INSERT OR IGNORE 配合UNIQUE去重 import sqlite3 from datetime import datetime def insert_finding(cursor, finding): 插入漏洞记录重复记录由UNIQUE约束直接忽略 now datetime.now().strftime(%Y-%m-%d %H:%M:%S) cursor.execute( INSERT OR IGNORE INTO scan_results (scan_time, url, param, vuln_type, risk_level, detail) VALUES (?, ?, ?, ?, ?, ?), (now, finding[url], finding.get(param), finding[vuln_type], finding[risk_level], finding.get(detail)) )参数说明scan_time用ISO格式的字符串存储2026-03-14 10:22:31这种写法排序直接按字典序比Unix时间戳直观也方便按天分组统计扫描轮次。detail字段建议只存命中正则的那段原文截前100个字符就足够——后面人工复核时需要看上下文但不需要把所有页面原文都塞进数据库否则文件体积膨胀得很快。4.2 config.py参数表与线程数调优先跑单线程不是怂config.py在多数组件里扮演「所有可变参数的集中地」这个项目也一样。我把几个真正影响扫描效果的参数列成了一张表参数默认值作用调整建议max_depth3爬虫最大遍历深度本地靶场可以提到5外网站点降到2防超时max_urls200最多抓取URL数课设200够用演示大站点再往上调thread_num1并发线程数先1线程跑通再逐步升到3-5timeout10单请求超时秒数外网扫描建议5本地靶场可以放宽到15retry_times3失败重试次数网络不稳定保持3内网环境可降到1# config.py 核心参数配置 MAX_DEPTH 3 MAX_URLS 200 THREAD_NUM 1 TIMEOUT 10 RETRY_TIMES 3 # 目标站点配置先本地靶场跑通再换授权站点 SEED_URL http://127.0.0.1:8080/ DB_FILE scan_results.db LOG_FILE log.txt逻辑说明THREAD_NUM从1开始是踩过坑之后改的习惯。爬虫开多线程顶多慢一点但漏洞扫描不一样——并发线程同时发出大量带payload的请求目标站点的WAF和反爬系统会立刻警觉403响应可能比扫描结果来得更快。线程数要加也必须在fetch_with_retry里配合随机延迟一起加而不是只改一个数字就完事。参数说明SEED_URL指向本地8080端口的靶场这是最稳的起步策略——先用DVWA这类自带漏洞的靶场环境验证扫描器确认误报漏报都收敛了再扫授权站点。max_urls和max_depth是双重刹车深度控制层数总量控制规模任何一个先到阈值都会自动停避免扫描器在某个动态生成页面的站点上无限跑下去。5. 避坑指南反爬拦截、动态渲染与编码乱码排查5.1 现象爬虫第二个页面就被403拦死日志全是Forbidden原因requests的默认User-Agent暴露身份加上扫描频率太快目标站点的基础反爬规则直接把你列为异常访客。很多课设项目第一次跑真实站点基本都死在这一步。解决必填完整浏览器请求头尤其是User-Agent和Accept-Language再加退避重试。我一般会在fetch_with_retry里把403和429单独拎出来处理收到这两个状态码就指数退避而不是无脑重试——重试次数过多且间隔固定会被反爬系统进一步盯上。5.2 现象爬虫抓到一堆空壳页面正文内容全是空白原因目标站点是Vue、React这类前端框架做的SPA单页应用内容靠JavaScript在浏览器里动态渲染。requests直接请求拿到的HTML是空壳只有div idapp骨架真正的链接和文本全部通过ajax异步加载后填充。解决先确认页面是否需要渲染再看要不要上Selenium。简单判断方法是拿响应文本搜script src如果页面里都是打包后的JS文件而没有实质内容基本就是SPA。Selenium引入成本不低——要装浏览器驱动、速度慢、资源占用高但面对SPA是唯一可行的路线。我一般会用Selenium只抓动态链接拿到链接列表后仍用requests做漏洞检测这样兼顾覆盖面和速度。5.3 现象扫描结果里莫名出现大量「疑似漏洞」人工复核发现全是乱码文本原因响应解码出了问题。requests的resp.text默认按响应头里的charset解码如果目标站点没写charset或者写了utf-8但页面实际是GBK编码就会得到乱码。乱码内容里碰巧出现某个字符组合命中了过宽的正则就制造一个假漏洞。解决解码时不要全信响应头用resp.apparent_encoding做兜底resp session.get(url, timeout5) # 优先用meta声明的编码缺失时让requests按字节内容推断 resp.encoding resp.apparent_encoding or gbkapparent_encoding是requests根据字节内容自动推断编码对中文页面准确率明显更高代价是推断过程稍慢所以只在不确定编码时才启用。5.4 现象扫描跑到一半中止log最后一条是某个请求抛异常原因crawl.py里的异常处理粒度不够。很多初版代码只包了一层try...except requests.RequestException但异常类型之间差别很大——连接超时是该跳过的SSL证书错误也是该跳过的数据库写入失败却需要终止整个流程。一刀切处理的结果是不该死的地方死了该停的地方反而没停。解决把异常按可恢复和不可恢复分开兜底def safe_crawl(crawler, url): try: crawler.process_url(url) except requests.Timeout: print(f超时跳过: {url}) # 可恢复记录后继续 except requests.ConnectionError: print(f连接失败跳过: {url}) # 可恢复记录后继续 except sqlite3.Error as e: raise SystemExit(f数据库错误终止扫描: {e}) # 不可恢复5.5 现象误报率高到无法人工复核报告里全是无效漏洞原因特征库里的正则写得太宽泛。比如拿error去匹配页面任何一个正常页面的错误提示、JS变量名、代码注释里都可能有这个词拿select去匹配十条命中里九条是正常英文。解决把单薄的关键字收敛成组合指纹。以SQL注入为例我一般会匹配数据库报错中才会出现的组合# 收敛后的SQL注入特征优先匹配数据库错误标识 SQLI_FINGERPRINTS [ rSQL syntax.*MySQL, rWarning.*pg_query, rORA-[0-9]{4,5}:, rUnclosed quotation mark, ]单个error会误伤ORA-00933这种Oracle报错编号几乎不可能出现在正常页面文本里。定特征库的原则是宁缺勿滥——漏报可以靠人补误报会浪费整份报告的可信度。6. 进阶玩法用本地DVWA靶场把扫描器准确率测明白6.1 搭一个自带漏洞的靶场先让扫描器在「已知答案」上跑我拿到这套源码后干的第一件事不是找站点测试而是在本地搭了个DVWADamn Vulnerable Web Application靶场。DVWA提供了SQL注入、XSS、CSRF、文件包含这些标准漏洞靶子每个漏洞都有已知的JSESSION_ID、参数名、触发位置。把SEED_URL指到http://127.0.0.1:8080/dvwa/跑一轮扫描然后把结果和靶场的漏洞清单逐一核对就知道自己的特征库哪些行、哪些漏。校准的核心指标只有两个漏报率和误报率。漏报等于漏洞存在但没扫出来这是特征库缺条目误报等于报告说有漏洞但靶场里其实是正常的这是特征写太宽。第一次跑的时候我把vulfile.txt里每一条正则的命中情况都打印出来对着DVWA页面逐条查筛掉了三条过宽的规则又补了五条针对DVWA特定报错文本的指纹误报率从七成降到两成以下。6.2 从靶场到授权测试的检查清单我给自己定了一套固定流程现在每次都走一遍本地靶场跑通核心漏洞命中率100%扫描一个静态站点验证爬虫完整性和去重逻辑扫描一个带登录的站验证Cookie注入没问题再回到靶场确认改配置后没引入新误报从那以后我每次拿这套扫描器测任何站点都强制先走完靶场校准确认配置没有破坏特征库的检出率才会把SEED_URL换成真实授权目标。这个习惯救过我很多次——最典型的翻车是某次改了thread_num4忘了加随机延迟靶场里没感觉换到外网站点直接整段IP被限流整个扫描任务白跑。所以现在config.py里凡是涉及频率的参数我都会写一行注释提醒自己改速度之前先确保退避逻辑还在。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网