wordpress5.0.0RCE分析

youncyb 发布于 2019-03-21 4721 次阅读 Web安全


1.环境配置

  1. WordPress5.0.1+ImageMagick6.9.6
  2. 由于官方所有版本都在wp-admin/includes/post.php中加了一个补丁
function _wp_get_allowed_postdata( $post_data = null ) {
	if ( empty( $post_data ) ) {
		$post_data = $_POST;
	}
	// Pass through errors
	if ( is_wp_error( $post_data ) ) {
		return $post_data;
	}
	return array_diff_key( $post_data, array_flip( array( 'meta_input', 'file', 'guid' ) ) );
}

所以在复现的时候,将meta_input去掉

  1. 作者(author)权限及以上
  2. 用exiftool生成恶意jpg
  3. 在安装wordpress之前,于wp-config-sample.php添加一句:
    define('AUTOMATIC_UPDATER_DISABLED', true);用于阻止自动更新,不然代码可能会有不小的变动

2.漏洞成因

  1. 数据库参数注入。由于没有对meta_input进行过滤,导致wp_postmeta表的meta_key(_wp_attached_file)和meta_value可控,借此为第二点打下基础。
  2. 目录穿越写文件。由于第一点作为基础,我们可通过后台图片编辑功能,并抓包修改操作为crop-image进行文件写入。
  3. 本地文件包含漏洞。通过模板文件包含,控制_wp_page_template的值为我们在第二步写入的文件,进行文件包含。

3.漏洞复现

  1. 在控制台媒体处上传一张jpg,上传后修改图片,添加
    &meta_input[_wp_attached_file]=2019/03/evil-2.jpg#/../../../../themes/twentynineteen/youncyb.jpg
    avatar
    avatar
    可以看到,数据库中,_wp_attached_file值已经变成了我们添加的数据。
  2. 点击编辑图片,抓包,修改数据为
    action=crop-image&_ajax_nonce=f1fd9506ea&id=6&cropDetails[x1]=10&cropDetails[y1]=10&cropDetails[width]=10&cropDetails[height]=10&cropDetails[dst_width]=100&cropDetails[dst_height]=100
    注意nonce和id要为修改前的,然后提交,可以看到已经在wp-content/themes/twentynineteen/成功写入了文件crop-xxx.jpg
    avatar
    avatar
  3. 继续上传一个txt文件,按照步骤1修改&meta_input[_wp_page_file]=cropped-youncyb.jpg&page_template=0(这里不加page_template将会导致_wp_page_file被更新为default,从而导致包含失败)
    avatar
  4. 点击查看附件
    avatar
    avatar

注:由于我没有安装ImageMagick6.9.6导致,代码只能用jd库来对图片进行裁剪,jd库会去除jpg中所有的php代码,需要精心构造,才能将代码保存,而ImageMagick会保存jpg的exif区域,这样我们可以利用exiftool:

exiftool evil.jpg -ownername="<?php phpinfo();?>"
复现只好人为的修改下cropped-youncyb.jpg。

4.漏洞分析

1. wp_postmeta 数据注入分析

根据路径wp-admin/post.php 以及action=editpost,我们直接定位到代码:
avatar
继续跟进edit_post()函数:
avatar
avatar
可以看到$post_data 先用_wp_translate_postdata()函数被转换为用于插入数据库的格式,然后继续进入waf:_wp_get_allowed_postdata( $post_data );这个waf就是我们最开始所讲的官方补丁,跟踪$translated变量继续往下看:
avatar
跟进wp_update_post( $translated );函数:
avatar
$post会根据$postarr从数据库中取出数据,然后将$post和$postarr合并,再进入wp_insert_attachment($postarr);函数,继续跟进:
avatar
继续跟进wp_insert_post( $data, $wp_error );函数:
avatar
继续跟进update_post_meta( $post_ID, $field, $value );函数,这个函数对应就是图片更新操作:
avatar
通过update_metadata函数对wp_postmeta进行更新,至于meta_input的key为什么是_wp_attached_file,我们接着分析第二个漏洞:

