FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1

渗透技巧 7个月前 admin
116 0 0

The Symfony doctrine/doctrine-bundle package is one of the most common bundles installed along Symfony applications. At the time we are releasing this blogpost, it has been downloaded 144 million times, making it an interesting target for unserialize exploitation. If you want to improve your knowledge about PHP unserialize exploitation and see why weak typed languages are considered less secure, this blogpost is for you.Symfony软件包是Symfony doctrine/doctrine-bundle 应用程序上安装的最常见的捆绑包之一。在我们发布这篇博文时,它已被下载了 1.44 亿次,使其成为反序列化利用的有趣目标。如果您想提高对 PHP 反序列化利用的了解,并了解为什么弱类型语言被认为不太安全,这篇博文适合您。

The first part of this article aims to show a full methodology of POP chain research, it details the full code analysis methodology used to identify a valid vulnerable path. The second part of it will be focused on the full build of a valid POP chain via a basic trial and error logic based on the code analyzed on this section.本文的第一部分旨在展示POP链研究的完整方法,它详细介绍了用于识别有效漏洞路径的完整代码分析方法。它的第二部分将侧重于通过基于本节分析的代码的基本试错逻辑来完整构建有效的 POP 链。

 TARGETING SYMFONY INDIRECTLY间接定位SYMFONY

As stated in our blogpost, finding POP chain in the main Symfony framework seems quite difficult due to its minimalism. Many basic features only come with extra dependencies such as Doctrine which is used as its ORM (Object Relation Mapping). This ORM is also one of the most commonly used along many other PHP projects : Drupal, Laravel, PrestaShop, etc. It is used to manage and abstract database access from the application.正如我们的博客文章中所述,由于其极简主义,在主要的Symfony框架中找到POP链似乎相当困难。许多基本功能仅带有额外的依赖项,例如用作其ORM(对象关系映射)的Doctrine。这个ORM也是许多其他PHP项目中最常用的项目之一:Drupal,Laravel,PrestaShop等。它用于管理和抽象应用程序的数据库访问。

To make Doctrine and Symfony compatible, the doctrine-bundle was created since Symfony version 2.1 if we believe the first paragraph of the first README released on the project.为了使Doctrine和Symfony兼容,如果我们相信该项目上发布的第一个自述文件的第一段, doctrine-bundle 那么它是从Symfony版本2.1开始创建的。

Because Symfony 2 does not want to force or suggest a specific persistence solutions on the users this bundle was removed from the core of the Symfony 2 framework. Doctrine2 will still be a major player in the Symfony world and the bundle is maintained by developers in the Doctrine and Symfony communities.因为Symfony 2不想强迫或建议用户特定的持久性解决方案,所以这个捆绑包从Symfony 2框架的核心中删除了。Doctrine2仍将是Symfony世界的主要参与者,该捆绑包由Doctrine和Symfony社区的开发人员维护。

IMPORTANT: This bundle is developed for Symfony 2.1 and up. For Symfony 2.0 applications the DoctrineBundle重要提示:此捆绑包是为Symfony 2.1及更高版本开发的。对于Symfony 2.0应用程序,DoctrineBundle
is still shipped with the core Symfony repository.仍然随核心 Symfony 存储库一起提供。

METHODOLOGY: IDENTIFYING INTERESTING ENTRYPOINTS方法:确定感兴趣的切入点

Finding POP chains can be time-consuming, because the scope on which they are based is huge when digging in PHP dependencies. This is why it is important to keep in mind where to start, and see how to jump from one object to another, step by step.查找 POP 链可能很耗时,因为在挖掘 PHP 依赖项时,它们所基于的范围很大。这就是为什么重要的是要记住从哪里开始,并逐步了解如何从一个对象跳到另一个对象。

The full methodology and logic followed to find them and make them work together is described in the following sections.以下各节介绍了查找它们并使它们协同工作所遵循的完整方法和逻辑。

PHP unserialization, finding what you want via static analysisPHP反序列化,通过静态分析找到你想要的东西

First, it is mandatory to find a __wakeup__unserialize or a __destruct method implemented in the project dependencies. The __toString method can also be called, but the object unserialized has to be called inside a function such as print or echo after its unserialization, which is unlikely to happen.首先,必须找到在 __wakeup 项目依赖项中实现的 或 __unserialize __destruct 方法。也可以调用该方法 __toString ,但未序列化的对象必须在函数内部调用,例如 print 在其反序列化之后或 echo 之后调用,这不太可能发生。

Without going too deep in the details (more details on use cases and tricks can be found on payload all the things), when a serialized string is put through unserialization, the __wakeup method will be called first (or __unserialize instead). The object will then eventually get destroyed, therefore calling its __destruct method if it is defined.在不深入细节的情况下(有关用例和技巧的更多详细信息可以在有效负载中找到所有内容),当序列化字符串通过反序列化时, __wakeup 将首先调用该方法(或 __unserialize 改为调用)。然后,对象最终将被销毁,因此如果已定义,则调用其 __destruct 方法。

