作者:phith0n@長亭科技

Pwnhub 公開賽出了個簡單的 PHP 代碼審計題目,考點有兩個:

  1. http://www.phpjiami.com/ 加密過的源碼還原
  2. 上傳取后綴方式不同導致的文件上傳漏洞

如果說僅為了做出題目拿到 flag,這個題目太簡單,后臺也有數十名選手提交了答案和 writeup。但深入研究一下這兩個知識點,還是很有意思的。

首先通過簡單的目錄掃描,找到備份文件 index.php.bak。下載后發現文件是經過了混淆加密處理的,大部分同學是直接網上找了付費解密的網站給解的,也有少數幾個人說明了解密方法,我挑幾種方法說一下。

0x01 phpjiami 代碼分析破解法

這種方法我最佩服了,作者甚至給出了解密腳本,文章如下: http://sec2hack.com/web/phpjiami-decode.html

我自己在出題目之前也進行過分析,但后面并沒有耐心寫一個完整的腳本出來,所以我十分佩服這個作者。

我們分析 phpjiami 后的文件,可以看到他有如下特點:

  1. 函數名、變量名全部變成“亂碼”
  2. 改動任意一個地方,將導致文件不能運行

之所以函數名、變量名可以變成“亂碼”,是因為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 總結

最后想說一句話:不求甚解是阻礙部分人進步的一大阻力。共勉。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/406/