BUUOJ Web的第一題,其實是很有質量的一道題,但是不知道為什么成了Solved最多的題目,也被師傅們笑稱是“勸退題”,這道題的原型應該是來自於phpMyadmin的一個文件包含漏洞(CVE-2018-12613)
解題過程
解題思路
進入題目查看源代碼發現提示:
跟進source.php得到源代碼:
<?php highlight_file(__FILE__); class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; } if (in_array($page, $whitelist)) { return true; } $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } $_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } echo "you can't see it"; return false; } } if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) ) { include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; } ?>
代碼分為兩部分來看,第一部分是定義了emmm類的一個checkFile函數,用來檢查傳入的file參數是否合規:
class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; if (! isset($page) || !is_string($page)) { echo "you can't see it"; return false; } if (in_array($page, $whitelist)) { return true; } $_page = mb_substr( $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } $_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') ); if (in_array($_page, $whitelist)) { return true; } echo "you can't see it"; return false; } }
第二部分是程序全局代碼,從這里我們可以得到包含文件的三個條件:
if (! empty($_REQUEST['file']) && is_string($_REQUEST['file']) && emmm::checkFile($_REQUEST['file']) //用$_REQUEST來接收file參數,如果file參數的值不為空、為字符串、可以通過emmm類checkFile函數檢測,則包含該文件 ) { include $_REQUEST['file']; exit; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />"; }
在emmm類的checkFile函數中我們可以看到白名單中有兩個文件:
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
包含一下hint.php文件來試試 /source.php?file=hint.php :
提示我們Flag在ffffllllaaaagggg中,那么我們解題的關鍵就是如何包含這個在whitelist外的文件
從第二部分代碼我們可以看出,重點是如何讓ffffllllaaaagggg通過checkFile的檢查,因此我們就需要從emmm類的checkFile函數入手
前置知識點
我們在讀這個函數的代碼前需要提前了解幾個checkFile函數中出現的函數:
1. in_array() 函數搜索數組中是否存在指定的值
更多信息參考:https://www.w3school.com.cn/php/func_array_in_array.asp
2. mb_substr() 函數返回字符串的一部分,之前我們學過 substr() 函數,它只針對英文字符,如果要分割的中文文字則需要使用 mb_substr()
更多信息參考:https://www.runoob.com/php/func-string-mb_substr.html
3. mb_strpos() 查找字符串在另一個字符串中首次出現的位置
更多信息參考:https://www.php.net/manual/zh/function.mb-strpos.php
核心代碼分析
在了解了上面的幾個小點之后我們返回來看核心代碼:
class emmm { public static function checkFile(&$page) { $whitelist = ["source"=>"source.php","hint"=>"hint.php"]; if (! isset($page) || !is_string($page)) { //page必須不為空或為字符串 echo "you can't see it"; return false; } if (in_array($page, $whitelist)) { //in_array()檢測page是否在whitelist中 return true; } $_page = mb_substr( //如果page含有?,則獲取page第一個?前的值並賦給_page變量 $page, 0, mb_strpos($page . '?', '?') ); if (in_array($_page, $whitelist)) { //檢測_page是否在whitelist中 return true; } $_page = urldecode($page); //給_page二次賦值,使其等於URL解碼之后的page $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') //如果_page有?,則截取_page(URL解碼后的page)兩個?中間的值 ); if (in_array($_page, $whitelist)) { //檢測_page是否在whitelist中 return true; } echo "you can't see it"; return false; } }
邏輯可能看着有點亂,所以我們梳理一下邏輯,這里引用王嘆之師傅的分析:
可以看到函數代碼中有四個if語句:
第一個if語句 對變量進行檢驗,要求$page為字符串,否則返回false //因為返回False所以這里無用
第二個if語句 判斷$page是否存在於$whitelist數組中,存在則返回true
第三個if語句 判斷截取后的$page是否存在於$whitelist數組中,截取$page中'?'前部分,存在則返回true
第四個if語句 判斷url解碼並截取后的$page是否存在於$whitelist中,存在則返回true
若以上四個if語句均未返回值,則返回false有三個if語句可以返回true,第二個語句直接判斷$page,不可用
第三個語句截取'?'前部分,由於?被后部分被解析為get方式提交的參數,也不可利用
第四個if語句中,先進行url解碼再截取,因此我們可以將?經過兩次url編碼,在服務器端提取參數時解碼一次,checkFile函數中解碼一次,仍會解碼為'?',仍可通過第四個if語句校驗。
只要這四個if語句有一個為true即可包含file,關鍵點在_page 經過截斷后返回true.
所以我們的突破點就在於第四個if語句中,只要滿足他的條件,我們就可以包含文件:
$_page = urldecode($page); $_page = mb_substr( $_page, 0, mb_strpos($_page . '?', '?') );
if (in_array($_page, $whitelist)) { return true; }
這里URL解碼了一次$page值,這里需要注意的是,PHP中$_GET、$_POST、$_REQUEST這類函數在提取參數值時會URL解碼一次
而這里在代碼中又一次URL解碼了一次,共計解碼了兩次,所以我們也需要對傳入的值進行兩次URL編碼
其次我們的突破點就在於這段代碼只會截取?之前的字符串拿去和whitelist比對,因此只要確保?前的值是source.php或hint.php即可返回true
等同於 /source.php?file=source.php%253F123456 便可以使用include()函數包含 source.php?123456 這個文件(%253f是?URL編碼兩次后的值)
所以可以構造Payload:
/index.php?file=source.php%253F/../../../../ffffllllaaaagggg
需要注意的是,這里之所以可以包含到ffffllllaaaagggg是因為PHP將 source.php%253F/ 視作了一個文件夾,然后 ../ 的用途是返回上級目錄
ffffllllaaaagggg位於根目錄下,一般Web服務的文件夾在/var/www/html目錄中,再加上source.php?/這個“文件夾”,所以我們總共需要../四次來返回到根目錄
(如果比賽中不知道flag具體位置的話可以一層一層來試)