
简介这是一套面向网络安全初学者与渗透测试人员的Web漏洞扫描系统源码基于Python与Django开发集成爬虫引擎与自定义规则库可对静态页面和动态交互式Web应用进行深度安全扫描、漏洞发现与风险评估。资源包共202个文件约1.16MB以118个Python源码文件为核心辅以20个HTML模板、9个JavaScript脚本、7个CSS样式及3个SQL文件构建前后端另含字典文件、说明文档与数据库文件结构完整、模块清晰。已有91人学习下载。读者可获得一套可直接部署运行的完整扫描工具通过爬虫遍历页面链接模拟攻击行为并借助可扩展的规则库定制扫描策略系统提供任务管理、结果查看与评估报告生成界面附带使用说明文档便于理解扫描流程、规则配置与多层防护机制适合用于课程设计、安全实验或二次开发参考。1. 从一次被 SQL 注入打穿的登录接口说起去年帮一个朋友看他的 Django 后台登录接口被人用 or 11 --直接绕过去了日志里连个像样的告警都没有。他问我有没有办法在代码提交前就自动把这类问题扫出来这就是「基于 Python 与 Django 构建的自动化 Web 应用漏洞扫描与安全检测系统」要解决的事——它不是又一个商业扫描器的替代品而是一套你能读源码、能改规则、能塞进 CI 的自建评估工具。核心由三块拼起来一个爬虫引擎负责把站点的 URL、表单、参数摸清楚一个检测调度层把请求打出去并比对响应一个自定义规则库让你把业务特有的坑写成可复用的检查项。适合谁手里有 Django 或其他 Web 项目、想在内网做持续安全自检、又不放心把流量交给外部平台的团队。下面我按自己搭过的一版讲能跑通、能改、能落地。2. 爬虫引擎怎么把站点结构摸干净2.1 为什么不用现成的爬虫框架直接上很多人第一反应是scrapy或requests-html一把梭。我试过翻车点在于漏洞扫描要的不是「抓内容」而是「抓可注入的入口」。一个页面里真正有价值的不是正文是form的 action、method、每个 input 的 name是 URL query 里的参数名是Set-Cookie和自定义 header。通用爬虫把这些当噪音过滤掉了你还得反过来再解析一遍。所以我的做法是自己写一个轻量爬虫基于requestsBeautifulSoup只提取「入口点」结构输出统一的数据模型。这样后面规则库拿到的就是干净的{url, method, params, cookies}不用再猜。另一个原因是可控性。扫描器最怕爬虫失控把人家站点爬崩。自己写就能精确控制并发数、请求间隔、最大深度出问题也好定位。2.2 入口点提取的核心代码import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse, parse_qs class EntryPoint: def __init__(self, url, methodGET, paramsNone, cookiesNone): self.url url self.method method.upper() self.params params or {} # 参数名 - 默认值 self.cookies cookies or {} def extract_entry_points(base_url, html, current_url): 从单个页面 HTML 中抽取表单和链接入口 soup BeautifulSoup(html, html.parser) points [] # 1. 表单method、action、所有 input 的 name for form in soup.find_all(form): action form.get(action) or current_url method form.get(method, get) params {} for inp in form.find_all([input, textarea, select]): name inp.get(name) if name: params[name] inp.get(value, ) points.append(EntryPoint(urljoin(base_url, action), method, params)) # 2. 带 query 的链接把参数名抽出来 for a in soup.find_all(a, hrefTrue): full urljoin(base_url, a[href]) parsed urlparse(full) if parsed.query: qs parse_qs(parsed.query) params {k: v[0] for k, v in qs.items()} points.append(EntryPoint(full.split(?)[0], GET, params)) return points逻辑说明extract_entry_points只做一件事——把页面里所有「能带参数请求」的地方结构化。表单部分保留 method 和全部 input name链接部分只保留带 query 的因为纯静态链接没有注入面。参数说明base_url用于拼接相对路径current_url是当前页地址防止 action 为空时丢目标。params里存默认值后面规则库做 fuzz 时会替换成 payload。2.3 去重、限速与深度控制爬虫最容易出的问题是重复请求同一个 URL 几百次。我用一个set存(url, method, frozenset(params.keys()))作为指纹去重参数名集合相同就认为是同一个入口不重复扫。限速用time.sleep加随机抖动别用固定间隔固定间隔反而容易被 WAF 识别成扫描器。深度控制上我只爬同域、最多三层外链一律不跟——扫描器跑去爬别人站点是事故。import time, random from collections import deque def crawl(start_url, max_depth3, delay(0.5, 1.5)): seen set() queue deque([(start_url, 0)]) all_points [] while queue: url, depth queue.popleft() if depth max_depth or url in seen: continue seen.add(url) try: resp requests.get(url, timeout8) except requests.RequestException: continue points extract_entry_points(start_url, resp.text, url) for p in points: fp (p.url, p.method, frozenset(p.params.keys())) if fp not in seen: seen.add(fp) all_points.append(p) # 只跟同域链接继续爬 for a in BeautifulSoup(resp.text, html.parser).find_all(a, hrefTrue): nxt urljoin(start_url, a[href]) if urlparse(nxt).netloc urlparse(start_url).netloc: queue.append((nxt, depth 1)) time.sleep(random.uniform(*delay)) return all_points参数说明max_depth3是经验值再深收益递减还容易触发风控delay给一个区间让请求间隔随机化。seen同时承担 URL 去重和入口指纹去重注意这里我把两者混在一个 set 里实际项目里建议分开否则 URL 和指纹可能碰撞。3. 检测调度层把 payload 打出去并判断结果3.1 检测器的抽象设计爬虫给你一堆入口点接下来要决定「用什么 payload、怎么判断命中」。我的做法是每个漏洞类型写一个 Detector 类统一接口check(entry_point) - Finding or None。这样加新规则就是加一个类不用动调度逻辑。常见类型先覆盖四类SQL 注入、XSS、命令注入、敏感信息泄露。别一上来追求全覆盖先把这四类做扎实误报率压下去比数量重要。class BaseDetector: name base payloads [] def check(self, ep): raise NotImplementedError class SQLiDetector(BaseDetector): name sqli payloads [, \, or 11, 1 AND SLEEP(3)--] def check(self, ep): for payload in self.payloads: data {k: payload for k in ep.params} try: if ep.method POST: resp requests.post(ep.url, datadata, timeout10) else: resp requests.get(ep.url, paramsdata, timeout10) except requests.RequestException: continue # 基于报错特征 时间盲注双重判断 if any(sig in resp.text.lower() for sig in [sql syntax, mysql_fetch, ora-, sqlite]): return {type: self.name, url: ep.url, payload: payload, evidence: error-based} return None逻辑说明check遍历 payload把入口点的每个参数都替换成 payload 发一次。判断命中用报错特征匹配这是最稳的初筛。参数说明timeout10给足时间因为后面要加时间盲注data和params分别对应 POST 和 GET。注意这里每个参数都单独替换成 payload而不是全部一起替换否则无法定位是哪个参数有问题。3.2 时间盲注的判断要留后悔药报错注入好判断时间盲注才是玄学。SLEEP(3)打出去响应慢了 3 秒但你没法确定是数据库睡了还是网络抖了。我的做法是打两次一次带SLEEP(3)一次带SLEEP(0)比较两次响应时间差差值接近 3 秒才判定命中。这样能过滤掉大部分网络波动导致的误报。import time def time_based_check(ep, payload_sleep, payload_normal): t0 time.time() requests.get(ep.url, params{k: payload_sleep for k in ep.params}, timeout15) t_sleep time.time() - t0 t0 time.time() requests.get(ep.url, params{k: payload_normal for k in ep.params}, timeout15) t_normal time.time() - t0 return (t_sleep - t_normal) 2.5参数说明阈值2.5是留了余量SLEEP(3)实际差值通常在 2.8 到 3.2 之间网络好的时候 2.5 足够。如果目标站点本身响应就慢这个阈值要往上调否则全是误报。3.3 调度与并发调度层用concurrent.futures.ThreadPoolExecutor控制并发别用进程池IO 密集场景线程更合适。并发数我一般设 5 到 10再高容易把目标打挂或者触发封禁。每个入口点跑完所有 detector结果汇总成 Finding 列表。from concurrent.futures import ThreadPoolExecutor, as_completed def run_scan(entry_points, detectors, workers8): findings [] def scan_one(ep): local [] for det in detectors: result det.check(ep) if result: local.append(result) return local with ThreadPoolExecutor(max_workersworkers) as pool: futures {pool.submit(scan_one, ep): ep for ep in entry_points} for fut in as_completed(futures): findings.extend(fut.result()) return findings参数说明workers8是内网扫描的稳妥值公网目标建议降到 3 到 5。as_completed保证结果按完成顺序收集不用等最慢的那个。4. 自定义规则库把业务特有的坑写成检查项4.1 规则用 YAML 描述别硬编码检测逻辑写死在 Python 里改一条规则要动代码、要重新部署这在实战里很痛苦。我的做法是把「匹配什么、怎么匹配、命中后报什么」抽成 YAML 规则文件Python 只负责加载和执行。这样安全同学不用懂代码也能加规则。- id: sensitive-file-exposure name: 敏感文件泄露 severity: high match: type: path patterns: - /.git/config - /.env - /backup.sql judge: status_code: 200 body_contains: [DB_PASSWORD, SECRET_KEY]逻辑说明match.typepath表示这条规则针对路径做探测patterns是要尝试的路径列表。judge定义命中条件——状态码 200 且响应体包含敏感关键字。参数说明severity用于结果分级id全局唯一方便后续做白名单和去重。4.2 规则加载与执行引擎import yaml class RuleEngine: def __init__(self, rule_path): with open(rule_path, encodingutf-8) as f: self.rules yaml.safe_load(f) def run(self, base_url): findings [] for rule in self.rules: if rule[match][type] ! path: continue for pattern in rule[match][patterns]: url base_url.rstrip(/) pattern try: resp requests.get(url, timeout8) except requests.RequestException: continue judge rule[judge] if resp.status_code ! judge.get(status_code, 200): continue if any(kw in resp.text for kw in judge.get(body_contains, [])): findings.append({ rule_id: rule[id], severity: rule[severity], url: url, }) return findings参数说明rule_path指向 YAML 文件base_url是扫描目标根地址。judge.status_code默认 200因为敏感文件通常返回 200 而不是 404。body_contains是关键字列表命中任意一个就算。注意这里没做并发规则数量少的时候够用规则上百条要改成线程池。4.3 规则库怎么和爬虫结果联动规则库有两种用法一种是独立探测比如上面扫敏感文件另一种是挂在爬虫发现的入口点上做参数级检查。后者需要规则里声明match.typeparam然后由调度层把规则转成 payload 注入到入口点参数里。这样一套规则库既能扫固定路径又能扫动态参数覆盖面就上来了。5. 避坑与排查那些让我熬夜的翻车现场5.1 扫描器把自己扫进了黑名单现象扫到一半目标站点全部返回 403换 IP 也不行。原因并发太高加固定间隔被 WAF 识别成攻击流量。解决并发降到 3请求间隔改成随机 1 到 3 秒并且给请求加上正常的User-Agent和Referer别用默认的python-requests。5.2 时间盲注误报一片现象报告里几十条 SQL 注入人工一看全是误报。原因目标站点本身响应波动大SLEEP(3)的差值判断被网络抖动干扰。解决改成双请求对比带 sleep 和不带 sleep 各一次阈值提到 2.5 秒以上并且对同一入口点重复测两次两次都命中才报。5.3 爬虫陷入无限循环现象扫描任务跑了半小时没结束日志里同一个 URL 反复出现。原因页面里有?page1这种自引用链接深度控制没生效。解决入口指纹去重加上 URL 规范化去掉 fragment、排序 query 参数并且对同一路径的 query 参数组合数量设上限超过就跳过。5.4 规则库 YAML 格式错误导致整个扫描挂掉现象加了一条新规则后扫描直接报错退出。原因YAML 缩进用了 Tab解析失败。解决加载规则时用try/except包住单条规则解析失败只跳过那条并打日志不要让整个引擎崩掉。另外统一用空格缩进编辑器里把 Tab 转空格打开。5.5 POST 表单漏扫现象爬虫抓到了表单但扫描结果里没有 POST 类型的漏洞。原因extract_entry_points里表单 method 默认取了get但有些表单没写 method 属性实际是 POST。解决method 为空时默认按 GET 处理没问题但要额外检查表单是否有enctype或隐藏字段有的话补一次 POST 尝试。6. 把扫描器塞进 CI一个 Django 项目的落地技巧前面讲的都是扫描器本身但真正让它产生价值的是「自动跑起来」。我一般把它做成 Django 的 management command这样能直接复用项目的配置和数据库不用额外维护一套环境。# yourapp/management/commands/security_scan.py from django.core.management.base import BaseCommand from scanner.crawler import crawl from scanner.detectors import SQLiDetector, XSSDetector from scanner.rules import RuleEngine class Command(BaseCommand): help 对指定目标执行安全扫描 def add_arguments(self, parser): parser.add_argument(target, typestr, help扫描目标根 URL) parser.add_argument(--rules, typestr, defaultrules/builtin.yaml) def handle(self, *args, **options): target options[target] points crawl(target, max_depth3) self.stdout.write(f发现入口点 {len(points)} 个) detectors [SQLiDetector(), XSSDetector()] findings run_scan(points, detectors, workers5) engine RuleEngine(options[rules]) findings.extend(engine.run(target)) for f in findings: self.stdout.write(self.style.WARNING( f[{f.get(severity, unknown)}] {f.get(type, f.get(rule_id))} {f[url]} )) self.stdout.write(self.style.SUCCESS(f扫描完成共 {len(findings)} 条发现))逻辑说明add_arguments让命令接受目标 URL 和规则文件路径handle里串起爬虫、检测器、规则引擎三步。参数说明--rules默认指向内置规则文件CI 里可以传自定义规则路径。输出用 Django 的self.style上色方便在 CI 日志里一眼看到高危项。接进 CI 就是在流水线里加一步python manage.py security_scan http://staging.example.com --rules rules/ci.yaml发现高危就exit 1阻断发布。这里有个血泪经验别把扫描器指向生产环境CI 里只扫 staging生产环境用只读的被动检查。另外规则文件要跟着代码走版本控制每次加规则都走 PR 评审避免有人随手加一条误报规则把流水线搞挂。验证扫描器本身有没有用我的习惯是拿一个故意留了漏洞的靶场项目跑一遍看能不能把已知的 SQL 注入、XSS、敏感文件全部报出来同时误报控制在可接受范围。每次改完检测逻辑都重跑一遍靶场这比看代码靠谱。这套东西我前后迭代了大半年最大的体会是扫描器的价值不在覆盖多少漏洞类型而在误报率够低、规则够好改、能持续跑。希望帮到你。本文还有配套的精品资源点击获取