Q:
为什么要写这篇文章?
A:
RCTF2020中的swoole一题刺激到我了(,那道题找了两天也找不到链。
再后来第五空间2020的那个laravel也是找了一天,最后还是靠phpggc做了次脚本小子。
我在想为啥我找不到链呢?故有此文。
PS: 虽然写的是一些总结性的东西。但作者也不过找到5条框架的链而已,见识还是太少。希望师傅们多多包涵,加以指正。
在使用php的反序列化漏洞前需要两个条件
本文不会对形成反序列化漏洞的点,进行讲解,其它大师傅已经讲解的十分详细了。
这里就我这两天的挖链经历进行一个总结。
我在挖链的途中总结出以下两点
我们接下来的pop chain构造便一直基于这两点
序列化是将对象的属性进行格式的转换,但不会包括方法。所以如果想要反序列化达成恶意的操作必须需要方法的执行。
对于起点来说,我们自然是要找到可以自动调用的方法。常见的可以自动调用的方法便是魔术方法。魔术方法的介绍有很多,这里就不详细介绍了。
目前只有两个魔术方法可以被使用
__destruct
__wakeup
其中,最为常用的魔术方法是__destruct
,其特性是对象被销毁前被调用。从系统结构的角度讲,其最常见的场景是关闭某些功能。比如关闭文件流,更新数据等。但从反序列化的角度讲,其特殊的使用场景,代表在这个方法内可能会调用类内的其它方法。
而另一个__wakeup
方法就不太常用了,其特性是反序列化时进行调用。那么可以想象开发人员在对其进行编写时,可能会将其作为一个“进行反序列化时属性合法性校验的”方法。
最经典的就是GuzzleHttp
包中的GuzzleHttp\Psr7\FnStream
类,其内部存在大量变量可控的危险函数。但以下这一个方法就直接避免了这个方法被恶意使用。
public function __wakeup() { throw new \LogicException('FnStream should never be unserialized'); }
当然也不是没有使用了__wakeup
的链,只不过从各个方面来讲,__destruct
确实更好用一些。
接下来再看一个例子。
phpggc是github上的一个项目,其存储着大量反序列化链,可以说是反序列化的武器库。
其存储了大量的laravel框架的RCE反序列化链,仔细观察发现一共6条反序列化链,5条都使用了同一个类作为起点,还有一条也间接调用了此类。其便是PendingBroadcast.php
下的__destruct
方法
public function __destruct() { $this->events->dispatch($this->event); }
为什么这个方法会变成公交车呢?
我认为共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。
总之,就是不计一切代价扩大链的可能性,为寻找到可以利用的方法提供机会。
终点在我看来有两类
动态调用就是像($this->a)($this->b)
或者$this->a[0]($this->b)
或者这样的危险动态调用。
危险函数,就是根据目的寻找需要的函数。如要RCE,则寻找类似于call_user_func
,array_walk
这样的会进行函数调用的函数。如要FW,则寻找file_put_content
这样的函数…
这里稍微讲一讲Yii2框架的链吧。当时搜了一波文章好像也没有。
环境准备我就不详细写了
composer create-project yiisoft/yii2-app-basic app
composer+docker+vscode一把梭
首先全局搜索__destruct
和__wakeup
这两个魔术方法。可以使用grep命令grep -A 10 -rn "__destruct"
。或者直接使用vscode的全局搜索也可。
最后我将其定位在yii\db\BatchQueryResult
类中的__destruct
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
方法
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
方法
public function getIsActive() { return session_status() === PHP_SESSION_ACTIVE; }
可以看到其对会话的状态进行了一个判别。这里我意外发现,只有当Yii的debug和gii这两个默认扩展都存在(不一定要开启)时,这里返回true。否则返回false。这里我还不知道为什么,希望有师傅可以解答...
这里算是这条链唯一的缺憾了吧...
总之返回true后继续调用了接口类yii\web\MultiFieldSession
的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
,但可惜参数无法控制,传入一个对象为参数的可用函数也不太多。那就像我刚才所说,赋值为[(new test), "aaa"]
这样的一个数组。就可以调用test
类中的aaa
公共方法。
那么有没有这样的公共方法?
grep -A 10 -rn "public[[:space:]]function[[:space:]].*\(\)"
最后我找到了yii\rest\IndexAction
中的run
方法
public function run() { if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id); } return $this->prepareDataProvider(); }
这下两个都可控了。
因为上一条链受扩展影响,所以打算再找一条
这次我定位到\Symfony\Component\String\UnicodeString
的__wakeup
类
public function __wakeup() { normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); }
我看了一下normalizer_is_normalized
这个函数,其要求参数是字符串。参数又可控,那就调用__toString
方法吧。
最后我找到了\Symfony\Component\String\LazyString
的__toString
方法
public function __toString() { if (\is_string($this->value)) { return $this->value; } try { return $this->value = ($this->value)(); } catch (\Throwable $e) { //... } }
啊这...结合最后一部分一打不就完事了?
喜加一
$this->aaa->bbb()
这样类似的结构前可以利用instanceof
进行检查,查看其是否是期望调用的类。__destruct
和__wakeup
。__wakeup
方法且类没必要序列化时,可以考虑使用__wakeup
阻止反序列化。不要让unserialize和文件类函数用户可控!!!
不要让unserialize和文件类函数用户可控!!!
不要让unserialize和文件类函数用户可控!!!
emmm感觉写了一篇水文
那就这样,这里放上我fork的phpggc,有想学习上面几条链的师傅可以看看
如有错误还请指出!