
简介面向网络安全课程设计与期末大作业的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字段占绝大多数。按注入形态分常见场景如下数字型注入id1 and 12直接改数字参与运算。字符型注入nametest需要闭合单引号再追加条件。搜索型注入keyword% or 11闭合加通配符常见于后台搜索框。报错注入updatexml(1,concat(0x7e,version()),1)靠数据库报错信息回显内容。时间盲注if(11,sleep(3),0)用响应时间差判断条件真假。还有一个容易忽略的事实自动化工具只能帮你发现“疑似漏洞”不能替你判断业务影响。比如登录接口存在时间盲注但接口本身有频率限制重放几次就锁账号这种场景要人工权衡。所以工具的输出应该按“疑似注入点”“确认注入点”“可数据提取”三个等级划分而不是笼统回一句“存在漏洞”。初筛阶段我一般只用最基础的一组Payload它们能覆盖数字型和字符型两种最常见情况也不会过早触发复杂解析逻辑用途Payload说明字符闭合探测、、%27观察是否触发报错或行为变化布尔真条件and 11、or 11 --与原始响应对比布尔假条件and 12与真条件形成对照时间延迟and sleep(3)、 and sleep(3) --用于无回显场景or 11 --这种写法就是网上常说的“万能密码”绕过形态的一种。注意--在数据库里是注释符有些解析器要求后面有空白URL里要么写成--要么写成--%20。这个细节看着小实际跑起来影响面很大。2.2 三种通用判定方法响应差异、布尔盲注与时间盲注把Payload发出去只是第一步关键是怎么判断“中没中”。自动化工具里最常用的判定方法有三条本质上都是对照实验。响应差异判定是拿原始请求结果作为基线再分别请求带、带and 11、带and 12的变体。如果基线响应与变体差异明显状态码变化、响应体出现SQL错误、页面结构变化而and 11接近正常、and 12明显异常基本可以判定存在注入点。这里有个必须讲清楚的技术细节判断“异常”不能只看HTTP状态码。很多框架会把SQL异常捕获后仍然返回200但响应体里留着SQL syntax error或数据库驱动的堆栈信息。所以响应差异要同时看三个信号状态码、响应体长度、响应体关键词error、syntax、SQLSTATE等。三个信号缺一个漏报率都会上升。布尔盲注判定用于页面没有报错回显的场景。思路是用and 11和and 12构造“真”“假”两个页面只要两个页面的响应存在稳定差异就能逐字符比较提取数据库内容。这个方法的准确性依赖一个前提目标页面在两类条件下的响应必须稳定。如果页面本身带随机广告、时间戳、动态验证码布尔盲注会频繁误判——这一点后面排错章节还会专门讲。时间盲注判定用于连页面差异都没有的场景。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_valuesTrue) # 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(querynew_query).geturl()逻辑说明原URL里参数顺序可能与parse_qs解析后的顺序不同urlencode会按字典顺序重新排列。对注入检测来说参数顺序变化影响很小但能保证每次生成的请求是确定性的。keep_blank_valuesTrue保留了值为空的参数有些注入点恰恰藏在空值参数里不能顺手丢掉。参数说明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/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def fetch(url: str, session: requests.Session) - tuple: 发送GET请求返回(状态码, 响应文本, 耗时) try: resp session.get(url, headersHEADERS, timeoutTIMEOUT, allow_redirectsFalse) 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 11}) false_url rebuild_url(base_url, {param: f{value} and 12}) _, 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_redirectsFalse是关键如果目标把带and 12的请求重定向到错误页或首页重定向会把两个原本不同的响应拉平差异对比直接失效。参数说明TIMEOUT8是给慢接口留的余量小于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 ford(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返回的是中位耗时不是平均耗时。网络抖动往往是偶发的中位数能把单次抖动的干扰滤掉这是我做时间盲注吃过亏之后才改过来的写法。参数说明samples3是速度与稳定的折中调到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 11WAF返回的拦截页与正常页面差异巨大工具把这个差异当成了注入特征。原因初筛payload的特征太明显。and 11、 or 11 --这些字符串是WAF规则库里优先级最高的命中规则线上目标十有八九会拦。解决先探测目标是否存在WAF再决定payload形态。可以在请求头里加X-Forwarded-For、伪装User-Agent但更可靠的是做编码变形比如把and写成%41nd的大小写混合、用/**/注释符插入到关键字中间。需要注意的是绕WAF有严格的授权边界只对你有权限的目标做。4.2 时间盲注的耗时抖动让结论不可信现象测试接口A时条件为假的耗时是2.8秒条件为真时反而只有1.5秒结论完全反了。换个时间段再跑结果又恢复正常。原因目标服务器在同一时刻负载变化大或者网络链路对长连接不稳定。单次采样被一次慢包带偏。解决把单次采样改成多次采样取中位数同时增加基准请求的采集次数。基准恒假条件与测试条件必须在同一窗口内交替请求而不是先跑完所有基准再跑测试否则时间差里混进了目标负载的变化。新手代码里最常见的省时间做法是先算好基准再测条件结果基准和测试隔了五分钟早就不是同一个参照系了。4.3 重定向把差异对比带偏了现象某个参数拼上and 12后响应从200变成302工具判定“状态码异常注入成功”但手工测试发现只是一个正常的登录失效跳转。原因代码里没关重定向或者关了重定向但没有处理302。requests默认跟随重定向跟随之后拿到的可能是登录页长度与正常响应差异很小不跟随的时候302本身也可能被误判为异常。解决把allow_redirectsFalse设为默认值把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是一个PHPMySQL的漏洞靶场它的SQL Injection模块按难度分级Low级别直接拼接参数Medium级别用了mysql_real_escape_string做转义High级别把参数限制成数字类型。用自动化工具去跑Low级别应能稳定判定注入Medium级别如果工具没做单引号逃逸就会漏报High级别如果工具只会拼字符串同样会漏报。这三个级别正好用来验证工具的“字符闭合”能力。SQLiLab则是专门为SQL注入设计的平台注入点分布更细——有基于GET的、基于POST的、基于Cookie的还有带着各种括号、注释符的拼接场景。相比DVWASQLiLab更能考验payload的通用性。常见搜索到的“sqlilab平台-sql注入漏洞”练习基本都按这个平台设计。配合CTFHub技能树的SQL注入关卡一起练能覆盖从简单报错注入到复杂盲注的完整路径。5.2 工具跑DVWA的SQL Injection模块预期结果与真实结果我把DVWA的security级别切到Low用上面的三段代码跑ID参数。预期结果如下表所示测试参数原始响应长度and 11长度and 12长度判定id1498249824724疑似注入id2498249824724疑似注入具体长度会随DVWA版本和数据变化但模式是固定的id1和id2的and 12响应长度都短了一截说明12把查询结果集清空了页面少渲染了一行用户记录而and 11没有改变长度证明注入条件参与到了SQL的WHERE子句里。这个模式就是“响应差异判定”最理想的正样本。然后我把security切到Medium再跑一次and 11会失败。原因是Medium的转义函数把传入的单引号加了反斜杠payload变成字符串的一部分。这时工具需要切换到“不依赖单引号”的payload——数字型注入直接用1 and 11字符型注入用闭合失败后记录为“字符型疑似失败”。很多工具在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( filenamescan.log, levellogging.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_workers6) 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_workers6是经验值目标接口限流严格就降到2到3内网靶场可以提到10。as_completed保证先返回的先处理避免一个慢请求卡住整个日志输出。参数说明这里每个线程用独立的Session而不是共用一个。共用Session在线程池里会有连接池竞争偶发连接重置独立Session虽然多占一点连接但行为干净排查问题省心得多。这类检测工具真正决定能不能长期用的不是初始化版本跑得多快而是后来者能不能看懂判定逻辑并在上面加新payload。我早期写过一个“能跑但没人敢维护”的版本结果三个月后连我自己都要靠文档恢复记忆。从那以后我坚持在代码里写清每个阈值的来源在文档里记录每条payload适配的数据库类型。希望帮到你也祝你的扫描器别变成第二个黑匣子。本文还有配套的精品资源点击获取