☰
sqli-labs Less-57实战:单引号闭合与报错注入提取数据
2026/10/16 20:52:49 网站建设 项目流程

sqli-labs 玩到 Less-57,说明你已经不是刚接触 SQL 注入的萌新了。前面几十关把联合查询、布尔盲注、时间盲注、堆叠注入轮番练了一遍,到了 Challenges 系列,题目风格突然就变了——不再是一眼能看穿的查询拼接,而是像真实靶场一样逼着你一步步探测、确认、构造、拿结果。Less-57 就是这一阶段很有代表性的一关:字符型单引号闭合配合报错注入,难度不算高,但思路非常正。

这一关适合两类人:一是正在刷 sqli-labs 想完整通关的练习者,二是希望在真实场景里把“报错注入”用得熟练的读者。它能解决的问题很具体:怎么在页面几乎不出数据、只给一个报错回显的情况下,靠数据库自身报错机制把表名、字段名、甚至最终 flag 一点点抠出来。整关打下来不会超过十分钟,但对闭合方式的理解、对报错函数的语法记忆、对查询拼接的敏感度,都会被逼着再巩固一遍。

1. 关卡环境与核心思路拆解

1.1 挑战系列的特殊机制

Less-54 到 Less-65 这十几关,跟前面的常规关卡完全不是一个套路。从这一阶段开始,靶场模拟的是“你已经通过某种方式拿到了数据库访问权,但每次尝试注入都只能提交一次请求”的场景。关卡里有一个专门的 challenges 数据表,每一关会随机生成一个表名,表里存着一个 secret 字段,目标就是通过注入把这个 secret 的值读出来。

Less-57 在这个系列里属于中规中矩的一关,没有特别离谱的过滤,也没有需要脑洞大开的绕过,但它把前面所有关卡的通用方法论都串起来了:确定注入点、判断闭合方式、选择注入手法、提取数据。很多人在前面几十关习惯了“无脑加单引号看报错”,到了这一关如果还这么做,大概率会碰壁,因为挑战系列的报错信息不像前面几关那么直观,需要自己构造语句去引爆。

这关给我的最大感受是:sqli-labs 前面给你搭好的脚手架全撤了,你必须自己判断当前查询大概长什么样。Less-57 的查询语句是典型的字符串拼接,大致结构如下:

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

注意最核心的两个点:id 值被单引号包裹,查询最后跟了 LIMIT 子句。这两点直接决定了后续注入语句怎么写。如果你把前面 Less-1 到 Less-4 的单引号闭合思路吃透了,到这一关其实就剩一个动作——把闭合符补齐,再把你要的查询塞进去。

1.2 为什么这一关用报错注入最稳

我试过在 Less-57 上用联合查询注入,理论上也能出数据,但有个问题:挑战系列每关只允许你提交有限次数的请求,超过次数后表名会重新随机生成,之前的探测结果全部作废。这意味着联合查询那套“先确定列数、再逐个字段替换”的流程很容易翻车,因为列数探测多试几次就可能触发重置机制。

报错注入就不一样了。它不依赖于 SELECT 查询的列数对齐,也不需要把整个结果集原样回显出来,而是通过构造一个会让数据库报错的表达式,让报错信息里带上你要查询的数据。一次请求就能拿到一条数据,效率高得多,而且对页面的输出环境要求很低——哪怕页面本身什么都不显示,只要数据库的错误信息能回显出来,报错注入就能正常工作。

我在实际打这一关的时候,核心思路就一句话:先把 URL 参数上的闭合方式搞清楚,然后用 extractvalue 或 updatexml 这类报错函数,把目标数据塞进 XPath 表达式里,让数据库在解析 XPath 时出错,把数据带进报错消息返回给我。

2. 报错注入与闭合方式的关键原理

2.1 报错注入为什么能出数据

很多人第一次接触报错注入时,会觉得这东西很玄学——明明是一条查询语句,怎么报错还能把数据带出来?其实原理不复杂。