Sorting __wakeup, __unserialize and __destruct分拣__wakeup、__unserialize和__destruct

To make the assumptions, and the flow of this blogpost easier to follow, each step of the logic followed in order to identify the full chain will be presented with a figure. So the first target during this research is to sort __wakeup__unserialize or __destruct functions from the code base analyzed.为了使假设和本博文的流程更容易遵循,为了识别整个链而遵循的逻辑的每个步骤都将以数字呈现。因此,本研究的第一个目标是从 __wakeup 分析的代码库中排序或 __unserialize __destruct 函数。

FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1
First assumption while looking for a POP chain.寻找 POP 链时的第一个假设。

Grepping your classes 清理你的类

doctrine-bundle dependencies can be installed from composer. After that, a simple grep can do the trick : the doctrine/doctrine-bundle dependencies contain many possible entrypoints.doctrine-bundle 可以从 安装 composer 依赖项。之后,一个简单的 grep 可以做到这一点: doctrine/doctrine-bundle 依赖项包含许多可能的入口点。

$ composer require doctrine/doctrine-bundle
./composer.json has been created
Running composer update doctrine/doctrine-bundle
Loading composer repositories with package information
Updating dependencies
Lock file operations: 35 installs, 0 updates, 0 removals
  - Locking doctrine/cache (2.2.0)
  - Locking doctrine/dbal (3.5.3)
  - Locking doctrine/deprecations (v1.0.0)
  - Locking doctrine/doctrine-bundle (2.8.2
[...]
$ cd vendor
$ grep -Ri 'function __destruct'
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:    public function __destruct()
doctrine/dbal/src/Logging/Connection.php:    public function __destruct()
symfony/framework-bundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php:    public function __destruct()
symfony/dependency-injection/Loader/Configurator/ServiceConfigurator.php:    public function __destruct()
symfony/dependency-injection/Loader/Configurator/AbstractServiceConfigurator.php:    public function __destruct()
symfony/dependency-injection/Loader/Configurator/ServicesConfigurator.php:    public function __destruct()
symfony/dependency-injection/Loader/Configurator/PrototypeConfigurator.php:    public function __destruct()
symfony/cache/Adapter/TagAwareAdapter.php:    public function __destruct()
symfony/cache/Traits/AbstractAdapterTrait.php:    public function __destruct()
symfony/cache/Traits/FilesystemCommonTrait.php:    public function __destruct()
symfony/error-handler/BufferingLogger.php:    public function __destruct()
symfony/routing/Loader/Configurator/ImportConfigurator.php:    public function __destruct()
symfony/routing/Loader/Configurator/CollectionConfigurator.php:    public function __destruct()
symfony/http-kernel/DataCollector/DumpDataCollector.php:    public function __destruct()

Sorting the possible entrypoints对可能的入口点进行排序

On deeply hardened projects such as Symfony, a protection against unserialization is often set by throwing an error when the __wakeup function is called, since it is called before the __destruct function. As shown in the following result, this in depth hardening is set on many Symfony classes.在像Symfony这样的深度强化项目上,防止反序列化的保护通常是通过在调用函数时抛出错误来设置的,因为它是在 __wakeup 函数之前 __destruct 调用的。如下面的结果所示,这种深度强化是在许多Symfony类上设置的。

$ grep -hri 'function __wakeup' -A4 . 
    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

--
    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

--
[...]

Keep in mind that it is possible to sort the classes implementing __destruct but not containing the BadMethodCallException keyword:请记住,可以对实现 __destruct 但不包含 BadMethodCallException 关键字的类进行排序:

$ grep -rl '__destruct' | xargs grep -L BadMethodCallException
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php
doctrine/dbal/src/Logging/Connection.php
symfony/dependency-injection/Loader/Configurator/ServiceConfigurator.php
symfony/dependency-injection/Loader/Configurator/AbstractServiceConfigurator.php
symfony/dependency-injection/Loader/Configurator/ServicesConfigurator.php
symfony/dependency-injection/Loader/Configurator/PrototypeConfigurator.php
symfony/var-dumper/Caster/ExceptionCaster.php
symfony/http-kernel/Tests/DataCollector/DumpDataCollectorTest.php

Luckily for us, the Doctrine\Common\Cache\Psr6\CacheAdapter class seems pretty promising! It was used as the default doctrine cache adapter along the doctrine/cache package until the Doctrine version 1.11.x (maintained since 2019). It was then deprecated, however, it was kept for backward compatibility, and even if the code should not be used anymore, doctrine/cache was kept for backward compatibility.对我们来说幸运的是,这 Doctrine\Common\Cache\Psr6\CacheAdapter 门课似乎很有前途!它被用作 doctrine/cache 软件包的默认原则缓存适配器,直到原则版本 1.11.x(自 2019 年以来一直维护)。然后它被弃用,但是,它被保留是为了向后兼容,即使不应该再使用代码, doctrine/cache 也是为了向后兼容而保留的。

<?php

namespace Doctrine\Common\Cache\Psr6;

final class CacheAdapter implements CacheItemPoolInterface
{
    [...]
    public function commit(): bool
    {
        [...]
    }

    public function __destruct() { 
        $this->commit(); 
    }
}

As we can see, the __destruct function is reachable and directly calls the commit function of the object, let’s see what can be done from this point.如我们所见,该 __destruct 函数是可访问的,并直接调用对象的 commit 函数,让我们看看从这一点可以做什么。

What about __call function definitions?__call函数定义呢?

Another path explored during this research was to analyze classes defining a __call function.在这项研究中探索的另一个途径是分析定义函数的 __call 类。

The PHP documentation explains that:PHP 文档解释说:

__call() is triggered when invoking inaccessible methods in an object context__call() 在对象上下文中调用无法访问的方法时触发

$ grep -Ri 'function __call' .
./doctrine/dbal/src/Schema/Comparator.php:    public function __call(string $method, array $args): SchemaDiff
./doctrine/dbal/src/Schema/Comparator.php:    public static function __callStatic(string $method, array $args): SchemaDiff
./symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:    public function __call(string $method, array $arguments): mixed
./symfony/dependency-injection/Loader/Configurator/EnvConfigurator.php:    public function __call(string $name, array $arguments): static
./symfony/dependency-injection/Loader/Configurator/AbstractConfigurator.php:    public function __call(string $method, array $args)
./symfony/cache/Traits/RedisClusterNodeProxy.php:    public function __call(string $method, array $args)

While it will not be described in this blogpost since it did not pay out here, keep in mind that it should also be covered if you look for POP chains since it can be reached in most cases.虽然它不会在本博文中描述,因为它没有在这里支付,但请记住,如果您寻找 POP 链,它也应该被覆盖,因为在大多数情况下都可以访问它。

Jumping from controlled functions从受控功能跳转

FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1
PhpAdapter commit function reached via __destruct call.

The commit function we reached is normally used to update the deferred items cached by the Doctrine cache. Basically, it deletes items which reached expiry and saves all the others in the cache definition.

The phpdoc (@var lines) on the class attributes suggests that $cache should implement the Cache interface, and $deferredItems should be an array of either CacheItem or TypedCacheItem. This is only for documentation purposes and does not enforce strong typing, meaning that any call to their methods can be hijacked because we can control which class will be implemented thanks to unserialization.类属性上的 phpdoc(@var行)建议应该 $cache 实现 Cache 接口,并且 $deferredItems 应该是 CacheItem or TypedCacheItem 的数组。这仅用于文档目的,不会强制强类型,这意味着对其方法的任何调用都可能被劫持,因为我们可以控制由于反序列化而实现哪个类。

From this point, 4 functions can be called from $this->cache or $this->deferredItems objects. Let’s have a look to each object implementing them to see if interesting code can be reached.从这一点开始,可以从 $this->cache or $this->deferredItems 对象调用 4 个函数。让我们看一下实现它们的每个对象,看看是否可以访问有趣的代码。

<?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;
        }

        $now         = microtime(true);
        $itemsCount  = 0;
        $byLifetime  = [];
        $expiredKeys = [];

        foreach ($this->deferredItems as $key => $item) {
            $lifetime = ($item->getExpiry() ?? $now) - $now; // [1]

            if ($lifetime < 0) {
                $expiredKeys[] = $key;

                continue;
            }

            ++$itemsCount;
            $byLifetime[(int) $lifetime][$key] = $item->get(); // [2]
        }

        $this->deferredItems = [];

        switch (count($expiredKeys)) {
            case 0:
                break;
            case 1:
                $this->cache->delete(current($expiredKeys)); // [4]
                break;
            default:
                $this->doDeleteMultiple($expiredKeys);
                break;
        }

        if ($itemsCount === 1) {
            return $this->cache->save($key, $item->get(), (int) $lifetime); // [3]
        }

        $success = true;
        foreach ($byLifetime as $lifetime => $values) {
            $success = $this->doSaveMultiple($values, $lifetime) && $success;
        }

        return $success;
    }

    public function __destruct() { 
        $this->commit(); 
    }
}
  • [1] The getExpiry() function from any object[1] 来自任何对象的 getExpiry() 函数

