反序列化漏洞的核心思維與案例解讀
web安全101之反序列化
序列化的核心思維旨在,將A變成B,最后再從B還原回A。
總之,在一些條件苛刻或者變化無常的環境與需求中,產生了這種靈活的可逆性的B的中間體。
理解不安全反序列化的最好方法是了解不同的編程語言如何實現序列化和反序列化。這里的序列化與反序列化指的是程序語言中自帶的實施與實現。而非自創或者自定義的序列化與反序列化機制(比如:N進制形式hashmap樹型等其他數據結構里的序列化中間體)。
總之,大多數面向對象的編程語言都具有類似的序列化和反序列化的接口或者說函數,但它們序列化對象的格式不同。一些編程語言還允許開發人員序列化為其他標準化格式,例如 JSON , YAML,N進制形式,hashmap,樹型等其他數據結構。
PHP前置知識
我們將已PHP與JAVA為例。
debug
您需要安裝 PHP 和 Java。或者使用在線運行時環境(并非所有的在線版都支持序列化與反序列化功能)
PHP序列化
有時會將PHP反序列化稱作PHP對象注入,但它們描述的是同一個東西。
https://extendsclass.com/php.html 在線編輯器
我們創建了類User,定義了變量,并實例化類和給它的變量賦值,最后生成序列化對象并echo。

PHP序列化的基本結構是 數據類型 : 數據
數據類型以及后面跟隨的數據如下
b:THE_BOOLEAN; # 布爾值
i:THE_INTEGER; # 整數
d:THE_FLOAT; # 浮點
s:LENGTH_OF_STRING:"ACTUAL_STRING"; # 字符串
a:NUMBER_OF_ELEMENTS:{ELEMENTS} # 數組
O:LENGTH_OF_NAME:"CLASS_NAME":NUMBER_OF_PROPERTIES:{PROPERTIES} # 對象
請注意數據類型的符號:O與s
對象:名字長度:類名稱:屬性個數:{屬性} {字符串:字符串長度:實際的字符串}
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
PHP序列化與反序列化
serialize函數將其序列化,然后unserialize再將其反序列化,var_dump函數查看反序列化的變量以及屬性

調試代碼
<?php
class User{
public $username;
public $status;
}
$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';
$serialized_string = serialize($user);
$unserialized_data = unserialize($serialized_string);
var_dump($unserialized_data);
var_dump($unserialized_data->status);
?>
基于對象屬性或值的注入類漏洞
前置知識告訴我們,原來有一個序列化函數,從攻擊者可控的地方將其序列化為中間格式,然后再反序列化。如同所有注入類的漏洞一樣,我們無法想象服務器究竟用了什么函數,方法,屬性或者配置是否存在缺陷等。因此可控地方的payload產生的情況具體例子具體討論。
因為在前置知識中,我們明顯的發現序列化之后的字符串是人類可讀的。因此,我們可以將not admin修改為admin的邏輯漏洞型,以至于和其他的修改包沒有任何差距性。又或者將其他注入類payload置于屬性名或者值中。
一切都因為PHP的序列化是人類可讀的。但遺憾的是,并非所有的語言都是這種人類可讀的。因此,我們需要在最初就賦值(準備好),然后再使用語言附帶的序列化函數生成這種序列化的中間體來發送payload(bp抓到的包,人類不可讀,無從修改與放置payload)
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
基于反序列化引擎的漏洞
PHP對象的創建與銷毀
如果對象使用了PHP魔術方法,這些方法將具有魔術屬性,它們有的會在某些執行點或滿足某些條件時自動運行。
其中兩個神奇的方法是:__wakeup()和__destruct()
官方手冊給出了反序列化函數的__wakeup() 解釋,如果是對象則會自動調用,這是它的執行點,觸發點。

