博客 / 詳情

返回

【攻防世界】web | easyphp詳細題解WP

## 今天我們來解析一道【攻防世界】中的web題--easyphp

首先我們打開這道題的場景:

發現這道題一上來就給了我們一大段的php代碼,很明顯這是一道代碼審計題,因此我們需要看懂這段代碼的意思後來構造符合代碼的payload。
大概審完代碼後我們知道只有當\(key1和\)key2均為1時才會包含Hgfks.php並輸出flag,因此這是一道設計PHP弱類型比較、函數行為差異與邏輯矛盾點的題。

首先我們審計代碼,發現一共有三個參數:

$a = $_GET['a'];
$b = $_GET['b'];
$c=(array)json_decode(@$_GET['c']); 

説明需要有三個參數來構造payload,即應該寫成(?a=xxxxx&b=xxxx&c=xxxxx)的形式。

1.審計$a的條件代碼語句:

if(isset($a) && intval($a) > 6000000 && strlen($a) <= 3)
//這行代碼先是查看a的值是否為空。a的值不為空後,將a值使用intval()函數轉化為整數並大於6000000,然後又要求a
//值的長度小於等於3
//一方面要求值大於6000000,另一方面又要求a值的長度小於等於3,這似乎很矛盾~~ 但卻可以通過PHP中的科學計數法
//來輕鬆解決(即1e7、2e7、3e7等)

**為啥PHP科學計數法可以解決這個矛盾點?**
PHP對科學計數法格式的字符串有着特殊的解析規則
1.科學計數法的格式:數字e數字(如1e7),表示1×10^7=10000000
2.intval("1e7"):PHP會將它解析為整數10000000(大於6000000)
3.sttrlen("1e7")=3,符合if語句對a值的判斷

2.審計$b的條件判斷語句:

if(isset($b) && '8b184b' === substr(md5($b),-6,6))
//這行代碼同樣先是判斷b值是否為空。b的值不為空後,接着將b值轉換為MD5哈希值後取最後六位,並要求最後六位嚴格等於 '8b184b' 這個字符串

**那如何獲取到MD5哈希值最後六位為 8b184b 這個字符串的值呢**
這裏我們使用python代碼來獲取:

import hashlib 
for i in range(1000000): ##遍歷0到1000000的數字足夠找到結果
    b_str = str(i) ##將數字轉化為字符串,MD5需要字符串輸入
    md5_result = hashlib.md5(b_str.encode('utf-8')).hexdigest() ##計算MD5哈希值(utf-8編碼)
    last_md5 = md5_result[-6:]  ##取最後的六位進行判斷
    if last_md5 == '8b184b':
        print(f"滿足條件的數字為:{b_str}")
        print(f"完整的md5值為:{md5_result}")
        break
##運行代碼得到b=53724

補充①:substr(md5(\(b),-6,6))的具體含義: 1.md5(\)b):對b值進行MD5哈希運算,MD5哈希會把任意長度的輸入,轉化為固定的32位的16進制字符串(比如:
$b=53724,轉為MD5後位xxxx8b184b。之所以會要求取最後的六位,而不是整個32位MD5哈希值也是為了讓
題目可解。
因為完整的MD5哈希值進行枚舉的話,組合數高達32×16≈10^38,不可能完成。失去題目的考察意義,而規定取
最後的6位,組合數雖然也有16×6= 16777216(約 1600 萬),但實際枚舉的過程中b的值是數字(沒有限制b值
的類型)。而數字枚舉最快,並且MD5值分佈均勻,大概在[0-100萬]的範圍就能找到,普通電腦枚舉 30秒內就
能出結果。
2.substr(......,-6,6)):substr是 PHP 的字符串截取函數,第一個參數:要截取的字符串(這裏是 MD5 哈希值)
第二個參數:起始位置(-6表示「從字符串末尾倒數第 6 位開始」) 第三個參數:截取長度(6表示截取 6 個字
符)→ 最終效果:取 MD5 哈希值的最後六位