This function is not a good match, it does not reach interesting code and is only defined in 2 classes:这个函数不是一个很好的匹配,它没有达到有趣的代码,只在 2 个类中定义:

$ grep -ri 'function getexpiry' -A 3
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/TypedCacheItem.php:    public function getExpiry(): ?float
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/TypedCacheItem.php-    {
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/TypedCacheItem.php-        return $this->expiry;
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/TypedCacheItem.php-    }
--
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php:    public function getExpiry(): ?float
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php-    {
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php-        return $this->expiry;
doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php-    }
  • [2] The get() function from any object[2] 来自任何对象的 get() 函数

This function definition seems promising. It is defined at least in 53 files in the doctrine/doctrine-bundle dependencies.这个函数定义似乎很有希望。它至少在 doctrine/doctrine-bundle 依赖项中的 53 个文件中定义。

$ grep -ri 'function get(' | wc -l
53

However, the $item->getExpiry() code is reached before, and we saw that only 2 objects implements a getExpiry function. Both of them only return a value, which makes get calls unreachable as shown on the following code snippet.但是, $item->getExpiry() 之前已经访问了代码,我们看到只有 2 个对象实现了函数 getExpiry 。它们都只返回一个值,这使得 get 调用无法访问,如以下代码片段所示。

<?php

