MYSQL GROUP BY+COUNT+RAND报错注入原理分析

这篇文章假设你和我一样只会基础的select操作,并在此基础上一步一步理解GROUP BY+COUNT+RAND这条报错注入的原理。

我们先来看一下这条语句

select count(*),(floor(rand(0)*2))x from information_schema.tables group by x;

你可能会有疑问,floor() 和rand()函数是做什么的?(floor(rand(0)*2))后面的x是什么,前后都有x,这个x是变量吗?group by 又是什么语句?他是如何报错的?rand(0)中的0可以是其他数吗?

认识函数

rand 函数

每次调用 RAND() 函数,都会随机生成一个 0~1 之间的随机数 。
当使用整数作为参数调用时,RAND() 使用该值作为随机数的种子。每次随机数的值使用给定值的种子生成,RAND() 将产生一个可重复的系列数字。

mysql> select rand(),rand(1),rand(1);
+-------------------+---------------------+---------------------+
| rand()            | rand(1)             | rand(1)             |
+-------------------+---------------------+---------------------+
| 0.743123070097643 | 0.40540353712197724 | 0.40540353712197724 |
+-------------------+---------------------+---------------------+
1 row in set (0.00 sec)

mysql> select rand(1)*2;
+--------------------+
| rand(1)*2          |
+--------------------+
| 0.8108070742439545 |
+--------------------+
1 row in set (0.00 sec)

floor函数

返回小于等于参数值的最大整数

mysql> select floor(0.8),floor(1.2);
+------------+------------+
| floor(0.8) | floor(1.2) |
+------------+------------+
|          0 |          1 |
+------------+------------+
1 row in set (0.01 sec)

floor(rand(0)*2)

返回一个0或1

同时0或1按固定序列生成。

设置随机数种子,每次生成的序列是固定的:

mysql> select floor(rand(0)*2) from user;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
|                1 |
|                0 |
|                1 |
|                1 |
+------------------+
6 rows in set (0.00 sec)

mysql> select floor(rand(0)*2) from user;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
|                1 |
|                0 |
|                1 |
|                1 |
+------------------+
6 rows in set (0.00 sec)

不设置随机数种子,生成的值是随机的:

mysql> select floor(rand()*2) from user;
+-----------------+
| floor(rand()*2) |
+-----------------+
|               1 |
|               0 |
|               0 |
|               1 |
|               0 |
|               0 |
+-----------------+
6 rows in set (0.00 sec)

mysql> select floor(rand()*2) from user;
+-----------------+
| floor(rand()*2) |
+-----------------+
|               0 |
|               0 |
|               0 |
|               0 |
|               1 |
|               1 |
+-----------------+
6 rows in set (0.00 sec)

group by 分组

user表结构:

mysql> select * from user;
+------------+----------+
| username   | password |
+------------+----------+
| test       | pass     |
| admin      | pass     |
| admin_root | pass12   |
| root       | pass12   |
| lofter     | 123456   |
| root       | pass12   |
+------------+----------+
6 rows in set (0.00 sec)

group by 后面接 字段名字字段位置 表示以该字段分组,当这个分组有多个记录时,只显示该分组的第一个记录值。

字段名字:

mysql> select * from user group by username;
+------------+----------+
| username   | password |
+------------+----------+
| test       | pass     |
| admin      | pass     |
| admin_root | pass12   |
| root       | pass12   |
| lofter     | 123456   |
+------------+----------+
5 rows in set (0.00 sec)

mysql> select * from user group by password;
+------------+----------+
| username   | password |
+------------+----------+
| test       | pass     |
| admin_root | pass12   |
| lofter     | 123456   |
+------------+----------+
3 rows in set (0.00 sec)

字段位置:

mysql> select * from user group by 1;
+------------+----------+
| username   | password |
+------------+----------+
| test       | pass     |
| admin      | pass     |
| admin_root | pass12   |
| root       | pass12   |
| lofter     | 123456   |
+------------+----------+
5 rows in set (0.00 sec)

mysql> select * from user group by 2;
+------------+----------+
| username   | password |
+------------+----------+
| test       | pass     |
| admin_root | pass12   |
| lofter     | 123456   |
+------------+----------+
3 rows in set (0.00 sec)

利用group by 我们可以很方便的进行分类计数:

每个用户名使用次数:

mysql> select username,count(*) from user group by username;
+------------+----------+
| username   | count(*) |
+------------+----------+
| test       |        1 |
| admin      |        1 |
| admin_root |        1 |
| root       |        2 |
| lofter     |        1 |
+------------+----------+
5 rows in set (0.00 sec)

每个密码使用次数:

mysql> select password,count(*) from user group by password;
+----------+----------+
| password | count(*) |
+----------+----------+
| pass     |        2 |
| pass12   |        3 |
| 123456   |        1 |
+----------+----------+
3 rows in set (0.01 sec)

group by 两个字段会发生什么?

mysql> select *,count(*) from user group by username,password;
+------------+----------+----------+
| username   | password | count(*) |
+------------+----------+----------+
| test       | pass     |        1 |
| admin      | pass     |        1 |
| admin_root | pass12   |        1 |
| root       | pass12   |        2 |
| lofter     | 123456   |        1 |
+------------+----------+----------+
5 rows in set (0.00 sec)

可以看到,只有两个字段都相等时才归为一类。

x是什么?

mysql> select 1,2,3;
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)

mysql> select 1,(2)x,3;
+---+---+---+
| 1 | x | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)

