作者:phith0n@長亭科技
Pwnhub 公開賽出了個簡單的 PHP 代碼審計題目,考點有兩個:
- 由 http://www.phpjiami.com/ 加密過的源碼還原
- 上傳取后綴方式不同導致的文件上傳漏洞
如果說僅為了做出題目拿到 flag,這個題目太簡單,后臺也有數十名選手提交了答案和 writeup。但深入研究一下這兩個知識點,還是很有意思的。
首先通過簡單的目錄掃描,找到備份文件 index.php.bak。下載后發現文件是經過了混淆加密處理的,大部分同學是直接網上找了付費解密的網站給解的,也有少數幾個人說明了解密方法,我挑幾種方法說一下。
0x01 phpjiami 代碼分析破解法
這種方法我最佩服了,作者甚至給出了解密腳本,文章如下: http://sec2hack.com/web/phpjiami-decode.html 。
我自己在出題目之前也進行過分析,但后面并沒有耐心寫一個完整的腳本出來,所以我十分佩服這個作者。
我們分析 phpjiami 后的文件,可以看到他有如下特點:
- 函數名、變量名全部變成“亂碼”
- 改動任意一個地方,將導致文件不能運行
之所以函數名、變量名可以變成“亂碼”,是因為PHP的函數名、變量名是支持除了特殊符號以外大部分字符的,比如漢字等。利用這一特點,phpjiami 就將所有正常的英文變量給轉換了一下形式,其實沒有什么特別的奧秘。
那么,為了方便分析,我們可以想辦法再將其轉換回英文和數字。比如,作者使用的是 http://zhaoyuanma.com/phpcodefix.html 對混淆過的代碼進行美化;而我是使用 https://github.com/nikic/PHP-Parser 對整個代碼進行了結構化的分析,并將所有變量和函數名進行了美化。
方法一的好處是我不需要寫任何代碼,就可以大致進行美化,但顯然,美化后的代碼是有錯誤的,原文中也提到了這一點;方法二,雖然需要自己寫代碼,但美化后的代碼沒有語法錯誤,看起來更加直觀,并且我還能進一步的進行美化,比如將字符串中的亂碼轉換成\x的形式。
我美化后的代碼如下:

后續的操作和上文也差不多,通過源碼的分析,正如上文中所說,phpjiami 加密源碼的整個流程是:
加密流程:源碼 -> 加密處理(壓縮,替換,BASE64,轉義)-> 安全處理(驗證文件 MD5 值,限制 IP、限域名、限時間、防破解、防命令行調試)-> 加密程序成品,再簡單的說:源碼 + 加密外殼 == 加密程序 (該段出處)
所以,其實這種方法并沒有對源碼進行混淆,只是對“解密源碼的殼”進行了混淆。所以你看到的中文變量、中文函數,其實是一個殼,去掉這層殼,我可以拿到完整的PHP源碼。
所以呀,后臺提交的 writeup 里,有的同學想當然地認為修改 eval 為 echo 就能輸出源碼了……實際上根本沒實際試過,改動文件是會導致不能運行的;還有同學認為這里僅是將源碼混淆為用戶體驗極差的代碼,導致人眼無法閱讀,并沒有理解這里其實混淆的不是源碼。
0x02 HOOK EVAL 法
0x01中說到的方法固然是很美好的,但是假如加密者隨意改動一點加密的邏輯,可能導致我們需要重新分析加密方法,寫解密腳本。我們有沒有更通用的方法?
HOOK EVAL應該是被提到過最多的方法,我也看到了 Medici.Yan 發布的一篇文章。
我前文說過,phpjiami 其實是只是混淆了殼,這個殼的作用是執行真正的源碼。那么,執行源碼必然是會經過 eval 之類的“函數”(當然也不盡然),那么,如果我們能夠有辦法將 eval 給替換掉,不就可以獲得源碼了么?
遺憾的是,如果我們僅僅簡單地將 eval 替換成 echo,將導致整個腳本不能運行——因為 phpjiami 檢測了文件是否被修改。
那么,我們可以尋求更底層的方法。就是很多人以前提到過的,將 PHP 底層的函數 zend_compile_string 給攔截下來,并輸出值。Medici.Yan 的文章中說的很清楚,也給出了參考文檔和源碼,我就不再贅述了。
我自己簡單寫了一個擴展,并用 php5.6 編譯: https://drive.google.com/open?id=0B4uxE69uafD5anVTZ1VwNXN0WEU
下載之,在 php.ini 中添加 extension=hookeval.so,然后直接訪問加密過的 php 代碼即可(當時參考 tool.lu 的站長 xiaozi 的代碼 ,所以分隔符里有關鍵字):