final class CacheAdapter implements CacheItemPoolInterface
{
    [...]
    public function commit(): bool
    {
    [...]
        foreach ($this->deferredItems as $key => $item) {
            $lifetime = ($item->getExpiry() ?? $now) - $now; // [1]

            if ($lifetime < 0) {
                $expiredKeys[] = $key;

                continue;
            }

            ++$itemsCount;
            $byLifetime[(int) $lifetime][$key] = $item->get(); // [2]
        }

    }

}
  •  [3] The save($param1, $param2, int $param3) function from any object[3] 来自任何对象的 save($param1, $param2, int $param3) 函数

save is a common function name and without surprise, many classes or traits are defining it.save 是一个常见的函数名称,毫不奇怪,许多类或特征都在定义它。

$ grep -ri 'function save('  .
./psr/cache/src/CacheItemPoolInterface.php:    public function save(CacheItemInterface $item): bool;
./doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php:    public function save(CacheItemInterface $item): bool
./doctrine/cache/lib/Doctrine/Common/Cache/Cache.php:    public function save($id, $data, $lifeTime = 0);
./doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php:    public function save($id, $data, $lifeTime = 0)
./symfony/http-foundation/Session/Storage/MockArraySessionStorage.php:    public function save()
./symfony/http-foundation/Session/Storage/SessionStorageInterface.php:    public function save();
./symfony/http-foundation/Session/Storage/NativeSessionStorage.php:    public function save()
./symfony/http-foundation/Session/Storage/MockFileSessionStorage.php:    public function save()
./symfony/http-foundation/Session/Session.php:    public function save()
./symfony/http-foundation/Session/SessionInterface.php:    public function save();
./symfony/cache/Adapter/ProxyAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Adapter/PhpArrayAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Adapter/TraceableAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Adapter/ChainAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Adapter/ArrayAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Adapter/NullAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Adapter/TagAwareAdapter.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Traits/RedisCluster6Proxy.php:    public function save($key_or_address): \RedisCluster|bool
./symfony/cache/Traits/AbstractAdapterTrait.php:    public function save(CacheItemInterface $item): bool
./symfony/cache/Traits/RedisCluster5Proxy.php:    public function save($key_or_address)
./symfony/cache/Traits/Redis6Proxy.php:    public function save(): \Redis|bool
./symfony/cache/Traits/Redis5Proxy.php:    public function save()
./symfony/http-kernel/HttpCache/Store.php:    private function save(string $key, string $data, bool $overwrite = true): bool

Many of these classes are useless for our purpose, but Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage can be used to write a file. This is one of the main targets for this POP chain. To reach its code, it is necessary to define one $item which has an expiration inferior to the current time. $key will be its first parameter.其中许多类对于我们的目的毫无用处,但 Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage 可用于编写文件。这是该POP链的主要目标之一。要达到其代码,有必要定义一个 $item 低于当前时间的代码 expiration 。 $key 将是它的第一个参数。

<?php

final class CacheAdapter implements CacheItemPoolInterface
{
    [...]
    public function commit(): bool
    {
    [...]
        foreach ($this->deferredItems as $key => $item) {
            $lifetime = ($item->getExpiry() ?? $now) - $now; // [1]

            if ($lifetime < 0) {
                $expiredKeys[] = $key;

                continue;
            }

            ++$itemsCount;
            $byLifetime[(int) $lifetime][$key] = $item->get(); // [2]
        }
    [...]
        if ($itemsCount === 1) {
            return $this->cache->save($key, $item->get(), (int) $lifetime); // [3]
        }

    }

}
  •  [4] The delete($param1) function from any object[4] 来自任何对象的 delete($param1) 函数

Unlike the save function, delete definitions are less common in PHP projects. However, it only makes it easier to look for them in all the files.与 save 函数不同, delete 定义在 PHP 项目中不太常见。但是,它只会使在所有文件中查找它们变得更加容易。