3.審計$c的條件判斷語句:

通過審計我們知道c的值是一個數組格式的json值,包含m和n兩個參數,我們先來説m這個參數:

$c=(array)json_decode(@$_GET['c']); //傳入的 c 參數是 GET 字符串,必須是合法的 JSON 格式(否則 json_decode 返回 null)。(array)強制將json_decode的結果轉化為關聯數組的形式,@:抑制錯誤(比如 c 參數不傳時,不報錯)
if(is_array($c) && !is_numeric(@$c["m"]) && $c["m"] > 2022) //首先判斷c的值是否為數組(只要 c 是合法 JSON 對象/數組,轉成數組後就滿足),滿足後要求$c["m"]的值不能是 “純數字”(包括整數、浮點數、科學計數法),並且要大於2022。
//滿足上述條件的方法就是給數字加非數字後綴(如 2023a):
// is_numeric("2023a")=false(非純數字)
// 2023a在比較時會隱式轉成 2023(>2022)

​ 再來説n這個參數:

if(is_array(@$c["n"]) && count($c["n"]) == 2 && is_array($c["n"][0]))
//首先判斷$c["n"]的值是否為數組,滿足後要求$c["n"]這個數組的長度必須為2,而且第一位還得是數組。因此便可以構造n為[[],0]

​ 接下來我們看滿足條件後的要求吧:

 $d = array_search("DGGJ", $c["n"]); 
 $d === false?die("no..."):NULL; //必須找到DGGJ($d≠false)
     foreach($c["n"] as $key=>$val){ // 遍歷n時,不能有元素嚴格等於DGGJ
            $val==="DGGJ"?die("no......"):NULL;
        }

**所以這裏這裏的矛盾點就是要保證array_search不為false(即找到DGGJ),但在遍歷n的時候又不能又元素嚴格等於DGGJ**
​ 因此這裏的繞過思路便是利用PHP array_search 的弱類型匹配:
​ array_search默認是非嚴格匹配機制(第三個參數 \(strict=false),會觸發隱式類型轉換: ​ array_secrch函數的格式:array_search(\)needle, $haystack, \(strict=false) ​ ①.\)strict=false(非嚴格匹配):不要求 “值 + 類型” 都一致,PHP 會先嚐試將(搜索值)和haystack(數組)的元素
​ ②. $strict=true(嚴格匹配):必須 “值 + 類型” 都一致(===),才會匹配成功;
​ ③.本題中沒有傳第三個參數,所以用默認的非嚴格匹配。

回到本題,所以PHP執行時的步驟如下:
①.當搜索字符串"DGGJ"時,PHP 會從字符串開頭找數字字符,找到就轉成對應整數;如果開頭沒有數字,直接轉成0(例如:"123abc" → 123;"abc123" → 0;"DGGJ" → 0(無任何數字)
②.數組中的0會和"DGGJ"發生弱比較:0 == "DGGJ" → 結果為 true;
③.但0 === "DGGJ" → 結果為 false(嚴格匹配,類型不同),使得遍歷時找不到DGGJ
因此結合 m 和 n 的合法值,c 參數的 JSON 格式為:

{"m":"2023a","n":[[],0]} //注意:JSON 中不能有空格(比如{"m": "2023a"}有空格,會導致 json_decode 失敗),必須緊湊寫

同時也要注意:
1.不是array_search函數 “主動轉”,而是 PHP 的弱類型比較機制自動觸發的;
2.轉換的觸發條件是 “搜索值和數組元素類型不同(字符串 vs 整數)”,且 “非嚴格匹配”;
3.轉換方向是「字符串轉整數」,而非「整數轉字符串」(這是 PHP 的固定規則)

所以本題的最終payload為:

?a=1e7&b=53724&c={"m":"2023a","n":[[],0]}

輸入後便可成功拿到flag:

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.