MySQL 内置了几个函数,比如 extractvalue 和 updatexml,它们接收的参数需要符合 XPath 格式。如果你传进去的字符串不是合法的 XPath 表达式,MySQL 会抛出一个错误,错误消息里会原样包含你传入的非法字符串。根据 MySQL 的报错机制,updatexml 函数最多能回显 32 个字符,extractvalue 也是类似限制。

利用方式是这样的:你让报错函数去处理一个拼接后的字符串,里面前半部分是固定的非法 XPath 内容,后半部分是你想查询的数据。比如updatexml(1,concat(0x7e,(select database()),0x7e),1),这里的 0x7e 是波浪号 ~ 的十六进制表示,用来作分隔符。数据库在执行时会尝试解析第二个参数作为 XPath,发现它不是合法格式,于是报错。报错内容大概长这样:

XPATH syntax error: '~数据库名~'

你的目标数据就被夹在两个波浪号中间回显出来了。

这招在 Less-57 上的用法跟在其他关一模一样,唯一的区别是前面拼接的闭合符不同。Less-57 的 id 参数是单引号包裹的字符串,所以你的注入语句必须先把前面的单引号闭合掉,再用注释符把查询语句最后面的内容处理掉。

2.2 闭合方式判断的通用方法

判断闭合方式这件事,很多人靠猜,但其实有一套非常成熟的探测流程。

第一步,先加一个单引号试试。如果页面报错或者显示异常,说明单引号很可能进入了 SQL 语句并且破坏了原有结构。在 Less-57 上,你提交?id=1'之后,拼接出来的 SQL 就会变成:

SELECT * FROM users WHERE id='1'' LIMIT 0,1

两个单引号连在一起,在 MySQL 里这是一个语法错误,页面会回显数据库的报错信息。

第二步,确定闭合符是单引号后,就要补全闭合。在 URL 里输入?id=1' and 1=1--+,如果页面正常显示,后面再试?id=1' and 1=2--+,如果页面内容发生了变化(通常是不出数据或显示不同结果),就说明你的闭合方式判断正确。

为什么用 and 1=1 和 and 1=2 来试?因为这两条语句一个恒真一个恒假,如果注入点存在且闭合正确,恒真条件下的查询会正常返回数据,恒假条件下返回结果会有明显差异。这种方法比单纯看报错更可靠,能确认你是否真正控制了查询逻辑。Less-57 的页面在恒真条件下会正常显示用户信息,恒假条件下不返回任何行,对比非常明显。

第三步,闭合符确认下来是单引号之后,后面所有的注入语句都要遵循这个固定格式。

2.3 关于列数的判断技巧

前面我说报错注入不依赖列数,但是如果你想在 Less-57 上用联合查询做补充验证,列数判断也值得掌握。用order by逐步增加数字,看它在哪个数字上开始报错。

在 Less-57 上提交?id=1' order by 3--+,如果正常返回,说明目标查询里的列数不少于 3 列;继续试?id=1' order by 4--+,如果这里报错,说明真实列数就是 3 列。我实测过这一关,目标表就是 3 列,跟前面很多关卡保持一致。

不过说实话,到了报错注入这一步,知道列数只是锦上添花,真正核心的还是把报错函数用好。

3. 实操过程与核心环节实现

3.1 信息收集与注入点确认

打开 Less-57 的页面,先观察 URL 结构和页面输出。这一关的 URL 参数就是id,页面正常显示时会列出对应的用户 ID、用户名和密码。第一件事就是在 URL 后面加个单引号,看数据库的反应。

我当时的操作记录是这样的:先提交?id=1',页面报错,报错内容里出现了 SQL 语法相关的错误信息,说明单引号确实干扰了原有查询。这一步同时确认了两件事:参数存在注入点,且闭合方式大概率是单引号。

接着用恒真恒假语句验证。提交?id=1' and 1=1--+,页面正常显示 ID 为 1 的用户信息;提交?id=1' and 1=2--+,页面无数据。这就完整确认了注入逻辑可控。

