本文介绍了使用简单的命令工具配合强大的逻辑寻找php依赖包中的POP反序列化链的过程,涉及的主要技术有:
- 反序列化的概念
- 任意文件写入
- 任意文件包含
- PHP特性分析
- PHP弱类型安全
翻译来源:
https://www.synacktiv.com/en/publications/finding-a-pop-chain-on-a-common-symfony-bundle-part-1
Symfony 的依赖包
doctrine/doctrine-bundle
是SymfonyPHP应用程序最常见的捆绑包之一。截至目前,它已被下载了 1.44 亿次,使它成为反序列化利用的有效目标,通过本文可以提高你对PHP反序列化利用的了解,并从源头理解为什么弱类型语言是被认为具有安全缺陷的。本文的第一部分旨在展示POP链研究的完整方法,详细介绍了用于识别完整有效漏洞路径的代码分析方法。第二部分则侧重于基于本节分析的代码的基本代码逻辑来完整构建有效的成功利用的POP 链。
由于Symfony代码的极简主义风格,因此在主要的Symfony框架中找到POP链非常相当困难。许多基本的函数仅带有额外的依赖项,例如用作其ORM(对象关系映射)的Doctrine,这个ORM也是许多其他PHP项目中最常用的项目之一,如:Drupal,Laravel,PrestaShop等。它用于管理和抽象应用程序的数据库访问。
为了使Doctrine和Symfony相互兼容,该项目上发布的第一个自述文件的第一段说明, doctrine-bundle
那么它是从Symfony版本2.1开始创建的。
因为Symfony 2不想强迫或建议用户特定的持久性解决方案,所以这个依赖包从Symfony 2框架的核心中删除了。Doctrine2仍将是Symfony世界的主要参与者,该捆绑包由Doctrine和Symfony社区的开发人员维护。
重要提示:此依赖包是为Symfony 2.1及更高版本开发的。对于Symfony 2.0应用程序,DoctrineBundle仍然随核心 Symfony 存储库一起提供。
查找 POP 链可能很耗时,因为在挖掘 PHP 依赖项时,它们所基于的范围很大。这就是为什么重要的是要记住从哪里开始,并逐步了解如何从一个对象跳到另一个对象,以下各节介绍了查找它们并使它们协同工作,并遵循的完整方法和逻辑。
首先,必须找到项目依赖项中实现的__wakeup
、__unserialize
和 __destruct
方法,也可以调用方法 __toString
,但未序列化的对象必须在函数内部调用,例如 在其反序列化之后被内部函数print
或 echo
调用。
https://www.php.net/manual/en/language.oop5.overloading.php
在不深入细节的情况下(有关更多的细节和技巧可以在https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Insecure%20Deserialization/PHP.md找到所有内容),当序列化字符串被反序列化时,首先调用`__wakeup`方法(或 __unserialize
方法),对象会最终将被销毁,如果已定义,则调用其 __destruct
方法。
https://www.php.net/manual/fr/language.oop5.magic.php#object.wakeup
为了识别整个pop链,遵循的逻辑的每个步骤都将以图片呈现。因此,本研究的第一个目标是从 __wakeup
、 __unserialize
或 __destruct
函数进行搜索过滤。
可以从 composer
安装 doctrine-bundle
依赖项。之后,一个简单的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()
像Symfony这样的深度强化的项目中,通常在调用 __wakeup
函数时抛出错误来设置对反序列化的保护,因为它是在 __destruct
函数之前调用的。如下面的结果所示:
$ grep -hri 'function __wakeup' -A4 . public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } -- public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } -- [...]
可以对不包含 BadMethodCallException
关键字的 __destruct
类进行搜索,搜索命令如下:
$ 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
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(); } }
正如我们所看到的, __destruct
函数是可以到达的,并直接调用对象的 commit
函数,让我们看看从这一点可以做些什么,研究的另一条途径是分析定义 __call
函数的类。
PHP文档解释说:
在对象上下文中调用不可访问的方法时触发
__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)
虽然它不会在这篇文章中描述,因为它无法在这里利用,但请记住,寻找流行链的过程中,它也应该包括在内,因为它在大多数情况下都可以到达。
类属性上的phpdoc(@var行)建议 $cache
应该实现 Cache
接口, $deferredItems
应该是 CacheItem
或 TypedCacheItem
的数组。这仅仅是出于文档的目的,并不强制强类型,这意味着对它们的方法任何调用都可能被劫持,因为我们可以通过反序列化来控制哪个类将被实现。
<?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(); } }
getExpiry()
函数这个函数不是一个很好的利用链,因为没有相关的利用代码,只在 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- }
get()
函数这个函数似乎很有希望,至少在 doctrine/doctrine-bundle
依赖项中的 53 个文件中找到定义,搜索命令如下:
$ grep -ri 'function get(' | wc -l 53
但是, 在到达语句$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] } } }
save($param1, $param2, int $param3)
函数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
其中许多类对于我们的目的毫无用处,但 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] } } }
delete($param1)
函数与 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)
从这些类中,Symfony\Component\Cache\Adapter\PhpArrayAdapter
类可用于任意文件包含include
,这是这个POP链的最终目标,不过需要定义一个 $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; } [...] } }
此链用于 include
包含任意路径中的文件。
在 PHP 代码中查找易受攻击的代码时,第一步是查看用户提供的数据是否被传递给危险的函数,例如 system
、、 eval
include
require
exec
popen
call_user_func
file_put_contents
。然而,还有许多其他人,这里的主要思想是,由于潜在的易受攻击的范围已细化为 save()
函数,因此现在有必要从分析的依赖项中审核每个可访问的保存函数,以识别 POP 链。
正如我们所看到的,唯一可访问和可利用的函数是在 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; }
虽然一开始看起来很有希望,但无法定义生成的文件的扩展名,这使得它并不好利用
<?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'; } }
但是,可以控制注入到文件中的序列化数据,然后作为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].
其中$path = $this->getFilePath()
代码用于定义 file_put_contents
在方法中写入的文件的路径。
https://www.php.net/manual/en/function.file-put-contents.php
<?php namespace Symfony\Component\HttpFoundation\Session\Storage; class MockFileSessionStorage extends MockArraySessionStorage { [...] private function getFilePath(): string { return $this->savePath.'/'.$this->id.'.mocksess'; } }
将后缀设置为 .mocksess
文件,防止我们在文件夹中创建一个 .php
文件来完成代码执行。在继续执行第二步之前,获取任意文件写入是唯一需要的先决条件,以下架构包含了 POP 链的第一个元素。
可以应用相同的方法来查找 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 [...]
在搜索了许多常见的危险功能后,很明显,这些功能没有立竿见影的效果,这意味着需要逐个深入研究它们,从寻找在这个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);
deleteItem
函数很有可能被利用,因为它是从许多 delete
函数调用的,看看从它可以到达什么。
通过调用 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) ?: [[], []]; }
可惜的不能使用PHP过滤器链来单独从这个路径执行命令,因为 elseif (!is_file($this->file)
条件,该条件验证文件是否存在于文件系统中,从而阻止对 php://
的任何调用,综上所述,现在的目标是找到一种到达 PhpArrayAdapter
的方法,以便到达它的 deleteItem
函数来将完整的流程拼在一起。
现在流程已经规划好了,看看如何从之前发现的 delete
函数到达 PhpArrayAdapter
。首先, 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; }
检查参数 $pool
是 CacheItemPoolInterface
接口,可以防止使用 PhpArrayAdapter
类。
既然最直接的反序列化路径已经失效了,看看还剩下什么选择。
$ 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 [...]
在定义 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
调用使用它的对象的 deleteItem
函数。如果目标类 PhpArrayAdapter
刚好使用了 CacheTrait
,那么就能够调用它的 deleteItem
函数,从而到达执行代码所需的 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; [...]
即使 PhpArrayAdapter
类不直接使用 CacheTrait
,它也使用它的 ContractsTrait
,因为traits特性可以嵌套。
经过更深入研究,最终发现 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) ?: [[], []];
这个POP链从 CacheAdapter
对象的 __destruct
函数开始,该对象调用其 commit
函数。在挖掘了它的代码之后,发现MockFileSessionStorage
可以通过 save
函数上的弱类型技巧来访问,这允许进行文件写入。最后, PhpArrayAdapter
可能是通过 delete
函数上的弱类型技巧到达的,导致在几个步骤后包含任意文件。
下图概述了本文POP链涉及的所有代码。
在巨大的依赖关系中寻找POP链是很耗时的,但是理解这么多的源代码是深入理解PHP机制的好方法。
在这项研究的第一部分,我们看到弱类型可以作为一种工具来达到意想不到的功能,所以并不总是需要使用各种“奇怪”的工具来找到可利用的代码路径,了解利用路径并知道我们在寻找什么,大多数情况下足以完成工作。也就是说,本文中使用的方法确实非常耗时,也可以结合使用调试器(例如 Xdebug
调试器)进行代码路径优化。
在下一部分中,将基于已经剖析的源代码构建完整的POP链,并展示对包含依赖包doctrine/doctrine-bundle
的易受攻击的Symfony应用程序的完整利用过程,因为这个链实际上是基于两个不同的PHP对象和一个通过PHP版本进化的代码库,所以涉及到一些更有趣的利用技巧,敬请期待。