目录
之前一直没怎么理解 floor 报错注入的原理,只是用固定语句打,这次详细理解了下原理,整理了笔记,希望对大家理解 Floor 报错注入有帮助。
1、基础函数铺垫
生成随机数:
sql
select rand();
//生成0-1之间的小数

向下取整:
floor 函数并不是四舍五入,而是直接向下取整
sql
select floor(1.78);
select floor(2.78);
select floor(1.28);

随机取 0 或者 1
sql
select floor(rand()*2);

给语句取别名:
sql
select floor(rand()*2) a;

拼接:
sql
select concat(1,2,3);

2、导致报错核心
以 id 进行排序
sql
select * from users group by id;

当 MySQL 看到 GROUP BY id 时,它的核心任务是:去重、分类、合并。
为了完成这个任务,MySQL 需要用到一个数据结构------临时哈希表
MySQL 会在内存中开辟一块空间,建立一张临时表。这张临时表的信息非常特殊:
它的主键(Unique Key) ,就是 GROUP BY 后面的那个字段(在这里是 id)。因为是主键,所以里面的值绝对不能重复。
MySQL 开始一行行扫描 users 表:
-
读取第 1 行,发现
id = 1。 -
去临时表里查找:"主键里有没有
1?" ➔ 没有。 -
在临时表中新建一行 ,把
id = 1作为主键写入。 -
读取第 2 行,发现
id = 1。 -
去临时表里查找:"主键里有没有
1?" ➔ 有。 -
直接跳过或做覆盖处理(因为已经有这个组了,不能重复插入)。
接着读取第 3 行 :发现 id = 2。
去临时表里查找 :"主键里有没有 2?" ➔ 没有 (因为现在里面只有 id=1 )。
在临时表中新建一行 :把 id = 2 作为新主键写入。
测试语句:
sql
select concat(0x3a,(database()),0x3a,floor(rand()*2)) x;
//0x3a 是十六进制,表示的是英文冒号:(方便我们定位报错位置)

这一句只是单纯的计算,触发了一次 rand() 计算,结果是 :security:1,就结束了。这里没有对比,没有表格,没有冲突,所以它永远都是正常的。
sql
select concat(0x3a,(database()),0x3a,floor(rand()*2)) x from information_schema.tables group by x;

MySQL 的优化器在解析这条语句时,它发现用户的意图非常简单:"用户只是想把所有的结果分个组,看看到底有哪些不重复的值。"
为了节省内存和提高速度,MySQL 在底层不需要记录数量 ,它的执行计划会变成 流式去重(Streaming / Filesort)。它的内部执行逻辑变成了这样:
-
扫描第 1 行 :调用一次表达式,
RAND()运行,算出来:security:0。MySQL 直接把:security:0吐给屏幕。 -
扫描第 2 行 :调用一次表达式,
RAND()运行,算出来:security:1。MySQL 直接把:security:1吐给屏幕。 -
扫描第 3 行 :调用一次表达式,
RAND()*运行,算出来:security:0。MySQL 发现屏幕上刚才输出过:security:0了,于是就把这一行丢弃(去重)。
在这种流式处理下,每扫描一行数据,上面的 CONCAT(...) 表达式有且仅会被调用"一次"。算出来是什么就是什么,直接拿去去重或输出,绝对不可能报错。
下一条语句,统计某个表中有多少行:
sql
select count(*) from users;

floor 报错注入语句通常长这样:
sql
select count(*),concat(0x3a,(database()),0x3a,floor(rand()*2)) x from information_schema.tables group by x;
该语句可能报错,可能正常,为什么?

因为 COUNT(*) 的加入,导致 MySQL 必须在内存里建立一张带计数器的临时哈希表。
它的结构长这样:
-
Key(主键/分组键) :存放
x的字符串。因为是主键,所以里面的值绝对不允许重复。 -
Value(计数器) :存放
COUNT(*)的统计数字。
现在,MySQL 开始一行行扫描 information_schema.tables 表。
MySQL 拿着第 1 行数据,第一次调用我们的表达式。此时 rand() 生成了一个数,假设计算结果为 :security:0。
MySQL 去查临时表:"现在有 :security:0 吗?" ➔ 没有(此时临时表完全是空的)。
判定 :这是一个新分组,MySQL 执行插入新行的动作,在写入主键的瞬间,MySQL重新调用 了一次表达式,假设运气不错,这次算出来的结果还是 :security:0。
结果:成功开辟新行。
此时临时表的状态: [:security:0] ➔ 计数为 1
接着 MySQL 扫描第 2 行数据,调用表达式。这次 rand() 变了,计算结果为 :security:1。
MySQL 去查临时表:"有 :security:1 吗?" ➔ 没有(目前只有 :security:0 )。
判定:又是一个新分组,MySQL 再次决定执行 INSERT 动作。
写入主键的瞬间,MySQL 重新调用了一次表达式。运气依然不错,算出来还是 :security:1。
结果:成功开辟第二行。
此时临时表的状态:
-
[:security:0]➔ 计数为 1 -
[:security:1]➔ 计数为 1
注意:此时 0 和 1 两个坑位都已经坐稳了。如果这句语句能撑过这一步,后面不管扫描多少行,都只会有 0 或 1,都不会再产生报错。
接下来我们看运气不好的情况:
假设在扫描前 2 行时,因为随机数的组合,临时表里目前只成功建立了一个房间:[:security:0] (1 房间还没被建立出来)
此时,MySQL 拿着第 3 行数据计算,rand() 恰好生成了一个让结果为 :security:1 的数字。
MySQL 拿着 :security:1 去查临时表:"有 :security:1 的房间吗?"
临时表回答:"没有 ,我这里现在只有 :security:0。"
MySQL 决定:"那就在临时表里新建一行(INSERT),把新组塞进去!"
就在插入的一瞬间,由于重新调用了表达式,rand() 突然变脸,导致整个 x 的计算结果变成了 :security:0
MySQL 拿着算出来的 :security:0,往临时表的新一行主键区里硬塞
临时表的主键索引瞬间报警:"疯了吧你!我第一行早就躺着一个:security:0了!主键必须唯一,你不能在查重的时候说是 1,写入的时候写成 0 让我重复!"
于是,整个 SQL 语句执行立刻中断,MySQL 抛出报错:
sql
ERROR 1062 (23000): Duplicate entry ':security:0' for key '<group_key>'