这里有一个细节值得单独说:URL 里的--+是注释符的 URL 编码写法。原本的注释符是--(双减号加一个空格),但空格在 URL 里会被浏览器和服务器处理掉,所以习惯上用+号代替空格。在很多环境中#号也可以当注释符用,但 URL 里的#容易被浏览器识别为锚点,后面的内容根本不会发送到服务器,所以--+是最稳妥的选择。

3.2 构造报错注入语句获取数据库信息

闭合确认之后,正式进入数据提取阶段。Less-57 的思路链条是:先拿数据库名,再拿当前库里的表名,然后定位到 challenges 那张随机表,最后读取 secret 字段的值。

第一步,获取当前数据库名。提交:

?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

这里把整个报错函数挂到了原有查询的条件判断位置。原有的 SQL 会被拼接成:

SELECT * FROM users WHERE id='1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)-- ' LIMIT 0,1

updatexml 函数的第二个参数是concat(0x7e,(select database()),0x7e),MySQL 执行子查询拿到数据库名,再拼上波浪号分隔符,然后尝试按 XPath 解析,必然失败并报错。报错信息会把数据库名带出来。

我实测时报错信息显示的是当前库名为 challenges 相关的内容。拿到库名后,后面的所有语句都基于这个库名来写。

第二步,获取当前库里的表名。挑战系列里每个关卡对应的表名是随机的,所以不能靠猜,得靠 information_schema 元数据表来查。使用 limit 逐条提取:

?id=1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),0x7e),1)--+

这条语句从 information_schema.tables 里取出这个库下的第一个表名,然后通过报错回显出来。挑战系列每关的库里面通常就两张表:一张是 challenges 相关的基础表,另一张就是本关的随机表。如果你 limit 0,1 拿到的表名看着不像目标表,就用 limit 1,1 继续试。

我当时是先看了 limit 0,1 的结果,发现是一个已知的固定表名,然后再用 limit 1,1 拿到了那一串随机字符组成的表名。这个随机表名才是承载 secret 的目标表。

第三步,查询目标表的字段名。这一关的 secret 字段名通常是固定的,但为了严谨,还是应该通过 information_schema.columns 确认一下:

?id=1' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='随机表名' limit 0,1),0x7e),1)--+

依次调整 limit 的偏移量,把目标表的所有字段名都列出来。Less-57 的目标表里通常就一个名为 secret 的字段,非常直接。

第四步,读取 secret 字段的值:

?id=1' and updatexml(1,concat(0x7e,(select secret from 随机表名),0x7e),1)--+

报错信息里出现的波浪号之间的内容,就是你要找的最终答案。到这里,Less-57 就算通关了。

3.3 关键参数的计算与选择

整个过程中有一个很值得琢磨的点:为什么用 0x7e 而不是直接用~字符?这其实是一个规避过滤的通用技巧。很多基础过滤规则会拦截特殊符号,但十六进制编码后的形式不容易被识别。0x7e 在 MySQL 里就是波浪号的十六进制表示,配合 concat 函数拼接出来的效果完全一样,但更容易通过各种简单的 WAF 规则。

报错函数的选择也要讲究。extractvalue 和 updatexml 在报错注入中用法几乎一样,区别在于参数位置。extractvalue 的原型是extractvalue(xml_document, xpath_string),所以你把注入内容放在第二个参数里;updatexml 的原型是updatexml(xml_document, xpath_string, new_value),也是把注入内容放在第二个参数里。两个函数报错回显的字符数上限都是 32 位左右,对于提取库名、表名、字段名和单个字段值来说完全够用。

如果拿到的数据长度超过 32 个字符,比如某条 secret 值很长,就需要用字符串截断函数配合多次注入来分段提取。比如用 substr 函数:

?id=1' and updatexml(1,concat(0x7e,substr((select secret from 随机表名),1,31),0x7e),1)--+

