原文链接:FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE: PART 1
译者:知道创宇404实验室翻译组
Symfony的doctrine/doctrine-bundle
包是与 Symfony 应用程序一起安装的最常见的捆绑包之一。截至本文发布,其已被下载了1.44亿次,使其成为一个反序列化利用目标。
本文的第一部分旨在展示POP链研究的完整方法论,详细介绍用于识别有效的易受攻击路径的完整代码分析方法。第二部分将将重点关注基于本节分析的代码,通过基本的试错逻辑来构建完整有效的 POP 链。
正如博文所述,在主要的Symfony框架中很难找到POP链,许多基本功能只能通过额外的依赖项来实现,如用作其 ORM(对象关系映射)的 Doctrine。这个ORM也是许多其他PHP项目中最常用的之一:Drupal、Laravel、PrestaShop等。它用于管理抽象应用程序对数据库的访问。
为了使Doctrine与Symfony兼容,从该项目发布的第一个README的第一段来看,它从Symfony 2.1版本以来就创建了doctrine-bundle
,:
由于Symfony 2不想强制或建议用户使用特定的持久化解决方案,所以已将该捆绑包从Symfony 2框架的核心中删除。Doctrine2仍将是Symfony的重要角色,并且该捆绑包Doctrine和Symfony主要由社区的开发人员进行维护。
重要提示:本捆绑包为Symfony 2.1及更高版本开发。对于Symfony 2.0应用程序,DoctrineBundle仍随核心Symfony存储库一起提供。
因为在挖掘 PHP 依赖项时基于的范围非常巨大,所以在PHP依赖项中查找POP链可能非常耗时,
以下部分描述了找到它们并使它们协同工作所遵循的完整方法和逻辑。
首先,必须找到__wakeup
、__unserialize
或在项目依赖项中实现的__destruct
方法。该方法也可以被调用,但反序列化后的对象必须在print
或echo
等函数内部调用,因此这种情况不太可能发生。
在不深入细节的情况下(关于用例和技巧的更多详细信息可在payload all the things上找到),当序列化字符串进行反序列化时,__wakeup
方法将首先被调用(或替代调用__unserialize
)。然后,如果定义了该对象的__destruct
方法,它最终将被销毁。
为了使本文中的假设和流程更易于理解,以便识别完整的链条,将使用一个图示来展示所遵循的逻辑的每个步骤。因此,在这项研究中,首要目标是对代码库中的__wakeup
、__unserialize
和__destruct
函数进行排序。
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()
在像Symfony这样的高度强化的项目中,因为它会在调用__destruct
函数之前被调用,因此通常通过在调用__wakeup
函数时抛出错误来设置防止反序列化保护。如下所示,许多Symfony类都设置了这种深度强化。
$ grep -hri 'function __wakeup' -A4 .
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
--
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
--
[...]
对实现__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
幸运的是,Doctrine\Common\Cache\Psr6\CacheAdapter
类看起来非常有希望!在Doctrine版本1.11.x之前(自2019年以来一直维护),它被用作默认的Doctrine缓存适配器,但目前已经被弃用。然而为了向后兼容性,即使代码不应该再使用,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
函数的类。
在上下文中调用不可访问的方法时会触发
__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)
虽然本文没有对其进行描述(因为在这里没有产生结果),但如果你正在寻找POP链,其也会覆盖__call
函数。
我们到达的commit
函数通常用于更新Doctrine缓存中的延迟项。基本上它会删除已过期的项,并将所有其他项保存在缓存定义中。
类属性的phpdoc(@var行)建议$cache
应该实现Cache
接口,而$deferredItems
应该是一个CacheItem
或TypedCacheItem
的数组。这仅用于文档目的,并不强制进行强类型化,这意味着我们可以通过反序列化来控制将要实现的类,从而劫持对它们方法的任何调用。
从这一点出发,可以从$this->cache
或$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();
}
}
getExpiry()
函数这个函数并不是一个好的匹配,它并没有触及到有趣的代码,并且只在两个类中定义:
$ 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
方法中写入的文件的路径。
<?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
。
$ grep -Ri 'return $this->pool->deleteItem($key);' .
./symfony/cache/Psr16Cache.php: return $this->pool->deleteItem($key);
乍一看,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文档将特性定义为代码重用的一种方式,基本上是一种在另一个类中编写函数属性并定义它们的方式。只需要通过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
,而ContractsTrait
使用了CacheTrait
,因为特性可以嵌套使用。
经过深入调查,我们最终发现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链从调用其函数的对象__destruct
的函数开始。在深入研究其代码后,我们确定可以通过对save
函数的弱类型技巧来实现MockFileSessionStorage
,这使我们能够进行文件写入。最后,通过对delete
函数的弱类型技巧,可以实现PhpArrayAdapter
,经过几个步骤后实现任意文件包含。
下图总结了POP链涉及的每一段代码。
在巨大的依赖关系中寻找 POP 链是很耗时的,但是使用如此多的源代码是深入理解 PHP 机制的好方法。
在研究的第一部分中,我们看到弱类型可以用作实现意想不到的功能的工具。
正如我们所看到的,不总是需要使用高级工具来找到有趣的代码路径。理解一个攻击路径并知道我们正在寻找什么通常足以完成工作!话虽如此,本文中使用的方法非常耗时,并且结合使用调试器(例如Xdebug
)可以大大优化效率。
在下一部分中,我们将基于已经分析过的源代码构建完整的POP链,并展示一个包含doctrine/doctrine-bundle
包的易受攻击的Symfony应用程序的完整利用。由于这个链实际上是基于两个不同的PHP对象和一个随着PHP版本变化的代码库,所以涉及了一些有趣的技巧,敬请关注!
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3041/