1. 程式人生 > 實用技巧 >【PHP]php://filter之妙用

【PHP]php://filter之妙用

php://filter是PHP中獨有的協議,利用這個協議可以創造很多“妙用”,本文說幾個有意思的點,剩下的大家自己下去體會。本來本文的思路我上半年就準備拿來做XDCTF2016的題目的,沒想到被三個白帽的一題搶先用了,我也就只好提前分享一下。

XXE中的使用

php://filter之前最常出鏡的地方是XXE。由於XXE漏洞的特殊性,我們在讀取HTML、PHP等檔案時可能會丟擲此類錯誤parser error : StartTag: invalid element name。其原因是,PHP是基於標籤的指令碼語言,<?php ... ?>這個語法也與XML相符合,所以在解析XML的時候會被誤認為是XML,而其中內容(比如特殊字元)又有可能和標準XML衝突,所以導致了出錯。

那麼,為了讀取包含有敏感資訊的PHP等原始檔,我們就要先將“可能引發衝突的PHP程式碼”編碼一遍,這裡就會用到php://filter。

php://filter是PHP語言中特有的協議流,作用是作為一個“中間流”來處理其他流。比如,我們可以用如下一行程式碼將POST內容轉換成base64編碼並輸出:

readfile("php://filter/read=convert.base64-encode/resource=php://input");

如下:

所以,在XXE中,我們也可以將PHP等容易引發衝突的檔案流用php://filter協議流處理一遍,這樣就能有效規避特殊字元造成混亂。

如下,我們使用的是php://filter/read=convert.base64-encode/resource=./xxe.php

巧用編碼與解碼

使用編碼不光可以幫助我們獲取檔案,也可以幫我們去除一些“不必要的麻煩”。

記得前段時間三個白帽有個比賽,其中有一部分程式碼大概類似於以下:

<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

$content在開頭增加了exit過程,導致即使我們成功寫入一句話,也執行不了(這個過程在實戰中十分常見,通常出現在快取、配置檔案等等地方,不允許使用者直接訪問的檔案,都會被加上if(!defined(xxx))exit;之類的限制)。那麼這種情況下,如何繞過這個“死亡exit”?

幸運的是,這裡的$_POST['filename']是可以控制協議的,我們即可使用 php://filter協議來施展魔法:使用php://filter流的base64-decode方法,將$content解碼,利用php base64_decode函式特性去除“死亡exit”。

眾所周知,base64編碼中只包含64個可列印字元,而PHP在解碼base64時,遇到不在其中的字元時,將會跳過這些字元,僅將合法字元組成一個新的字串進行解碼。

所以,一個正常的base64_decode實際上可以理解為如下兩個步驟:

<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);

所以,當$content被加上了<?php exit; ?>以後,我們可以使用 php://filter/write=convert.base64-decode 來首先對其解碼。在解碼的過程中,字元<、?、;、>、空格等一共有7個字元不符合base64編碼的字元範圍將被忽略,所以最終被解碼的字元僅有“phpexit”和我們傳入的其他字元。

“phpexit”一共7個字元,因為base64演算法解碼時是4個byte一組,所以給他增加1個“a”一共8個字元。這樣,"phpexita"被正常解碼,而後面我們傳入的webshell的base64內容也被正常解碼。結果就是<?php exit; ?>沒有了。

最後效果是 :

利用字串操作方法

有的同學說,base64的演算法我不懂,上面的方法太複雜了。

其實,除了使用base64特性的方法外,我們還可以利用php://filter字串處理方法來去除“死亡exit”。我們觀察一下,這個<?php exit; ?>實際上是什麼?

實際上是一個XML標籤,既然是XML標籤,我們就可以利用strip_tags函式去除它,而php://filter剛好是支援這個方法的。

編寫如下測試程式碼即可檢視 php://filter/read=string.strip_tags/resource=php://input 的效果:

echo readfile('php://filter/read=string.strip_tags/resource=php://input');

可見,<?php exit; ?>被去除了。但回到上面的題目,我們最終的目的是寫入一個webshell,而寫入的webshell也是php程式碼,如果使用strip_tags同樣會被去除。

萬幸的是,php://filter允許使用多個過濾器,我們可以先將webshell用base64編碼。在呼叫完成strip_tags後再進行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被還原。

最終的資料包如下:

除此之外,我們還可以利用rot13編碼獨立完成任務。原理和上面類似,核心是將“死亡exit”去除。<?php exit; ?>在經過rot13編碼後會變成<?cuc rkvg; ?>,在PHP不開啟short_open_tag時,php不認識這個字串,當然也就不會執行了:

當然,這個方法的條件就是不開啟短標籤。