先提取前 31 个字符,然后再调整 substr 的起始位置,比如从 32 开始,提取后面的内容,最后拼接起来就是完整数据。这是报错注入处理长数据时最标准的操作。

3.4 使用 Burp Suite 或 HackBar 提交请求的体会

Less-57 这类关卡用浏览器地址栏直接改 URL 也能打,但我不太推荐,原因有两个。一是 URL 里有大量特殊字符,浏览器可能会自动解码或编码,导致提交的参数跟你想的不一样;二是挑战系列有请求次数限制,手速慢的人容易超次数触发重置,前面的工作白做。

我推荐用 Burp Suite 的 Repeater 模块或者浏览器插件 HackBar 来提交。Burp Suite 的好处是你可以完全控制请求内容,不会出现意料之外的编码转换,而且历史记录一目了然,方便回溯每一步的操作。HackBar 则胜在轻量,直接在浏览器里构造请求,对新手更友好。

用工具还有一个隐藏好处:方便批量替换参数。比如我从获取库名到获取表名,中间只改了一个子查询的内容,工具里直接复制上一条请求,改动一小段就能提交,效率比在地址栏里重新敲快得多。

4. 常见问题与排查技巧实录

4.1 报错信息不显示或页面空白

这是报错注入最头疼的问题。如果提交报错注入语句后页面什么都没有,先别急着怀疑是注入方式不对,大概率是你把注释符写错了。Less-57 的 URL 里必须用--+结尾,如果你在浏览器里手动输入,有时候 URL 编码会把+号转成空格,导致注释符失效,SQL 语句后面多出一段LIMIT 0,1,整个语句还是会报错,但报错内容可能不是你想要的。

排查方法是先确认注释符是否生效:提交一个最基础的语句,比如?id=1' and 1=1--+,看页面是否正常返回数据。如果正常,说明注释没问题;如果报错或空白,把提交方式换成 Burp Suite 再试一次。

还有一种情况是报错信息被页面隐藏了。sqli-labs 的挑战系列大部分关卡会直接回显报错,但个别环境下数据库错误显示被关闭,这时候报错注入就会失效,得切换到布尔盲注或时间盲注的思路。Less-57 默认是能显示报错的,如果你发现不显示,先检查 PHP 环境配置或浏览器控制台有没有吞掉错误信息。

4.2 单引号被过滤或转义

在真实环境中,单引号经常会被转义,但在 sqli-labs 的 Less-57 里没有这个过滤条件,所以这个问题更多是在提示你提前思考。如果某天你遇到单引号被转义的情况,可以考虑宽字节注入或者把查询条件转换成不带引号的写法,比如用十六进制表示字符串:

SELECT * FROM users WHERE id='1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database()),0x7e),1)-- '

注意这里 table_schema=database() 没有用引号包裹,database() 函数返回的就是当前库名,省去了写字符串的麻烦。这类技巧在后期关卡里会频繁出现,Less-57 里提前理解了不亏。

4.3 updatexml 与 extractvalue 报错不出数据

有时候函数本身没写错,但报错信息里只有 XPATH syntax error,没有看到你要的数据,这是因为 XPath 解析失败的位置太靠前了。比如你传的第二个参数以数字开头,MySQL 在解析时可能还没碰到你拼接进去的数据就报错了。

解决办法是把数据部分尽量放在字符串开头,同时保留一个非法的前缀字符来触发报错。像concat(0x7e,(select ...),0x7e)这种结构里,波浪号本身就是非法 XPath 字符,必须走到那里才会报错,所以数据能被带出来。如果你用了concat((select ...),0x7e)这种把数据放前面的写法,要确认 select 的结果本身不是合法 XPath 格式,否则可能不出错。

实操中我倾向于固定写concat(0x7e,xx,0x7e)这种对称结构,简单可靠,不用每次纠结。

4.4 请求次数过多导致表名重置

