PHP框架反序列化漏洞:从原理到实战深度剖析
前言:为什么是反序列化?
在PHP安全领域,反序列化漏洞(亦称PHP对象注入)被誉为“核武器”级别的漏洞。与SQL注入、XSS等传统漏洞不同,反序列化漏洞往往能直接导致远程代码执行(RCE),且利用手法极其精巧。而在现代Web开发中,几乎99%的项目都基于框架构建,这使得“PHP框架反序列化”成为了安全研究员和红队人员必须攻克的堡垒。
本文将围绕以下核心展开:
底层原理:PHP序列化机制与魔术方法。
POP链:面向属性编程的核心思想。
主流框架深度剖析:ThinkPHP、Laravel、Symfony的经典链分析与挖掘。
高级利用技巧:原生类利用、GC绕过、字符串逃逸。
防御与修复:反序列化入口的识别与加固。
第一部分:PHP反序列化基础(约3000字)
1.1 序列化与反序列化
序列化是将变量(对象、数组等)转换为可存储或传输的字符串格式的过程;反序列化则是将字符串还原为PHP变量。
序列化格式示例:
php
$user = new User(); $user->name = "admin"; $user->isAdmin = true; echo serialize($user); // 输出: O:4:"User":2:{s:4:"name";s:5:"admin";s:7:"isAdmin";b:1;}格式解析:
O:4:"User":2:对象(Object),类名长度4,类名User,属性数量2。{...}:属性列表。s:4:"name":字符串类型,长度4,值name。s:5:"admin":对应的值。
关键点:当unserialize()处理外部可控的字符串时,如果字符串被恶意构造,则可能触发意料之外的对象方法执行。
1.2 魔术方法:漏洞的入口点
魔术方法是反序列化漏洞利用的基石。当反序列化过程中满足特定条件时,PHP会自动调用这些方法。
| 魔术方法 | 触发时机 | 在链中的作用 |
|---|---|---|
__wakeup() | 反序列化恢复对象时立即调用 | 最常见的入口点,用于初始化对象。 |
__destruct() | 对象被销毁时(脚本结束或unset) | 链的终点,通常在此执行危险操作。 |
__toString() | 对象被当作字符串使用时 | 常用于从简单对象跳转到复杂对象。 |
__call() | 调用对象中不可访问的方法时 | 用于代理方法调用,绕过限制。 |
__get() | 读取不可访问的属性时 | 用于属性劫持。 |
__set() | 给不可访问的属性赋值时 | 用于属性覆盖。 |
__invoke() | 将对象作为函数调用时 | 常用于执行回调。 |
示例:最简单的利用
php
class Evil { public $cmd; function __destruct() { system($this->cmd); } } unserialize($_GET['data']); // 攻击者传入: O:4:"Evil":1:{s:3:"cmd";s:2:"id";}1.3 面向属性编程
POP(Property-Oriented Programming)是反序列化利用的核心方法论。它不关注代码逻辑的正常执行流,而是:
寻找起点:魔术方法(如
__destruct)。构建链条:通过控制对象的属性,让起点调用其他类的敏感方法,形成一条从入口到危险函数(如
eval,system,file_put_contents)的调用链。利用现有代码:完全依赖目标应用已存在的类和函数,无需注入新代码。
POP链的本质:利用程序中已有的“Gadget”(小工具),通过属性操控将它们串联起来。
第二部分:常见POP链构造模式(约2000字)
2.1 简单链:__destruct->eval
php
class A { public $b; function __destruct() { $this->b->action(); } } class B { public $code; function action() { eval($this->code); } } // 链: A::__destruct -> B::action -> eval2.2 利用__toString跳转
当对象被用于字符串上下文(如echo $obj)时,__toString被触发。
php
class Log { public $obj; function __toString() { return $this->obj->read(); } } class FileReader { public $file; function read() { return file_get_contents($this->file); } } // 链: 某处 echo $log; -> Log::__toString -> FileReader::read -> 文件读取2.3 利用__call进行方法代理
如果类中不存在某方法,__call会被调用,可用于动态调用任意函数。
php
class Proxy { public $func; function __call($name, $args) { call_user_func_array($this->func, $args); } } // 实例: $proxy->anything() 实际调用 call_user_func($this->func, ...)2.4 利用__get进行属性访问
php
class LazyLoader { public $class; function __get($key) { return new $this->class(); } }2.5 数组与ArrayAccess
实现了ArrayAccess接口的类,允许像数组一样访问对象,常与__destruct中的数组遍历结合。
第三部分:主流框架反序列化深度剖析(约8000字)
3.1 ThinkPHP v5.x 反序列化链分析
ThinkPHP 5.x 是反序列化漏洞的“重灾区”。其利用链主要利用了ORM(对象关系映射)和数据库操作的特性。
3.1.1 经典链:Windows->Pivot->Model->Db-> RCE
核心入口:think\process\pipes\Windows类的__destruct方法。
php
// thinkphp/library/think/process/pipes/Windows.php public function __destruct() { $this->close(); // 调用 close $this->removeFiles(); // 调用 removeFiles }关键点:removeFiles()方法中存在file_exists调用,如果传入的$file是一个对象,则会触发该对象的__toString方法。
php
private function removeFiles() { foreach ($this->files as $file) { if (file_exists($file)) { // 触发 __toString @unlink($file); } } }链的延伸:
__toString触发后,寻找可利用的__toString方法,例如think\model\concern\Conversion中的__toString->toJson->toArray。toArray中会遍历属性,并可能调用模型的获取器(Getter)。通过控制模型属性,最终进入数据库查询构建器
think\db\Query的__call或__callStatic,利用call_user_func_array执行任意函数。
最终Payload构造思路:
构建一个
Windows对象,使其$files属性指向一个Model对象。控制
Model对象的数据表名、查询条件等,使得查询构建器中能调用call_user_func(['某个类', '某个方法'], '参数'),如call_user_func('system', 'id')。
3.1.2 利用__include_file实现文件包含
ThinkPHP 5.0.0-5.0.23 版本存在反序列化导致任意文件包含的链,最终利用think\View::display或think\Loader::__include_file包含恶意文件(配合phar伪协议)。
3.2 Laravel 反序列化链分析
Laravel 因其高度的抽象和组件化,反序列化链通常较长,且需要绕过多层限制。
3.2.1 核心组件:Illuminate\Broadcasting\PendingBroadcast
Laravel 5.x-8.x 的经典反序列化链入口通常是Illuminate\Broadcasting\PendingBroadcast的__destruct。
php
// vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php public function __destruct() { $this->broadcast(); // 调用 broadcast 方法 }broadcast方法中:
php
public function broadcast() { $this->events->dispatch(...); // 触发事件分发 }如果$this->events可控,可控制其dispatch方法的行为。
常用利用点:
Illuminate\Events\Dispatcher的dispatch方法最终会调用call_user_func_array。通过控制
$this->events为Illuminate\Events\Dispatcher,并控制其$listeners属性,使得dispatch执行恶意回调。
链的延伸:
利用
Faker\Generator的__call或__get作为中间跳板。利用
Illuminate\Validation\Validator的__toString进行文件读取。
3.2.2 从__destruct到 RCE 的完整路径
PendingBroadcast::__destructDispatcher::dispatch(事件分发)Dispatcher::makeListenercall_user_func执行回调,回调可以是system,或是一个对象的__invoke方法。若
__invoke存在(如Mockery\Loader\EvalLoader),最终可执行代码。
3.2.3 Laravel 的 Guard 机制绕过
Laravel 5.8+ 引入了unserialize时的类型检查,通过$allowedClasses限制了可反序列化的类。攻击者需要通过原生类(如Error或Exception)或已存在的白名单类绕过。
3.3 Symfony 反序列化链分析
Symfony 组件广泛使用,其反序列化链常出现在symfony/serializer或symfony/validator中。
3.3.1 利用Symfony\Component\Validator\ObjectInitializer链
Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory中存在__destruct调用$this->loader->loadClassMetadata,结合Symfony\Component\Validator\Mapping\Loader\YamlFileLoader可实现任意文件读取。
3.3.2 利用Symfony\Component\Cache的 RCE 链
Symfony Cache 组件中,PdoAdapter等类在反序列化时可能触发数据库连接,结合 PDO 的本地文件读取或代码执行(如MySQL的SELECT ... INTO OUTFILE)可实现攻击。
3.3.3 结合Error与Exception的原生类利用
这是现代PHP反序列化的核心技巧。Error类和Exception类在反序列化时拥有特殊的__toString方法,会输出堆栈跟踪,其中可能包含可控的文件名或内容,结合file_exists或include可实现 PHAR 反序列化。
第四部分:高级利用技巧(约3000字)
4.1 原生类利用
PHP 内置类在反序列化中扮演着重要角色。
| 原生类 | 利用方式 |
|---|---|
Error/Exception | __toString输出可控内容,触发PHAR反序列化;利用堆栈跟踪绕过某些过滤。 |
SoapClient | 存在__call方法,可发起SSRF(服务器端请求伪造)。 |
SimpleXMLElement | 存在__toString,结合XXE(XML外部实体注入)。 |
GlobIterator | 存在__toString,可列举目录文件。 |
SplFileObject | 读取文件。 |
ZipArchive | 写入或解压文件。 |
示例:利用Error触发 PHAR 反序列化
php
$e = new Error("<payload>", 0); $e->file = "phar://path/to/file.jpg"; // 当 file_exists($e) 或类似操作触发 __toString 时,会尝试读取 phar 文件,触发反序列化4.2 字符串逃逸
当反序列化过程涉及字符串替换或过滤时,可能导致序列化字符串的长度与实际内容不匹配,从而注入新的属性。
场景:程序将用户输入进行过滤后反序列化。
原始序列化串:O:1:"A":1:{s:4:"name";s:6:"hacker";}
如果程序将hacker替换为hacker_clean(长度变长),但没有修正前面的长度标识s:6,就会导致反序列化失败或解析混乱。
利用思路:构造恶意字符串,使得替换后的字符串能够“吞掉”后续内容,并注入新的属性。
4.3 GC(垃圾回收)绕过
PHP 在销毁对象时,如果对象属性中存在循环引用,可能会触发额外的析构。攻击者可以利用__destruct中的unset或gc_collect_cycles来重新激活已经释放的对象,形成复杂的利用链。
4.4 绕过__wakeup的 CVE
在 PHP 5.6.25 及之前的版本中,存在 CVE-2016-7124:当序列化字符串中属性数量大于实际数量时,__wakeup不会被调用。虽然在高版本中已修复,但在老旧环境中依然存在。
4.5 Phar 反序列化
Phar(PHP Archive)是 PHP 的打包格式。当使用file_exists()、file_get_contents()、stat()等文件系统函数操作phar://伪协议时,PHP 会自动解析 Phar 文件的元数据并进行反序列化。
条件:
存在一个文件操作函数,参数可控(如
file_exists($_GET['file']))。能够上传一个构造好的 Phar 文件到服务器。
生成恶意 Phar:
php
class Evil { public $cmd; function __destruct() { system($this->cmd); } } $phar = new Phar('exploit.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $phar->setMetadata(new Evil()); // 序列化对象存入 metadata $phar->addFromString('test.txt', 'test'); $phar->stopBuffering();上传后,通过phar://触发:file_exists('phar://upload/exploit.phar')。
4.6 Session 反序列化
PHP 的会话机制支持多种序列化处理器,如php、php_binary、wddx等。如果配置不当或存在session.upload_progress功能,攻击者可以控制 session 内容,注入恶意序列化数据。
常见漏洞点:session_start()后,程序从 session 中取出数据,如果数据是用户可控的,且程序对数据进行了反序列化操作(如unserialize($_SESSION['data'])),则可触发。
第五部分:挖掘与审计方法论(约2000字)
5.1 寻找反序列化入口
在PHP框架中,反序列化入口通常位于:
unserialize函数:直接接收用户输入(GET、POST、Cookie、php://input)。session机制:session_decode或自定义处理器。phar://文件操作:file_exists,file_get_contents,fopen,stat等。serialize函数的存储:数据库、缓存、文件中的序列化数据被取回后反序列化。第三方库:如
jms/serializer、symfony/serializer在反序列化时可能缺乏类型校验。
5.2 自动挖掘工具
PHPGGC:最知名的 PHP 反序列化 Payload 生成器,支持 ThinkPHP、Laravel、Symfony、CodeIgniter 等主流框架。
Rogue:自动化寻找 POP 链的工具。
静态分析:使用
grep或 AST 工具搜索魔术方法调用和危险函数。
5.3 手动审计流程
定位
__destruct和__wakeup:从框架的核心类库开始,搜索这些方法,分析是否存在危险操作(如call_user_func、文件操作、数据库操作)。追踪属性传递:从入口点开始,记录哪些属性是外部可控的,如何传递给后续方法。
构建链:尝试将不同类的方法串联起来,形成闭环。
验证可行性:使用
PHPGGC或手动编写测试代码,确认unserialize后是否执行了目标代码。
第六部分:防御与修复(约1500字)
6.1 输入验证与禁止反序列化
最直接的方式是:永远不要反序列化不可信的数据。如果必须反序列化,应采用以下措施:
使用
allowed_classes选项:PHP 7.0+ 的unserialize支持第二个参数['allowed_classes' => false]或['allowed_classes' => ['MyClass']],禁止实例化任意类。使用 JSON 替代:对于数据交换,使用
json_encode/json_decode代替序列化,因为 JSON 不包含类信息。签名验证:对序列化数据进行签名(HMAC),确保数据未被篡改。
6.2 框架层面的防御
Laravel:从 5.8 开始,在反序列化时使用
Illuminate\Foundation\PackageManifest等白名单机制,限制可被反序列化的类。Symfony:
symfony/security组件提供了TokenSerializer并限制了可反序列化的类型。ThinkPHP:官方在新版中移除了危险的反序列化入口,但开发者仍需注意自定义代码。
6.3 代码审计最佳实践
禁止在
__destruct或__wakeup中调用危险函数:避免在对象销毁时执行eval、system、call_user_func等。避免使用
__call和__get进行无限制的动态调用:如果必须使用,严格控制参数来源。升级 PHP 版本:高版本 PHP(7.x, 8.x)修复了大量反序列化相关漏洞(如 CVE-2016-7124、属性类型混淆等)。
6.4 监控与检测
在 WAF(Web应用防火墙)层,检测
unserialize的输入是否包含对象标记O:或C:,并分析其长度和内容。监控
phar://协议的访问,尤其是上传目录中的 Phar 文件。
第七部分:实战案例模拟(约2000字)
7.1 案例一:ThinkPHP 5.0.15 反序列化 RCE
环境:某 CMS 基于 ThinkPHP 5.0.15,存在一处反序列化入口:
php
$data = unserialize(base64_decode($_GET['data']));
利用步骤:
使用 PHPGGC 生成针对 ThinkPHP 5.0.x 的 payload:
bash
phpggc ThinkPHP/RCE2 system id --phar
获取生成的序列化字符串(base64 编码)。
发送请求:
/?data=base64_encoded_payload。服务器返回
uid=33(www-data) ...,证明 RCE 成功。
Payload 原理:利用Windows类的__destruct->removeFiles触发__toString,进入Model的__toString->toArray,最终在数据库查询中执行call_user_func('system', 'id')。
7.2 案例二:Laravel 5.7 反序列化 + Phar 文件上传
环境:某 Laravel 应用允许上传图片,但未校验文件内容。存在一处file_exists($_POST['file_path'])。
利用步骤:
生成恶意 Phar 文件,内容为合法图片头
GIF89a拼接 Phar stub。上传图片
exploit.gif。构造请求:
file_path=phar://./storage/app/public/exploit.gif。触发反序列化,执行
__destruct中的恶意代码。
7.3 案例三:原生类 SSRF 结合内网 Redis
环境:某应用反序列化时未限制类,允许SoapClient实例化。
利用步骤:
构造
SoapClient对象,指定 WSDL 为内网 Redis 地址http://127.0.0.1:6379/,并设置user_agent为 Redis 命令(如CONFIG SET dir /var/www/html)。反序列化后,
SoapClient在发起请求时会发送 HTTP 请求,但通过 CRLF 注入可转化为 Redis 命令。成功写入 Webshell 或反弹 Shell。
第八部分:总结与未来趋势(约500字)
PHP 反序列化漏洞自 PHP 4 时代就已存在,但在现代框架中,其利用方式已经从简单的__destruct+system演变为复杂的多步链式调用。随着 PHP 8.x 的普及,类型安全和JIT虽然提升了性能,但也引入了新的属性类型绕过的可能性。同时,Composer 生态的复杂性使得 POP 链的挖掘更加依赖自动化工具。
未来趋势:
属性类型严格化:PHP 8 的构造函数属性提升和联合类型,使得传统的属性覆盖变得更困难,但同时也引入了新的类型混淆漏洞。
Fiber 与协程:异步编程的普及可能带来新的生命周期漏洞。
供应链攻击:针对 Composer 包的恶意代码注入,结合反序列化后门,将成为更隐蔽的攻击手段。
作为安全从业者,深入理解 PHP 底层 Zend 引擎的对象存储机制,熟悉主流框架的架构设计,是发现和防御此类漏洞的根本途径。
附录:常用工具与资源
| 工具/资源 | 用途 |
|---|---|
| PHPGGC | 生成主流框架的反序列化 Payload。 |
| Burp Suite | 抓包、重放、测试反序列化入口。 |
| PHP_CodeSniffer | 检测代码中不安全的unserialize使用。 |
| Seay源代码审计系统 | 辅助审计 PHP 项目。 |
| PHP Manual: Object Serialization | 官方文档,基础必读。 |
| OWASP PHP Security Cheat Sheet | 防御指南。 |