2. 目录穿越写文件

根据wp-admin/admin-ajax.php以及action=crop-imagee,我们定位到代码:
avatar
拼接后就是:wp_ajax_crop_image函数,全局搜索在wp-admin/includes/ajax-actions.php:
avatar
我们可以看到check_ajax_referer函数会检查nonce和id,这也就是为什么更改id和nonce的原因,继续跟进wp_crop_image函数:
avatar
进入get_attached_file( $src );函数:
avatar
<codee>get_post_meta( $attachment_id, '_wp_attached_file', true );函数会从wp_postmeta表中取出数据,这也就说明了为什么meta_key要为_wp_attached_file,然后将$file拼接为:/Applications/MxSrvs/www/wordpress/wp-content/upload/2019/03/evil.jpg#/../../../../themes/twentynineteen/youncyb.jpg 返回到wp_crop_image函数,通过file_exists( $src_file )函数判断文件是否存在,但由于evil.jpg#是个假目录,文件不存在,所以进入_load_image_to_edit_path( $src, 'full' );函数:
avatar
同样会进行上一步的操作,但由于文件不存在的原因,程序进入第二个分支,跟进wp_get_attachment_url( $attachment_id )函数:
avatar
同样$file = get_post_meta( $post->ID, '_wp_attached_file', true )会从wp_postmeta表中取出_wp_attached_file的值,经过判断,进入最后一个分支:所以$url=http://127.0.0.1/wordpress/wp-content/uploads/2019/03/evil.jpg#/../../../../themes/twentynineteen/youncyb.jpg,然后返回到wp_crop_image函数,进入到wp_get_image_editor( $src );函数:
avatar
继续跟进_wp_image_editor_choose( $args );函数:
avatar
可以看到,会用array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' )其中之一作为图片剪辑的函数,根据代码逻辑会优先选择ImageMagick,这也是为什么我们需要安装ImageMagick扩展了,返回到wp_crop_image函数,根据代码:$dst_file = str_replace( basename( $src_file ), 'cropped-' . basename( $src_file ), $src_file );$dist_file被修改为:/Applications/MxSrvs/www/wordpress/wp-content/upload/2019/03/evil.jpg#/../../../../themes/twentynineteen/cropped-youncyb.jpg,通过wp_mkdir_p( dirname( $dst_file ) );函数创建目录,会创建一个假目录evil.jpg#,接着看$editor->save( $dst_file );函数:
avatar
跟进make_image函数:
avatar
注意到:这里也有个wp_mkdir_p函数,由于已经存在evil.jpg#目录,这里就可以通过../跨目录直接写文件

3. 本地模板包含

wp-includes/template-loader.php中:
avatar
先回通过is_404()、is_search()、is_home()等函数对页面类型进行判断,进而包含相应的模板,全局搜索_wp_page_tempatewp-includes/post-template.php可以看到:
avatar
get_page_template_slug函数,会取_wp_page_template作为模板,查找其用法,发现正好是template-loader.php中:
avatar
get_page_template_slug函数调用的也是get_post_meta函数,所以只需和第一步同样的操作即可

4. 参数page_template=0

在 wp-includes/post.php中:

	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'] );
		}
	}

由于update_post_meta( $post_ID, '_wp_page_template', 'default' );会再次使_wp_page_template的值变为default,导致包含失败,所以我们需要加入page_template=0这样就可以避免进入这个分支!

5.参考

http://120.79.189.7/?p=578
http://hu3sky.ooo/2019/02/28/wp/
https://paper.seebug.org/822/#post-meta
https://mochazz.github.io/2019/03/01/WordPress5.0远程代码执行分析/
https://mp.weixin.qq.com/s/Yy1W8Bd75Ibis0aSD7yawg