WordPress 5.0.0 RCE 漏洞分析

WordPress 5.0.0 RCE 漏洞分析

环境搭建

操作系统:win10
PHP 5.6.28
WP 4.9.4

注意:WP有自动更新会自动修补漏洞,需要在wp-config.php中添加define('AUTOMATIC_UPDATER_DISABLED',true);

漏洞利用过程中的一种代码执行流程(最后会有不同系统的利用过程)

由于这个漏洞的特殊性,不同的系统的利用链和细节不一样,但是漏洞本身的原理是一致的。一种利用流程为:

  1. 进入管理后台,媒体文件页面,上传图片马,在更新图片信息处使用burp增加post参数_wp_attached_file并写入目录穿越的路径到数据库
  2. 编辑图片马,在编辑图片处任意编辑后,调用save函数时会读取上一步构造的路径,写入图片到目录穿越的任意文件夹(此处的关键函数为mkdir,有很多细节在后文中会提到)

—前面两步做到了将图片马上传至任意目录—

  1. 上传一个任意格式的辅助文件(txt等都行),在更新文件信息处增加post参数_wp_page_template并写入模板信息到数据库(将模板信息写为图片马的地址)
  2. 访问辅助文件的对应链接,则执行了图片马(模板会被加载,无视图片马后缀)

复现

首先,上传图片:

在编辑图片信息处点击更新:

使用burp截包,加入payload:

1
&meta_input[_wp_attached_file]=2019/07/1.jpg#/../../../../themes/twentyseventeen/1.jpg

  • 注意:主题的目录要根据现在使用的主题来定,主题的根目录即为该目录,如我在复现时根目录为:twentyseventeen

可以看到数据库成功写入将要目录穿越的路径:

然后对刚刚的图片任意编辑并点击保存:

可以看到,图片马成功转移到主题目录下:

由于此处的图片经过gd或imagick库处理,在真实利用时需要制作出能抵抗处理的图片马,我在此处复现时跳过这步,直接硬写入phpinfo:

到此时,已经将图片马成功移动到主题目录,就等着被包含了。

—分割线—

模板包含时,首先上传一个辅助文件:

点击更新:

使用burp截包修改payload:

1
&meta_input[_wp_page_template]=1-e1562921273409.jpg

访问test文件所对应的网页,成功执行代码:

漏洞原理以及修复手段分析

路径变量覆盖漏洞

