简介:这是一套面向网络安全初学者与渗透测试人员的Web漏洞扫描系统源码,基于Python与Django开发,集成爬虫引擎与自定义规则库,可对静态页面和动态交互式Web应用进行深度安全扫描、漏洞发现与风险评估。资源包共202个文件,约1.16MB,以118个Python源码文件为核心,辅以20个HTML模板、9个JavaScript脚本、7个CSS样式及3个SQL文件构建前后端,另含字典文件、说明文档与数据库文件,结构完整、模块清晰。已有91人学习下载。读者可获得一套可直接部署运行的完整扫描工具,通过爬虫遍历页面链接模拟攻击行为,并借助可扩展的规则库定制扫描策略;系统提供任务管理、结果查看与评估报告生成界面,附带使用说明文档,便于理解扫描流程、规则配置与多层防护机制,适合用于课程设计、安全实验或二次开发参考。
1. 从一次被 SQL 注入打穿的登录接口说起
去年帮一个朋友看他的 Django 后台,登录接口被人用' or 1=1 --直接绕过去了,日志里连个像样的告警都没有。他问我:有没有办法在代码提交前就自动把这类问题扫出来?这就是「基于 Python 与 Django 构建的自动化 Web 应用漏洞扫描与安全检测系统」要解决的事——它不是又一个商业扫描器的替代品,而是一套你能读源码、能改规则、能塞进 CI 的自建评估工具。核心由三块拼起来:一个爬虫引擎负责把站点的 URL、表单、参数摸清楚,一个检测调度层把请求打出去并比对响应,一个自定义规则库让你把业务特有的坑写成可复用的检查项。适合谁?手里有 Django 或其他 Web 项目、想在内网做持续安全自检、又不放心把流量交给外部平台的团队。下面我按自己搭过的一版讲,能跑通、能改、能落地。
2. 爬虫引擎怎么把站点结构摸干净
2.1 为什么不用现成的爬虫框架直接上
很多人第一反应是scrapy或requests-html一把梭。我试过,翻车点在于:漏洞扫描要的不是「抓内容」,而是「抓可注入的入口」。一个页面里真正有价值的不是正文,是<form>的 action、method、每个 input 的 name,是 URL query 里的参数名,是Set-Cookie和自定义 header。通用爬虫把这些当噪音过滤掉了,你还得反过来再解析一遍。所以我的做法是自己写一个轻量爬虫,基于requests+BeautifulSoup,只提取「入口点」结构,输出统一的数据模型。这样后面规则库拿到的就是干净的{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, method="GET", params=None, cookies=None): 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", href=True): 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_depth=3, 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, timeout=8) 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", href=True): 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_depth=3是经验值,再深收益递减还容易触发风控;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 '1'='1", "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, data=data, timeout=10) else: resp = requests.get(ep.url, params=data, timeout=10) 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 发一次。判断命中用报错特征匹配,这是最稳的初筛。参数说明:timeout=10给足时间,因为后面要加时间盲注;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}, timeout=15) t_sleep = time.time() - t0 t0 = time.time() requests.get(ep.url, params={k: payload_normal for k in ep.params}, timeout=15) 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, workers=8): findings = [] def scan_one(ep): local = [] for det in detectors: result = det.check(ep) if result: local.append(result) return local with ThreadPoolExecutor(max_workers=workers) 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参数说明:workers=8是内网扫描的稳妥值,公网目标建议降到 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.type=path表示这条规则针对路径做探测,patterns是要尝试的路径列表。judge定义命中条件——状态码 200 且响应体包含敏感关键字。参数说明:severity用于结果分级,id全局唯一,方便后续做白名单和去重。
4.2 规则加载与执行引擎
import yaml class RuleEngine: def __init__(self, rule_path): with open(rule_path, encoding="utf-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, timeout=8) 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.type=param,然后由调度层把规则转成 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 反复出现。原因:页面里有?page=1这种自引用链接,深度控制没生效。解决:入口指纹去重加上 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", type=str, help="扫描目标根 URL") parser.add_argument("--rules", type=str, default="rules/builtin.yaml") def handle(self, *args, **options): target = options["target"] points = crawl(target, max_depth=3) self.stdout.write(f"发现入口点 {len(points)} 个") detectors = [SQLiDetector(), XSSDetector()] findings = run_scan(points, detectors, workers=5) 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、敏感文件全部报出来,同时误报控制在可接受范围。每次改完检测逻辑都重跑一遍靶场,这比看代码靠谱。这套东西我前后迭代了大半年,最大的体会是:扫描器的价值不在覆盖多少漏洞类型,而在误报率够低、规则够好改、能持续跑。希望帮到你。
本文还有配套的精品资源,点击获取