什么是SQL双注入?
SQL双注入属于SQL报错注入的一种,即通过利用数据库的bug,将我们想要的信息放入报错信息中,在前端可以回显报错信息的前提下,完成敏感信息的获取。
背景知识
什么是双查询?
双查询指的就是在一个Select语句中再次嵌入一个select语句,如:
SQL
SELECT COUNT((select database()))
在查询的时候,会先执行select database()语句,然后再将该语句的执行结果传递给count()函数,从内到外依次执行。 对于双查询注入,大家需要先了解count()、rand()、floor()这三个函数的功能以及group by语句的用法。
要用到的SQL函数
count()
count():用来统计表中或数组中记录数目的一个函数,count(*)表示计算查询出的表中所有的行数
floor()
floor():返回小于等于该值的最大整数,采用向下取整方式
rand() --- rand(): 产生随机数,可以指定固定种子,即调用时采用rand(0)的方式,此时随机种子固定为0
关于rand函数有一点需要说明: 如果使用 rand()函数,该函数生成的随机序列随机性较高,而指定随机种子后,如下图所示:
rand(0)的查询结果几乎消除了floor(rand()*2)
函数原有的随机性,或者说每次查询时其生成的随机序列相同,连续查询几次,我们会发现它的规律如下:01101
group by子句
在表中再插入两条数据,name值都为"bbb":
sql
mysql> INSERT INTO test VALUES("3","bbb");
mysql> INSERT INTO test VALUES("4","bbb");
成功后表如下:
这时候我们使用group by 语句时,MySQL会将查询结果分类汇总,重复的内容会合并为一项:
vbnet
mysql> SELECT name FROM test GROUP BY name;
这时候再使用count()函数就可以对不同的条目计数:
sql
mysql> SELECT count(*),name FROM test GROUP BY name;
如图:aaa有一条,bbb有3条
group by语句的实现原理
在MYSQL中,group by语句有五种实现方式,其中四种方式都是通过临时表来实现的,以上图中的语句为例,即:
sql
mysql> SELECT count(*),name FROM test GROUP BY name;
在执行group by name语句时,MySQL会在内部建立一个虚拟表,用来储存列的数据,而group by字句所指明的列会作为表的主键,当查询数据时,取数据库数据,然后查看虚拟表中是否存在相同主键值的数据,不存在则插入新记录。如下图所示:
- 图1代表mysql建立了一张空白虚拟表,其中name列作为主键,count(*)记录主键为某一值的行的数目。
- 图2为当读取到第一行数据时,aaa不存在,将aaa放入主键列中,1放在id列中
- 图3为,当读取name=='bbb'的数据时,发现主键没有值'bbb',于是插入新行并计数
- 往下执行,遇到多余的bbb,已经有bbb存在,则将'bbb'行的count(*)列加1。内部情况如图4
这样就能对上面的分类结果进行统计,然后将统计结果返回:
而双查询报错的关键就在这里,主要的原因在于rand()函数在group by的过程中被触发了多次,事实上这也算MYSQL数据库的一个bug。
报错原理
让我们回看一下构造的报错语句:
sql
mysql> SELECT count(*),concat((SELECT database()),"~",floor(rand(0)*2))as a FROM test GROUP BY a;
首先,当rand(0)函数执行五次时,其产生的序列为:01101
因此,当MYSQL运行这条语句时,虚拟表中的变化情况如下图所示:
1. 执行前虚拟表为空:
2. 第一次执行
当处理第一行时,这时的concat((SELECT database()),"~",floor(rand(0)*2))
生成结果为sql_test~0
,group就以sql_test~0
查询虚拟表,发现表中没有该值的主键,于是将这条语句的结果 插入到虚拟表中。注意!是将这条语句的结果插入到虚拟表中,而不是将 sql_test~0
插入到虚拟表中。 或者说,在查询某一列数据是否存在于虚拟表和向虚拟表插入数据时,rand()函数都会运行一次。
由于虚拟表为空,所以会将其插入到虚拟表中,这里的插入过程中,concat((SELECT database()),"~",floor(rand(0)*2))
语句中的rand()函数会再次执行,即插入的值为sql_test~1
:
而原本我们想插入的记录实际上是sql_test~0
。所以上面的情况就是用sql_test~0
这个结果查询虚拟表,不存在该数据,于是插入虚拟表,插入时又运算一次,然后插入的值变成了sql_test~1
,所以这就是主要的冲突,当表中没有数据时,即使查询虚拟表的值和插入虚拟表的值不是同一个,但虚拟表也只生成一条记录,不会出现问题。
然而当表的数据出现两条以上的时候,即group by 在处理完第一条数据后会往下继续处理第二条,于是第二条还会按第一条的处理方式进行:
3. 第二次执行
在处理第二行数据时,此时执行rand(0)返回的结果为1(此时对应上面01101的第三次查询结果1),生成结果为sql_test~1,MYSQL查询虚拟表发现有该条记录,于是将更新计数,注意更新计数时rand函数不会再次执行。
4. 第三次执行
取第三条记录查询,此时执行rand(0)返回的结果为0(此时对应上面01101的第四次查询结果0),发现虚拟表中没有键0,所以要将其写入虚拟表。同样在写入虚拟表的时候,rand(0)又执行了一遍,此时查询结果为上面01101的第五次结果1,但是键1已经存在虚拟表中,由于键只能唯一,所以此时就会报错。所以在使用floor()、rand(0)、count()、group by时,数据表中至少要有3条记录才会报错.
报错提示如下:
rust
ERROR 1062 (23000): Duplicate entry 'sql_test~0' for key 'group_key'
由此,就可以通过报错提示来获取敏感信息了~