$ grep -ri 'function delete('  .
./doctrine/cache/lib/Doctrine/Common/Cache/Cache.php:    public function delete($id);
./doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php:    public function delete($id)
./doctrine/dbal/src/Query/QueryBuilder.php:    public function delete($delete = null, $alias = null)
./doctrine/dbal/src/Connection.php:    public function delete($table, array $criteria, array $types = [])
./symfony/cache-contracts/CacheTrait.php:    public function delete(string $key): bool
./symfony/cache-contracts/CacheInterface.php:    public function delete(string $key): bool;
./symfony/cache/Psr16Cache.php:    public function delete($key): bool
./symfony/cache/Adapter/TraceableAdapter.php:    public function delete(string $key): bool
./symfony/cache/Adapter/ArrayAdapter.php:    public function delete(string $key): bool
./symfony/cache/Adapter/NullAdapter.php:    public function delete(string $key): bool
./symfony/cache/Traits/Redis6Proxy.php:    public function delete($key, ...$other_keys): \Redis|false|int
./symfony/cache/Traits/Redis5Proxy.php:    public function delete($key, ...$other_keys)

From these classes, Symfony\Component\Cache\Adapter\PhpArrayAdapter can be used to arbitrary include a file. This is the final target of this POP chain.从这些类中, Symfony\Component\Cache\Adapter\PhpArrayAdapter 可用于任意 include 文件。这是这个POP链的最终目标。

To reach it, it is necessary to define one $item which has an expiration higher than the current time. $item will be its first parameter.要达到它,有必要定义一个 $item 高于 expiration 当前时间的时间。 $item 将是它的第一个参数。

<?php

final class CacheAdapter implements CacheItemPoolInterface
{
    [...]
    public function commit(): bool
    {
    [...]
        foreach ($this->deferredItems as $key => $item) {
            $lifetime = ($item->getExpiry() ?? $now) - $now; // [1]

            if ($lifetime < 0) {
                $expiredKeys[] = $key;

                continue;
            }

            ++$itemsCount;
            $byLifetime[(int) $lifetime][$key] = $item->get(); // [2]
        }

        switch (count($expiredKeys)) {
            case 0:
                break;
            case 1:
                $this->cache->delete(current($expiredKeys)); // [4]
                break;
            default:
                $this->doDeleteMultiple($expiredKeys);
                break;
        }
    [...]
    }
}

This chain is used to include a file from an arbitrary path.此链用于 include 任意路径中的文件。

First step, MockFileSessionStorage to get file write第一步,模拟文件会话存储以获取文件写入

Now that we know better where we can search, let’s dig more!现在我们更好地了解了可以搜索的位置,让我们挖掘更多!

The first step while looking for vulnerable code in PHP code would be to see if user supplied data is passed to dangerous functions such as systemevalincluderequireexecpopencall_user_funcfile_put_contents. There are many others, however, the main idea here is that since the potential vulnerable scope was refined to save() functions, it is now necessary to audit each reachable save functions from the analyzed dependencies to identify the POP chain.在 PHP 代码中查找易受攻击的代码时,第一步是查看用户提供的数据是否被传递给危险的函数,例如 system 、、 eval include require exec popen call_user_func file_put_contents 。然而,还有许多其他人,这里的主要思想是,由于潜在的易受攻击的范围已细化为 save() 函数,因此现在有必要从分析的依赖项中审核每个可访问的保存函数,以识别 POP 链。

As we can see, the only reachable and interesting function seems to be file_put_contents in the save function of the Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage class.正如我们所看到的,唯一可访问和有趣的函数似乎是在 Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage 类的 save 函数 file_put_contents 中。

$ grep -hri 'function save(' -A50 . | grep system
$ grep -hri 'function save(' -A50 . | grep eval
$ grep -hri 'function save(' -A50 . | grep include
$ grep -hri 'function save(' -A50 . | grep require
     * When versioning is enabled, clearing the cache is atomic and does not require listing existing keys to proceed,
     * but old keys may need garbage collection and extra round-trips to the back-end are required.
$ grep -hri 'function save(' -A50 . | grep exec
$ grep -hri 'function save(' -A50 . | grep popen
$ grep -hri 'function save(' -A50 . | grep call_user_func
[...]
$ grep -hri 'function save(' -A50 . | grep file_put_content
                file_put_contents($tmp, serialize($data));
$ grep -ri 'file_put_contents($tmp, serialize($data))' .
./symfony/http-foundation/Session/Storage/MockFileSessionStorage.php:                file_put_contents($tmp, serialize($data));
$ grep -i 'file_put_contents($tmp, serialize($data))' -B 21 -A 12 ./symfony/http-foundation/Session/Storage/MockFileSessionStorage.php
    public function save()
    {
        if (!$this->started) {
            throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.');
        }

        $data = $this->data;

        foreach ($this->bags as $bag) {
            if (empty($data[$key = $bag->getStorageKey()])) {
                unset($data[$key]);
            }
        }
        if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) {
            unset($data[$key]);
        }

        try {
            if ($data) {
                $path = $this->getFilePath();
                $tmp = $path.bin2hex(random_bytes(6));
                file_put_contents($tmp, serialize($data));
                rename($tmp, $path);
            } else {
                $this->destroy();
            }
        } finally {
            $this->data = $data;
        }

        // this is needed when the session object is re-used across multiple requests
        // in functional tests.
        $this->started = false;
    }