挑战系列的机制是:超过一定次数的请求后,目标表名会被重置。这意味着你之前探测到的随机表名已经失效,再拿它去查询会拿到错误结果或者没有结果。

规避方法就一个:少用探测性请求。很多人习惯每走一步就在浏览器里刷新几次、多试几个参数,这在普通关卡没问题,在挑战系列里就是送人头。我打 Less-57 的流程尽量压缩到十次请求以内,每一步都确保语句正确后再提交。如果确实遇到表名重置了,也别慌,重新走一遍 information_schema 查表名就行,库名和固定表名不会变,只有随机表名变了。

5. 从 Less-57 延伸到真实场景的思路

5.1 报错注入在实战中的价值

很多人觉得报错注入是靶场玩具,真实环境里很难遇到。这个观点我部分认同,但也不全对。真实环境下,开发者确实更倾向于使用参数化查询来防注入,但总有一些历史遗留系统、快速迭代的项目、或者对 SQL 拼接不够敏感的代码,会给你留下报错注入的机会。

报错注入在实战中的真正优势不是它有多隐蔽,而是它不依赖页面正常回显。很多注入场景里,页面上只有一个登录成功或失败的提示,没有详细的数据输出,联合查询根本无从下手。这时候报错注入只要能触发数据库报错,并且错误信息能回显到页面上,就能把数据一点一点抠出来。

不过要注意的是,MySQL 的报错信息在 5.1.5 之后的版本里新增了针对 XML 函数的报错回显能力,更早的版本或者某些数据库品牌(比如 Oracle、PostgreSQL)并不支持这套写法。在真实环境里,你得先确认数据库类型和版本,再决定用哪种注入手法。

5.2 报错注入与布尔盲注的配合使用

Less-57 这种有报错回显的关卡是最理想的情况。万一某天你遇到的页面把数据库错误完全屏蔽了,报错注入变成盲打,但你对闭合方式的判断已经掌握,就可以切换到布尔盲注或者时间盲注的思路。

布尔盲注是基于页面内容变化来逐字符猜解数据。比如提交?id=1' and ascii(substr((select secret from 表名),1,1))>64--+,如果页面正常显示说明第一个字符的 ASCII 码大于 64,继续调整数值逼近真实值。这种方式速度慢,但稳定性极高,只要页面有正常和异常两种状态就能用。

时间盲注则是利用 sleep 函数制造可观测的延迟。当页面完全没有内容差异时,通过响应时间判断条件真假。这种方法在自动化工具中更常用,手动打的话效率太低,但理解原理是必要的。

5.3 对防御侧的启发

攻防是一体的。Less-57 这种关卡玩多了,你会自然而然地对 SQL 注入的防御产生更深的理解。单引号闭合法在真实代码里太常见了,很多老项目的 SQL 就是这么写的。

最有效的防御方式永远是参数化查询,也就是预处理语句。用占位符替代直接拼接,让数据库分清楚代码和数据,从根上杜绝注入。但在没法大改代码的场景里,至少应该做两层防护:一是对输入做白名单校验,比如 id 参数只允许数字;二是关闭数据库错误回显,不要让报错信息直接暴露到前端页面。这两招组合起来,Less-57 这种报错注入就没法打了。

我个人在实际测试中的体会是:sqli-labs 的 Less-57 比前面几十关更接近实战节奏,因为它逼着你把整个注入流程压缩到最短、最准确。前面你可以慢悠悠地试错,但挑战系列每关都有次数压力,你会被迫养成“先想清楚再提交”的习惯。这个习惯一旦养成,后面遇到真实环境里的注入测试就会从容很多。

如果你正在刷这一关卡住过不去,别急着看答案,按照“闭合确认、库名、表名、字段名、数据”这个顺序一步步走,每一步只提交一次请求,基本不会失败。Less-57 只是一道开胃菜,后面的关卡会在这个基础上叠加更多过滤和绕过技巧,把这一关的核心思路吃透,后面的路会顺畅不少。

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

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

立即咨询