如何挖掘自己的php反序列化链

本文先发于先知社区: https://xz.aliyun.com/t/8082

前言

Q:
为什么要写这篇文章?

A:
RCTF2020中的swoole一题刺激到我了(,那道题找了两天也找不到链。
再后来第五空间2020的那个laravel也是找了一天,最后还是靠phpggc做了次脚本小子。
我在想为啥我找不到链呢?故有此文。

PS: 虽然写的是一些总结性的东西。但作者也不过找到5条框架的链而已,见识还是太少。希望师傅们多多包涵,加以指正。

简单介绍

在使用php的反序列化漏洞前需要两个条件

  1. 可以进行反序列化的点
  2. 合理的pop chain
    这一对组合拳形成的反序列化漏洞可以进而造成RCE、文件读写、信息泄露等危害。

本文不会对形成反序列化漏洞的点,进行讲解,其它大师傅已经讲解的十分详细了。
这里就我这两天的挖链经历进行一个总结。

总则

我在挖链的途中总结出以下两点

  1. 变量可控
    在危险的函数和结构上的可控变量要尽可能的多
  2. 扩大影响
    尽可能的去寻找可以扩大攻击面的结构与方法

我们接下来的pop chain构造便一直基于这两点

寻找起点

序列化是将对象的属性进行格式的转换,但不会包括方法。所以如果想要反序列化达成恶意的操作必须需要方法的执行。
对于起点来说,我们自然是要找到可以自动调用的方法。常见的可以自动调用的方法便是魔术方法。魔术方法的介绍有很多,这里就不详细介绍了。

目前只有两个魔术方法可以被使用

  1. __destruct
  2. __wakeup

其中,最为常用的魔术方法是__destruct,其特性是对象被销毁前被调用。从系统结构的角度讲,其最常见的场景是关闭某些功能。比如关闭文件流,更新数据等。但从反序列化的角度讲,其特殊的使用场景,代表在这个方法内可能会调用类内的其它方法。

而另一个__wakeup方法就不太常用了,其特性是反序列化时进行调用。那么可以想象开发人员在对其进行编写时,可能会将其作为一个“进行反序列化时属性合法性校验的”方法。
最经典的就是GuzzleHttp包中的GuzzleHttp\Psr7\FnStream类,其内部存在大量变量可控的危险函数。但以下这一个方法就直接避免了这个方法被恶意使用。

1
2
3
4
public function __wakeup()
{
throw new \LogicException('FnStream should never be unserialized');
}

当然也不是没有使用了__wakeup的链,只不过从各个方面来讲,__destruct确实更好用一些。

接下来再看一个例子。
phpggc是github上的一个项目,其存储着大量反序列化链,可以说是反序列化的武器库。
https://github.com/ambionics/phpggc
其存储了大量的laravel框架的RCE反序列化链,仔细观察发现一共6条反序列化链,5条都使用了同一个类作为起点,还有一条也间接调用了此类。其便是PendingBroadcast.php下的__destruct方法

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

为什么这个方法会变成公交车呢?
我认为共3点

  1. 可自动调用
  2. 参数可控
  3. 攻击面广
    自动调用自然不必多说,__destruct方法嘛。但值得注意的是其参数,和结构。
    我们可以看到$this->events$this->event都是可控的,这意味着我们的链可以有两条走向。
    其一是将$this->events赋值为没有dispatch方法的实例,来调用其__call方法。
    其二是去调用各种类中dispatch方法,如果有的dispatch中有危险的函数或者结构,那就考虑使用它。
    这样就大幅扩大了我们的攻击面。

跳板挑选

所谓的跳板,就是在方法和方法、结构和结构、方法和结构之间的跳跃。

常见的例子是一些字符串函数,例如trim,如果其参数可控,我们将其赋值为存在__toString方法的对象即可调用这个方法。

还有类似于call_user_func($this->test);或者$test();这种只能调用没有参数的函数的结构。出来简单的调用phpinfo以外,我们也可以考虑将变量赋值为[(new test), "aaa"]这样的一个数组。就可以调用test类中的aaa公共方法。

再者,就是new $test1($test2, $test3);这样的结构也可以调用__construct方法。或者像RCTF2020-swoole一题一样,新建一个PDO对象来进行mysql的load file。

总之,就是不计一切代价扩大链的可能性,为寻找到可以利用的方法提供机会。

终点

终点在我看来有两类

  1. 危险动态调用
  2. 危险函数

动态调用就是像($this->a)($this->b)或者$this->a[0]($this->b)或者这样的危险动态调用。

危险函数,就是根据目的寻找需要的函数。如要RCE,则寻找类似于call_user_funcarray_walk这样的会进行函数调用的函数。如要FW,则寻找file_put_content这样的函数…

实战演练

这里稍微讲一讲Yii2框架的链吧。当时搜了一波文章好像也没有。

环境准备我就不详细写了

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

composer+docker+vscode一把梭

Yii2/RCE1

首先全局搜索__destruct__wakeup这两个魔术方法。可以使用grep命令grep -A 10 -rn "__destruct"。或者直接使用vscode的全局搜索也可。

最后我将其定位在yii\db\BatchQueryResult类中的__destruct

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

public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}