While looking promising at first, the extension of the generated file cannot be defined, which makes it less interesting.虽然一开始看起来很有希望,但无法定义生成的文件的扩展名,这使得它不那么有趣。

<?php

namespace Symfony\Component\HttpFoundation\Session\Storage;

class MockFileSessionStorage extends MockArraySessionStorage
{
    private string $savePath;

    public function save()
    {
[...]

        try {
            if ($data) {
                $path = $this->getFilePath();
                $tmp = $path.bin2hex(random_bytes(6));
                file_put_contents($tmp, serialize($data));
                rename($tmp, $path);
            } else {
                $this->destroy();
            }
        } finally {
            $this->data = $data;
        }
        $this->started = false;
    }
    private function getFilePath(): string
    {
        return $this->savePath.'/'.$this->id.'.mocksess';
    }

}

However, as we can see, we can control a serialized data injected in a file, which can be executed as PHP code if executed.但是,正如我们所看到的,我们可以控制注入到文件中的序列化数据,如果执行,可以作为PHP代码执行。

$ php -r "echo serialize('<?php phpinfo(); ?>');" > /tmp/test_serialize
$ php /tmp/test_serialize 
s:19:"phpinfo()
PHP Version => 8.1.22
[...]
questions about PHP licensing, please contact [email protected].

The $path = $this->getFilePath() code is used to define the path of the file written in the file_put_contents method.该 $path = $this->getFilePath() 代码用于定义 file_put_contents 在方法中写入的文件的路径。

<?php

namespace Symfony\Component\HttpFoundation\Session\Storage;

class MockFileSessionStorage extends MockArraySessionStorage
{
[...]
    private function getFilePath(): string
    {
        return $this->savePath.'/'.$this->id.'.mocksess';
    }

}

The .mocksess is suffixed to the file, preventing us from getting code execution by creating a .php file in the folder exposing sources to the user. However, getting an arbitrary file write is the only prerequisite needed before proceeding to the second step. The following schema wraps up this first element of the POP chain.

FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1
POP chain path to write any file with any content ending with the extension .mocksessPOP 链路径,用于写入任何内容以扩展名 .mocksess 结尾的文件

Second step, finding the path to file inclusion第二步,查找文件包含的路径

The same methodology can be applied to look for delete function calls.可以应用相同的方法来查找 delete 函数调用。

$ grep -hri 'function delete(' -A50 . | grep file_put_content | grep system
$ grep -hri 'function delete(' -A50 . | grep eval
[...]
$ grep -hri 'function delete(' -A50 . | grep include
$ grep -hri 'function delete(' -A50 . | grep require
$ grep -hri 'function delete(' -A50 . | grep exec
[...]
$ grep -hri 'function delete(' -A50 . | grep popen
$ grep -hri 'function delete(' -A50 . | grep call_user_func
[...]

After searching for many common dangerous functions, it was clear that there was no quick win in those. This then means we need to dive into all of them individually. Let’s start by looking for the good old weak typing trick used at the start of this POP chain, allowing us to call delete functions from other objects.在搜索了许多常见的危险功能后,很明显这些功能没有快速的胜利。这意味着我们需要单独深入研究所有这些。让我们首先寻找在这个 POP 链开始时使用的老式弱类型技巧,它允许我们从其他对象调用 delete 函数。

$ grep -hri 'function delete(' -A3 .
    public function delete($id);
--
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }
--    
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }
--
    public function delete($id)
    {
        return $this->doDelete($this->getNamespacedId($id));
    }