mysql> select user()x,(2)x,3;
+----------------+---+---+
| x              | x | 3 |
+----------------+---+---+
| root@localhost | 2 | 3 |
+----------------+---+---+
1 row in set (0.00 sec)

mysql> select user()x,(2)a,3;
+----------------+---+---+
| x              | a | 3 |
+----------------+---+---+
| root@localhost | 2 | 3 |
+----------------+---+---+
1 row in set (0.01 sec)

x充当着字段名的作用,实际上可以理解为字段名别名,可以是能组成字段名的任意字符。

mysql> select (username)a from user group by a;
+------------+
| a          |
+------------+
| test       |
| admin      |
| admin_root |
| root       |
| lofter     |
+------------+
5 rows in set (0.00 sec)

mysql> select username as a from user group by a;
+------------+
| a          |
+------------+
| test       |
| admin      |
| admin_root |
| root       |
| lofter     |
+------------+
5 rows in set (0.01 sec)

它是如何报错的?

虚表

回答这个问题之前,还有一个问题。

mysql是如何实现分组计数的?

在其他编程问题中你可以很容易的想到,循环遍历字段的值并将它作为字典的键,如果字典中已经有这个键,那这个键的值+1,否则创建一个新的键,继续循环。

mysql的"字典"为虚表。

以上面的select password,count(*) from user group by password;举例

为了对照方便,我们再在这里粘贴一下执行结果:

mysql> select password,count(*) from user group by password;
+----------+----------+
| password | count(*) |
+----------+----------+
| pass     |        2 |
| pass12   |        3 |
| 123456   |        1 |
+----------+----------+
3 rows in set (0.00 sec)

mysql> select * from user;
+------------+----------+
| username   | password |
+------------+----------+
| test       | pass     |
| admin      | pass     |
| admin_root | pass12   |
| root       | pass12   |
| lofter     | 123456   |
| root       | pass12   |
+------------+----------+
6 rows in set (0.00 sec)

由于user中有6条数据,那么这个“循环”就要执行6次

第一次“循环”:

检查虚表,发现不存在pass,

插入以pass为主键的一行数据

keycount(*)
pass1

第二次“循环”:

检查虚表,发现已存在pass,只进行count+1的操作

keycount(*)
pass2

第三次"循环":

检查虚表,发现不存在pass12,

插入以pass12为主键的一行数据

keycount(*)
pass2
pass121

第四次"循环":

检查虚表,发现已存在pass12,只进行count+1的操作

keycount(*)
pass2
pass122

第五次"循环":

检查虚表,发现不存在123456,

插入以123456为主键的一行数据

keycount(*)
pass2
pass122
1234561

第六次"循环":

检查虚表,发现已存在pass12,只进行count+1的操作

keycount(*)
pass2
pass123
1234561

至此,我们已经得到和mysql一样的数据

mysql> select password,count(*) from user group by password;
+----------+----------+
| password | count(*) |
+----------+----------+
| pass     |        2 |
| pass12   |        3 |
| 123456   |        1 |
+----------+----------+
3 rows in set (0.00 sec)

当我们引入随机数以后发生了什么?

随机数的问题

select count(*),(floor(rand(0)*2))x from information_schema.tables group by x;

其实可以化成这句

select count(*) from information_schema.tables group by floor(rand(0)*2);

道理一样。

现在我们只是引入了一个随机数,就使他报错了。

发生了什么呢?

其实mysql官方有给过提示,就是查询的时候如果使用rand()的话,该值会被计算多次,那这个“被计算多次”到底是什么意思,就是在使用group by的时候,floor(rand(0)*2)会被执行一次,如果虚表不存在记录,插入虚表的时候会再被执行一次

什么意思?我们还是一步一步来看。

再此之前,我们先准备一个长一点的floor(rand(0)*2)序列

mysql> select floor(rand(0)*2) from information_schema.tables limit 1,20;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
|                1 |
|                0 |
|                1 |
|                1 |
|                0 |
|                0 |
|                1 |
|                1 |
|                1 |
|                0 |
|                1 |
|                1 |
|                1 |
|                0 |
|                1 |
|                0 |
|                0 |
|                0 |
+------------------+
20 rows in set (0.00 sec)

第一次“循环”:

检查虚表,发现不存在floor(rand(0)*2)floor(rand(0)*2) 第一次运行 其值为0)的键,

插入以floor(rand(0)*2)floor(rand(0)*2) 第二次运行 其值为1)为主键的一行数据

keycount(*)
11

第二次“循环”:

检查虚表,发现存在floor(rand(0)*2)floor(rand(0)*2) 第三次运行 其值为1)的键,只对count(*)进行+1

keycount(*)
12

第三次“循环”:

检查虚表,发现不存在floor(rand(0)*2)floor(rand(0)*2) 第四次运行 其值为0)的键,

插入插入以floor(rand(0)*2)floor(rand(0)*2) 第五次运行 其值为1)为主键的一行数据,报错

此时,主键已经有键为1的数据,主键不可重复,固发生报错。

keycount(*)
12

结束

至此我们已经分析完了GROUP BY+COUNT+RAND报错注入的原理,报错注入的数据位置实际上就相当于1的位置变成了另一个固定值,不影响整个流程。GROUP BY,COUNT,RAND三者缺一不可。

感兴趣的师傅可以分析一下当rand(1)时为什么查出来的数据是这样的:

mysql> select count(*) from user group by floor(rand(1)*2);
+----------+
| count(*) |
+----------+
|        3 |
|        3 |
+----------+
2 rows in set (0.00 sec)