16年 kuuki 曾分享過一個在線解密的工具,但測試了一下 phpjiami 解密不了。原因是,phpjiami 在解密的時候會進行驗證:
php_sapi_name() == 'cli' ? die():'';
所以如果這個源碼是在命令行下運行,在執行這條語句的時候就 die 了。所以,即使你編譯好了 hookeval.so 并開啟了這個擴展,也需要在 Web 環境下運行。
提高篇:有沒有什么簡單的辦法在命令行下也能模擬 web 環境呢?方法我先不說,大家可以自己思考思考。
0x03 手工 dump 法
那么有的同學說:php 擴展太難了,我不會寫C語言,怎么辦?
不會寫C語言也沒關系,你只需要會寫 PHP 即可。這是我鳳凰師傅提到的一個方法,也是我理想中的一個解,非常簡單,兩行代碼搞定,解密用時比你去網上花錢解密還短:
<?php
include "index.php";
var_dump(get_defined_vars());
原理其實也很簡單。phpjiami 的殼在解密源碼并執行后,遺留下來一些變量,這些變量里就包含了解密后的源碼。
雖然我們不能直接修改 index.php,將這些變量打印出來,但是我們可以動態包含之,并打印下所有變量,其中必定有我們需要的源碼(var_dump 輸出的不完整,只是用它舉個例子):

當然,這個方法雖然簡單,但有個很嚴重的問題:假如在執行源碼的過程中exit()了,我們就執行不到打印變量的地方了。
所以,這個方法并不一定適用于所有情景,但對于本題來說,已經足夠了。
0x04 動態調試法
那么,如果我們遇到0x03解決不了的情況怎么辦?
這時候就要祭出動態調試武器了。盡管加密后的文件看起來亂七八糟,但其仍然是一個符合 php 語法的 php 文件,那么我們就可以直接利用動態調試工具進行單步調試,拿到源碼。
簡單拿 xdebug 進行調試,不停單步調試后,就可以發現我們需要的源碼已經在上下文變量中的:

右鍵“復制值”,即可拿到源碼。這也算一個比較簡單的方法了。
當然,假如有一天 phpjiami 修改了混淆流程,源碼不再儲存于變量中,那么就需要分析一下代碼執行的流程。所謂萬變不離其中,最終斷在 eval 的那一步,一定有你需要的源碼。
0x05 代碼審計 Getshell
后面的部分反而比較簡單。拿到 index.php 的源碼后,發現其包含了 FileUpload.class.php,所以再次下載這個文件的源碼進行解密。
分析 FileUpload 類,發現其取后綴有兩種方式:將文件名用.分割成數組$arr,一是用$arr[count($arr)-1]的方式取數組最后一個元素,二是用end($arr)的方式取數組最后一個元素。
正常來說,字符串用.分割成的數組,用這兩種方法取到的末元素應該是相同的。但取文件名的時候,如果我們已經傳入的是數組,則不會再次進行分割:
$filename = $_POST[...];
if(!is_array($filename)) {
$filename = explode('.', $filename);
}
也就是說我能控制 $filename 這個數組。所以,我只需要找到 $arr[count($arr)-1] 和 end($arr) 的區別,即可繞過后綴檢查。
顯然,前者是取根據數組下標來取的值,后者取的永遠是數組里最后一個元素。所以,我們只需要讓下標等于count($arr)-1的元素不是數組最后一個元素即可。
比如:[1=>'gif', 0=>'php']或者['0'=>'abc', '2'=>'gif', '100'=>'php']。
0x06 總結
最后想說一句話:不求甚解是阻礙部分人進步的一大阻力。共勉。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/406/
暫無評論