可以看到,就像我刚才说的一样,这里既可以调用__call方法,也可以调用close方法。

在搜了一波__call方法感觉没戏后,这里我选择调用了close方法
全局搜grep -A 10 -rn "function[[:space:]]close",或者vscode

这里我选择了yii\web\DbSession类中的close方法

1
2
3
4
5
6
7
8
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}

首先调用了父类的getIsActive方法

1
2
3
4
public function getIsActive()
{
return session_status() === PHP_SESSION_ACTIVE;
}

可以看到其对会话的状态进行了一个判别。这里我意外发现,只有当Yii的debug和gii这两个默认扩展都存在(不一定要开启)时,这里返回true。否则返回false。这里我还不知道为什么,希望有师傅可以解答…

这里算是这条链唯一的缺憾了吧…
总之返回true后继续调用了接口类yii\web\MultiFieldSessioncomposeFields方法

1
2
3
4
5
6
7
8
9
10
11
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,但可惜参数无法控制,传入一个对象为参数的可用函数也不太多。那就像我刚才所说,赋值为[(new test), "aaa"]这样的一个数组。就可以调用test类中的aaa公共方法。

那么有没有这样的公共方法?
grep -A 10 -rn "public[[:space:]]function[[:space:]].*\(\)"
最后我找到了yii\rest\IndexAction中的run方法

1
2
3
4
5
6
7
8
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}

return $this->prepareDataProvider();
}

这下两个都可控了。

调用栈

Yii2/RCE2

因为上一条链受扩展影响,所以打算再找一条
这次我定位到\Symfony\Component\String\UnicodeString__wakeup

1
2
3
4
public function __wakeup()
{
normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
}

我看了一下normalizer_is_normalized这个函数,其要求参数是字符串。参数又可控,那就调用__toString方法吧。

最后我找到了\Symfony\Component\String\LazyString__toString方法

1
2
3
4
5
6
7
8
9
10
11
12
public function __toString()
{
if (\is_string($this->value)) {
return $this->value;
}

try {
return $this->value = ($this->value)();
} catch (\Throwable $e) {
//...
}
}

啊这…结合最后一部分一打不就完事了?
喜加一

调用栈

防御策略

动态调用与危险函数

  1. 在写到动态调用和危险函数时,务必对变量和方法进行回溯。查看变量是否是可控的。
  2. 在容许的情况下,使用静态属性进行动态调用可以防止可控变量调用危险函数。
  3. 在调用$this->aaa->bbb()这样类似的结构前可以利用instanceof进行检查,查看其是否是期望调用的类。

方法

  1. 注意尽量少的在魔法方法中写入可以调用大量其它方法的方法。尤其是__destruct__wakeup
  2. 注意在公共且不需要参数的方法中不要直接调用危险函数和动态调用。
  3. 在不需要__wakeup方法且类没必要序列化时,可以考虑使用__wakeup阻止反序列化。

最最最最最重要的

不要让unserialize和文件类函数用户可控!!!
不要让unserialize和文件类函数用户可控!!!
不要让unserialize和文件类函数用户可控!!!

后言

emmm感觉写了一篇水文
那就这样,这里放上我fork的phpggc,有想学习上面几条链的师傅可以看看
https://github.com/AFKL-CUIT/phpggc
如有错误还请指出!

Comments