wakeup() 高頻出現的緣由
unserialize() 在還原時遇到對象就會執行 __wakeup() 方法并執行其中的代碼。 __wakeup() 方法通常用于重建對象可能擁有的任何資源,重新建立序列化期間丟失的任何數據庫連接,以及執行其他重新初始化任務。它在 PHP 對象注入攻擊期間通常很有用,因為它為服務器的數據庫或程序中的其他函數提供了一個方便的入口點。
析構函數__destruct():沒人用這個對象時就會調用析構函數來銷毀對象。
這也算是具備一個銷毀,刪除的功能點了。攻擊者可能通過控制傳遞給這些函數的輸入來破壞文件系統的完整性。
反序列化的RCE
當您控制傳遞給 unserialize() 的序列化對象時,您就控制了所創建對象的屬性。如果在這些可控的基礎之上您還可以控制傳遞給自動執行的方法(如__wakeup()或__destruct())的值。就有可能實現 RCE。
序列化對象屬性--> 自動執行的方法(魔術方法等引擎)--> 根據具體函數等功能點看看是否可控,是否能RCE。
代碼示例
參考資料:
https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
比如下面的對象屬性hook,就有一個危險函數eval() 。如果hook可控,其值就傳遞到__wakeup()中的eval()中,形成了上述的反序列化的RCE條件。
class Example2
{
private $hook;
function __construct(){
// some PHP code...
}
function __wakeup(){ # 反序列化時遇到對象自動執行
if (isset($this->hook)) eval($this->hook); # eval危險方法
}
}
// some PHP code...
$user_data = unserialize($_COOKIE['data']);

要讓hook可控,就需要將cookie的data設置為序列化的Example2 對象
將 hook 屬性設置為您想要執行的任何 PHP 代碼。將payload賦值給hook。
class Example2
{
private $hook = "phpinfo();"; # 基于對eval()的理解,直接將payload賦值給可控屬性
}
print urlencode(serialize(new Example2));
1.序列化的 Example2 對象作為數據 cookie 傳遞到程序中,而不是特殊語義符號,因此需要對數據進行url編碼urlencode()。
2.程序對數據 cookie 調用 unserialize()。
3.因為數據 cookie 是一個序列化的 Example2 對象,所以 unserialize() 會實例化一個新的 Example2 對象。
4.unserialize() 函數看到 Example2 類已經實現,所以調用了__wakeup()。
5.__wakeup() 函數查找對象的 $hook 屬性,如果它不為 NULL,則運行 eval($hook)。
6.$hook 屬性不是 NULL,因為它被賦值為 phpinfo();,所以 eval("phpinfo();") 被運行。
讓我們再回頭看看這個總結:序列化對象屬性--> 自動執行的方法(魔術方法等引擎)--> 根據具體函數等功能點看看是否可控,是否能RCE。
使用其他魔術方法
__toString()僅當對象被視為字符串時才調用方法
當調用未定義的方法時,程序會調用__call()方法。例如,對 $object->undefined($args) 的調用將變成 $object->__call('undefined', $args)。
以上提及的四種魔術方法是出場率最高的,如果遇到其他需求,可翻翻官方手冊
https://www.php.net/manual/en/language.oop5.magic.php
使用POP鏈或稱為“小工具”鏈
當我們使用以上提及的魔術方法中沒有那么直接的危險的eval()出現時,就形成了一種現象:這些屬性確實可控,但幾個方法又沒有可執行函數。
那么我能不能用這些可控的屬性拼拼湊湊的將它們形成gadgets(小玩具)串起來變成POP鏈(小工具)鏈?【太多的術語都在表達同一個意思了。。】
總之:所有的情況都在這里,不是直接利用就是間接的拼拼湊湊來利用。您可能會遇到非常多的叫法:比如小工具,鏈,POP,gadget,chain甚至利用工具ysoserial等全部都可以理解為就是一個東西都包含在這兩種情況里。
這些小工具憑借的是:從代碼庫中借用的代碼片段。POP 鏈使用魔法方法作為他們的初始小工具。然后攻擊者可以使用這些方法來調用其他小工具。因為涉及代碼庫,那就會隨著時間的變遷出現以前版本代碼庫中的可行變為無效,需要結合如今的代碼庫進行修正與比對。
實例理解:以下代碼為代碼片段,但是沒有形成閉環。無法調用CodeSnippet類中的危險方法evaluate()
https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
class Example
{
private $obj;
function __construct()
{
// some PHP code...
}
function __wakeup()
{
if (isset($this->obj)) return $this->obj->evaluate();
}
}
class CodeSnippet
{
private $code;
function evaluate() # 無法調用CodeSnippet類中的危險方法evaluate()
{
eval($this->code);
}
}
// some PHP code...
$user_data = unserialize($_POST['data']);
// some PHP code...
我們想從以下流程讓RCE變得實際可行。因為危險方法執行的變量是code,所以payload最終需要交付給$code