在编辑并更新图片的时候会调用edit_post()函数,wp-admin/includes/post.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
function edit_post( $post_data = null ) {
global $wpdb;
if ( empty($post_data) )
$post_data = &$_POST;
...
$success = wp_update_post( $post_data );
跟进 wp_update_post->
if ($postarr['post_type'] == 'attachment')
return wp_insert_attachment($postarr);
跟进 wp_insert_attachment->
return wp_insert_post( $data, $wp_error );
跟进 wp_insert_post ->
if ( ! empty( $postarr['meta_input'] ) ) {
foreach ( $postarr['meta_input'] as $field => $value ) {
update_post_meta( $post_ID, $field, $value );
}
}
//终于到了关键点,可以看到对每个meta_input都更新进数据库,这样一来,攻击者就可以覆盖数据库中的变量了。

由于WordPress默认的上传路径为wp-content/uploads/years/month,此时,可以利用目录遍历将其覆盖,payload为&meta_input[_wp_attached_file]=2019/07/1.jpg#/../../../../themes/twentyseventeen/1.jpg

文件任意路径写入

在编辑图片并保存时,会调用 /wp-admin/admin-ajax.php ,跟进时,可以发现函数进入了wp_save_image,此时会从数据库中取出被恶意更改的路径:

1
2
3
4
...
$path = get_attached_file( $post_id ); //查询数据库获取路径,而此时读取到的路径已经是覆盖过的了
...
parent::make_image //调用保存图片的函数

在调用创建图片时,首先调用wp_mkdir_p( dirname( $filename ) ), 整个函数会创建目录;其中,包含创建目录的操作 @mkdir( $target, $dir_perms, true );但是在windows下,创建文件的操作会失败。

然后会使用call_user_func_array调用gd或者imagick保存图片文件(在windows下虽然前一步创建失败,但是不存在的目录也能解析,所以可以直接保存图片)
此时,就成功把图片马写入了模板文件夹。

构造?#的意义,WP的图片获取机制

在编辑页面等地方获取图片时,如果此时使用普通的路径./themes/twentyseventeen/1.jpg,WP会不能获取到图片从而不能进行图片编辑和保存的操作。
在获取图片时,WP首先从数据库中获取路径信息, 由于有的插件根据访问的URL动态生成图片,WP中的图片获取逻辑为:

  1. 在本地文件夹中查找。
  2. 本地未找到(判断为动态图片),使用http协议获取。

关键函数为:

1
2
3
4
5
6
7
/**
* Retrieve the path or url of an attachment's attached file.
*
* If the attached file is not present on the local filesystem (usually due to replication plugins),
* then the url of the file is returned if url fopen is supported.
**/
_load_image_to_edit_path

  • 所以,此处构造的payload意义在WP首先使用fopen访问2019/07/1.jpg#/../../../../themes/twentyseventeen/1.jpg,显然,fopen不能访问该地址;然后再使用http访问http://xxxx....2019/07/1.jpg#/../../../../themes/twentyseventeen/1.jpg,#后的内容被当做锚点处理,成功访问到图片;从而可以顺利进行图片的编辑操作。

  • 在WordPress成功访问到图片后,就会触发保存操作,这样就绕过了图片存在性检验,达到了向任意路径写图片的操作。

模板变量覆盖

在数据库中很容易注意到一条数据:

1
_wp_page_template default

该变量使用数据库变量覆盖的方法可以类似地进行覆盖,将其覆盖为图片马的名称。但是要注意以下代码(/wp-includes/post.php),可以看到,如果page_template已经被设置为非default,并攻击者试图继续覆盖时,操作不能成功,也就是说,每个媒体文件只能被设置一次模板文件,所以可以使用辅助上传新文件的方式来写入模板位置:

1
2
3
4
5
6
7
8
9
10
11
12
if ( ! empty( $postarr['page_template'] ) ) {
$post->page_template = $postarr['page_template'];
$page_templates = wp_get_theme()->get_page_templates( $post );
if ( 'default' != $postarr['page_template'] && ! isset( $page_templates[ $postarr['page_template'] ] ) ) {
if ( $wp_error ) {
return new WP_Error( 'invalid_page_template', __( 'Invalid page template.' ) );
}
update_post_meta( $post_ID, '_wp_page_template', 'default' );
} else {
update_post_meta( $post_ID, '_wp_page_template', $postarr['page_template'] );
}
}

模板文件包含

全局搜索发现了_wp_page_template的引入位置(wp-includes/post-template.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function get_page_template_slug( $post = null ) {
$post = get_post( $post );
if ( ! $post ) {
return false;
}
$template = get_post_meta( $post->ID, '_wp_page_template', true );
if ( ! $template || 'default' == $template ) {
return '';
}
return $template;
}

打断点访问一下上传的辅助文件对应的网页,查看一下调用栈,可以看到程序首先通过index主页路由至test,然后经过一系列模板调用操作,最终加载到了数据库中保存的模板文件:

1
2
3
4
5
post-template.php:1683, get_page_template_slug()
template.php:487, get_single_template()
template-loader.php:55, require_once()
wp-blog-header.php:19, require()
index.php:17, {main}()

最后包含了模板文件:

1
apply_filters( 'template_include', $template )

官方补丁

  1. 首先,补丁限制了参数,不允许传入’meta_input’, ‘file’, ‘guid’;这样一来,攻击者就没有办法覆盖Post Meta变量了,从而在更改路径和更改模板上都不能利用了。
  2. 然后,post_ID也不直接从POST请求获取,而是读取变量获得:$post_data['post_ID'] = $post_ID;,这样,攻击者就不能控制post_id了。

利用思路比较

由于此漏洞涉及到mkdir这个关键的函数,对不同操作系统,其表现不同;所以网上发布的各种分析文章产生了多种利用方法。 mkdir各环境特点概要如下:

  • Linux平台上mkdir不支持不存在的目录跳转。(即会检测每一层有效性)
  • Windows平台上mkdir支持不存在的目录跳转,并且只会创建最后一步的文件夹(即先化简路径,化简后再创建文件夹,不存在的路径不会创建)
  • Windows下*?不能出现在路径中

经过总结,大概有以下几种:

Windows下:

  1. 直接调用crop-image(此时输出的图片为cropped_xxx.jpg
  2. windows下直接利用后台管理页面上提供的image-editor的任意一项修改(裁剪、旋转、对称均可),可以无视不存在的目录直接创建在目标目录下(我使用win10、PHP 5.6.28、wp4.9.4环境下成功利用)(此时输出xxx-e1562915044612.jpg

Linux下:

  1. 直接调用crop-image
  2. 首先利用image-editor创建文件夹(一次只能创建一层文件夹),再次调用image-editor创建图片马;创建文件夹时的payload:&meta_input[_wp_attached_file]=2019/07/1.jpg#/test.jpg,此时在07创建了1.jpg#文件夹(在Windows下也能成功):

使用crop-img的payload为:

1
2
3
4
5
action=image-editor&_ajax_nonce=d6b0263153&postid=4&history=%5B%7B%22c%22%3A%7B%22x%22%3A86%2C%22y%22%3A61%2C%22w%22%3A227%2C%22h%22%3A110%7D%7D%5D&target=all&context=edit-attachment&do=save
->
action=crop-image&_ajax_nonce=d6b0263153&id=4&cropDetails[x1]=10&cropDetails[y1]=10&cropDetails[width]=10&cropDetails[height]=10&cropDetails[dst_width]=100&cropDetails[dst_height]=100

遇到的坑

自动更新

wp会自动更新小版本,需要在wp-config.php添加define('AUTOMATIC_UPDATER_DISABLED',true);

mkdir

windows环境下,文件夹中不能出现?号,此时,使用php内置函数mkdir就无法执行成功;网上的payload不能直接用;需要将?改为#

id参数名变更

使用crop-image时,postid改为id。

template覆盖

  • 如果设置了这个值,但这个文件不存在,则会被定义为default。
  • 如果该值被设置,则没办法通过这种方式修改。
  • 所以要写模板的路径必须新上传一个媒体文件

写在最后

整个利用链涉及到的模块很多,也涉及到了不同操作系统的某些特性。漏洞的逻辑较为复杂,自己花了比较长的时间,也学到很多东西。

  • 就这个漏洞而言,WP直接留下的媒体文件信息更新接口在前端上并没有直接的反应,不知道更新文件路径、裁剪图片和文件模板的接口直接暴露在外是怎样的想法。也许是功能未实现的遗留,也许是计划要实现的功能,为什么出现这些函数也不得而知。需要对WP整个执行流程甚至源码开发本身有比较深入的理解才能知道其中产生的原因。
  • 就不同操作系统而言,mkdir函数的变现直接影响到了利用过程,在不同系统、不同php版本下都有很多不一样的细节。在天融信阿尔法实验室的文章中有较为详细的底层探究知识,自己在这块还比较薄弱,需要进一步学习。

参考资料

  • https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/
  • https://paper.seebug.org/863/
  • https://zeroyu.xyz/2019/03/06/wordpress-5-0-0-rce-aanalysis/
  • http://blog.nsfocus.net/wordpress-5-0-0-rce/
  • https://lorexxar.cn/2019/02/22/wordpress-core-rce/
  • https://paper.seebug.org/825/
  • http://yulige.top/?p=578