--
    public function delete($table, array $criteria, array $types = [])
    {
        if (count($criteria) === 0) {
            throw InvalidArgumentException::fromEmptyCriteria();
--
    public function delete($key): bool
    {
        try {
            return $this->pool->deleteItem($key);
[...]

$ grep -Ri 'return $this->pool->deleteItem($key);' .
./symfony/cache/Psr16Cache.php:            return $this->pool->deleteItem($key);

As we can see, the deleteItem function seems promising since it is called from many delete functions, let’s see what can be reached from it.如我们所见,该 deleteItem 函数似乎很有前途,因为它是从许多 delete 函数调用的,让我们看看可以从中达到什么。

From a call to the PhpArrayAdapter function deleteItem, it is possible to reach its initialize method which includes an arbitrary file. Since we already have a file write, we would be able to include it in order to get code execution.从对 PhpArrayAdapter 函数 deleteItem 的调用,可以到达包含 initialize 任意文件的方法。由于我们已经有文件写入,因此我们可以包含它以获得代码执行。

$ grep -hri 'function deleteItem(' -A6 .
    public function deleteItem(mixed $key): bool
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if (!isset($this->values)) {
            $this->initialize();
[...]

$ grep -Ri '            $this->initialize();' . 
./symfony/cache/Adapter/PhpArrayAdapter.php:            $this->initialize();
[...]
$ grep 'function initialize' -A10 ./symfony/cache/Adapter/PhpArrayAdapter.php
    private function initialize()
    {
        if (isset(self::$valuesCache[$this->file])) {
            $values = self::$valuesCache[$this->file];
        } elseif (!is_file($this->file)) {
            $this->keys = $this->values = [];

            return;
        } else {
            $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
        }

Unfortunately for us, it is not possible to use a PHP filter chain to get command execution from this path alone. This is due to the elseif (!is_file($this->file) condition, which verifies that the file is present on the file system, preventing any call to the php:// wrapper.对我们来说不幸的是,仅从此路径使用 PHP 过滤器链来获取命令执行是不可能的。这是由于 elseif (!is_file($this->file) 验证文件是否存在于文件系统上的条件,从而阻止对 php:// 包装器的任何调用。

To sum up, the goal is now to find a way to reach PhpArrayAdapter in order to reach its deleteItem function to put together the puzzle pieces.总而言之,现在的目标是找到一种方法来达到 PhpArrayAdapter 它 deleteItem 的功能,将拼图拼凑在一起。

FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1
File inclusion via the initialize function from PhpArrayAdapter.通过 PhpArrayAdapter 的初始化函数包含文件。

Getting rekt by PHP strong typing通过 PHP 强类型获取 rekt

Now that our plan is well-defined, let’s see how to reach PhpArrayAdapter from our previously discovered delete function.现在我们的计划已经明确定义,让我们看看如何从 PhpArrayAdapter 我们之前发现 delete 的函数到达。

$ grep -Ri 'return $this->pool->deleteItem($key);' .
./symfony/cache/Psr16Cache.php:            return $this->pool->deleteItem($key);

At first sight, the Psr16Cache class seems perfect to reach our target as we might be able to reach any other deleteItem function by defining its pool attribute as a PhpArrayAdapter object. However, as we said, while PHP is a weakly typed language, it is also possible to harden it by enforcing strong typing. Unfortunately for us, this is the case in the Psr16Cache class.乍一看,该 Psr16Cache 类似乎非常适合达到我们的目标,因为我们可以通过将其 pool 属性定义为 PhpArrayAdapter 对象来访问任何其他 deleteItem 函数。然而,正如我们所说,虽然PHP是一种弱类型语言,但也可以通过强制强类型来强化它。对我们来说不幸的是, Psr16Cache 课堂上就是这种情况。

cat ./symfony/cache/Psr16Cache.php
<?php

[...]
class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface
{
    use ProxyTrait;

    private ?\Closure $createCacheItem = null;
    private ?CacheItem $cacheItemPrototype = null;
    private static \Closure $packCacheItem;

    public function __construct(CacheItemPoolInterface $pool)
    {
        $this->pool = $pool;
}

The check that the parameter $pool is a CacheItemPoolInterface interface prevents us to use a PhpArrayAdapter class instead.检查参数 $pool 是否为 CacheItemPoolInterface 接口会阻止我们使用 PhpArrayAdapter 类。

PHP traits analysis PHP特征分析

Now that the most straight forward path has been invalidated, let’s see what options are left to us.现在最直接的路径已经失效,让我们看看留给我们哪些选项。

$ grep -Ri 'function delete(' .
./doctrine/cache/lib/Doctrine/Common/Cache/Cache.php:    public function delete($id);
./doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php:    public function delete($id)
./doctrine/dbal/src/Query/QueryBuilder.php:    public function delete($delete = null, $alias = null)
./doctrine/dbal/src/Connection.php:    public function delete($table, array $criteria, array $types = [])
./symfony/cache-contracts/CacheTrait.php:    public function delete(string $key): bool
[...]

In the objects defining the delete function, the CacheTrait trait seems promising. The PHP documentation defines a trait as a way to reuse code, which is basically a way to write a function or an attribute and to define them inside another class. All there is to do is to add it in the class via the use keyword.在定义函数的对象 delete 中, CacheTrait 特征似乎很有希望。PHP 文档将 trait 定义为重用代码的一种方式,这基本上是一种编写函数或属性并在另一个类中定义它们的方法。所要做的就是通过 use 关键字将其添加到类中。

$ cat ./symfony/cache-contracts/CacheTrait.php | grep 'function delete(' -A 3
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }

CacheTrait calls the deleteItem function of the object using it. If by chance our target, the PhpArrayAdapter class, uses the CacheTrait, we would then be able to call its deleteItem function and therefore reach the require function needed to get code execution.CacheTrait 调用 deleteItem 使用它的对象函数。如果我们的目标 The PhpArrayAdapter Class 偶然使用 ,那么我们将能够调用它的 deleteItem 函数 CacheTrait ,从而到达获取代码执行所需的 require 函数。

$ grep -Ri 'use CacheTrait' .
./symfony/cache/Traits/ContractsTrait.php:    use CacheTrait {
$ grep -Ri 'use ContractsTrait' .
./symfony/cache/Adapter/ProxyAdapter.php:    use ContractsTrait;
./symfony/cache/Adapter/PhpArrayAdapter.php:    use ContractsTrait;
[...]

Even if the PhpArrayAdapter class does not directly use the CacheTrait, it uses the ContractsTrait which uses it because traits can be nested.即使 PhpArrayAdapter 类不直接使用 CacheTrait ,它也使用 ContractsTrait how 使用它,因为特征可以嵌套。

Finally, reaching PhpArrayAdapter for the win最后,达到PhpArrayAdapter获胜

After a deeper investigation we finally discovered that the PhpArrayAdapter had already everything needed to reach its vulnerable initialize function. The CacheTrait deleteItem function is defined, allowing to call the initialize function to finally reach the include function to execute the PHP code we put in a file at the beginning.经过更深入的调查,我们终于发现,已经 PhpArrayAdapter 具备了达到其脆弱 initialize 功能所需的一切。函数 CacheTrait deleteItem 是定义的,允许调用函数最终到达 initialize include 函数以执行我们在开始时放入文件中的PHP代码。

$ cat ./symfony/cache-contracts/CacheTrait.php | grep 'function delete(' -A 3
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }
$ cat ./symfony/cache/Adapter/PhpArrayAdapter.php | grep 'function deleteItem(' -A6
    public function deleteItem(mixed $key): bool
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if (!isset($this->values)) {
            $this->initialize();
$ cat ./symfony/cache/Adapter/PhpArrayAdapter.php | grep 'function initialize(' -A9
    private function initialize()
    {
        if (isset(self::$valuesCache[$this->file])) {
            $values = self::$valuesCache[$this->file];
        } elseif (!is_file($this->file)) {
            $this->keys = $this->values = [];

            return;
        } else {
            $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];

FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1
File inclusion code reached from PhpArrayAdapter

Recap all the vulnerable code

This POP chain started from the __destruct function of the CacheAdapter object which calls its commit function. After digging its code, we identified that the MockFileSessionStorage can be reached via a weak typing trick on the save function, this allowed us to get file write. Finally, PhpArrayAdapter could be reached from a weak typing trick on the delete function, leading to an arbitrary file inclusion after a few more steps.

The following diagram recaps every piece of code covered by the POP chain.下图回顾了 POP 链涵盖的每一段代码。

FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1
Full recap of the code reached by the POP chain in the doctrine/doctrine-bundle package.完整回顾 POP 链在学说/学说捆绑包中达成的代码。

CONCLUSION 结论

Looking for POP chain in huge dependencies is time-consuming, but playing with so much sources is a good way to understand PHP’s mechanism deeply.在巨大的依赖关系中寻找 POP 链是很耗时的,但使用如此多的源代码是深入了解 PHP 机制的好方法。

In the first part of this research, we saw that weak typing can be used as a tool to reach unexpected functions.在这项研究的第一部分中,我们看到弱类型可以用作实现意外功能的工具。

As we could see, it is not always required to use fancy tools to find interesting code path. Understanding an exploitation path and knowing what we are looking for is most of the time sufficient to get the work done! That being said, the methodology used in this article is really time-consuming and could be greatly optimized combined with the usage of a debugger such as Xdebug for example.正如我们所看到的,并不总是需要使用花哨的工具来查找有趣的代码路径。了解开发路径并知道我们正在寻找什么在大多数情况下足以完成工作!话虽如此,本文中使用的方法确实非常耗时,并且可以与调试器的使用( Xdebug 例如)结合使用来大大优化。

In the next part, we will build the full POP chain based on the sources we already dissected and show a full exploit of a vulnerable Symfony application containing the doctrine/doctrine-bundle package. Since this chain is in fact based on 2 different PHP objects and on a code base evolving through PHP versions, some interesting tricks are involved so stay tuned!在下一部分中,我们将基于我们已经剖析的源代码构建完整的POP链,并展示包含该 doctrine/doctrine-bundle 软件包的易受攻击的Symfony应用程序的完整利用。由于该链实际上基于 2 个不同的 PHP 对象和通过 PHP 版本演变的代码库,因此涉及一些有趣的技巧,敬请期待!

原文始发于Rémi MatasseFINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1

版权声明:admin 发表于 2023年9月13日 下午3:25。
转载请注明:FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE : PART 1 | CTF导航

相关文章

暂无评论

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