由于最后一行包含不安全的反序列化漏洞,攻擊者可以使用以下代碼生成序列化對象:
class CodeSnippet
{
private $code = "phpinfo();";
}
class Example
{
private $obj;
function __construct()
{
$this->obj = new CodeSnippet; # 生成CodeSnippet類,讓危險方法變得可以調用
}
}
print urlencode(serialize(new Example));
由此可見:POP 鏈通過鏈接和重用應用程序代碼庫中的代碼來實現 RCE。
它的利用思維與二進制的ROP同源:因為存在可執行空間保護(數據執行防護),使得傳統的緩沖區溢出不切實際,它將用戶可寫內存區域的可執行權限割掉了,無法執行是保護的關鍵所在。為了繞過這個保護,ROP操作的是返回地址,就像上述的POP鏈一樣,操作同名的存在于內存(代碼庫)里面的“類”,將危險函數,指令串起來。
https://en.維科.org/wiki/Return-oriented_programming
JAVA前置知識
java 在線版編譯器
https://www.tutorialspoint.com/compile_java_online.php
在java環境中有許多應用程序處理序列化對象
java序列化與反序列化
讀取就是還原的過程,肯定是反序列化操作函數 readObject()

要使 Java 對象可序列化,使用 java.io.Serializable 接口。這些類還實現了特殊方法 writeObject() 和 readObject(),以分別處理該類對象的序列化和反序列化。讓我們看一個例子以下代碼在名為 SerializeTest.java 的文件中:
import java.io.ObjectInputStream; # 對象輸入輸出
import java.io.ObjectOutputStream;
import java.io.FileInputStream; # 文件輸入輸出
import java.io.FileOutputStream;
import java.io.Serializable; # 可序列化
import java.io.IOException; # IO 異常
class User implements Serializable{ # 定義 User類可序列化(解鎖序列化和反序列化操作)
public String username; # 屬性 username
}
public class SerializeTest{ # 文件名通常與公開類名一致
public static void main(String args[]) throws Exception{
User newUser = new User(); # 實例化
newUser.username = "vickie"; # 賦值
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(newUser); # 序列化并寫入文件
os.close();
FileInputStream is = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(is);
User storedUser = (User)ois.readObject(); # 讀取文件并反序列化
System.out.println(storedUser.username);
ois.close();
}
}
編譯與執行

在java應用程序中,HTTP頭,參數,cookie都可以被用于序列化對象
Java 序列化對象不像 PHP 序列化字符串那樣的人類可讀但還算是有跡可尋。
特征:以十六進制的 AC ED 00 05 或 base64 的 rO0 開頭。(您可能會在參數或者cookie的地方看見它們的存在)
特征:HTTP Content-Type 頭設置為 application/x-java-serialized-object。
注意
由于 Java 序列化對象包含許多特殊字符,因此在傳輸之前對它們進行編碼是很常見的,因此還要注意這些簽名的不同編碼版本。前文說過,各語言的不同版本與棄用。
因為人類不可讀,您也不可能直接修改十六進制。往往是先從源碼放置payload,再將其序列化為中間態,最后發送給服務器。
利用思路還是基于對象屬性與值的修改或者通過POP鏈的思路嘗試創建任何類的對象來構造RCE。
JAVA版本的POP鏈
這與PHP的POP鏈沒有任何本質上的區別。查找和鏈接小工具以制定漏洞利用可能非常耗時,因為您也無法提前就知道具體的應用加載了哪些庫,庫里面哪些類可用,只能用已知來枚舉未知。
流行庫中的小工具創建漏洞利用鏈發揮了巨大的作用。
使用流行的,常用的這些命名空間來枚舉未知。總不能現場直接給它挖個0day出來吧。
例如 Apache Commons-Collections、Spring Framework、Apache Groovy 和 Apache Commons FileUpload
自動化利用工具Ysoserial (理解上類似于sqlmap)
Ysoserial (https://github.com/frohoff/ysoserial/) ,使用常見的 Java 庫中發現的一組小工具鏈來生成payload
$ java -jar ysoserial.jar gadget_chain command_to_execute // 語法格式 $ java -jar ysoserial.jar CommonsCollections1 calc.exe // 實例:彈計算器
很明顯:在使用工具時,它的第一個參數確切的指定了是哪個庫,您在不知道的情況下甚至可以全部懟一遍所有可選的庫。。但看得明白想得清楚才是正路,盡量去識別出目標應用程序使用了哪些易受攻擊的庫。偵察與觀察很重要。
參考資料 https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet/
防御
防御反序列化漏洞很困難。根據所使用的編程語言、庫和序列化格式保護應用程序免受這些漏洞影響的最佳方法有很大差異 不存在一刀切的解決方案。