0x01 概述

2月20日,RIPS披露了Wordpress内核Image模块相关的一个高危漏洞,该漏洞由目录穿越和文件包含组成,最终可导致远程代码执行,目前还没有PoC披露。

RIPS描述的细节来看,漏洞出现在wordpress编辑图片时,由于没有过滤Post Meta 值导致可以修改数据库中wp_postmeta表的任意字段,而在加载本地服务器上的文件时没有对路径进行过滤,导致可以传递目录穿越参数,最终保存图片时可以保存至任意目录。当某个主题include了某目录下的文件时,便可以造成代码执行。

0x02 环境搭建

该漏洞影响4.9.9版本以下的wordpress程序,4.9.9引入了过滤函数,对用户输入的post data进行了检查,不合法的参数被过滤,主要修改如下图:

值得注意的是,在安装低版本时,安装过程中会自动更新核心文件,因此旧版本的wp-admin/includes/post.php会更新至最新版本,所以安装过程中可以删除自动更新相关模块,或者离线安装。

0x03 漏洞分析

漏洞一:数据覆盖

漏洞出现在wordpress媒体库裁剪图片的过程,当我们上传图片到媒体库时,图片会被保存至wp-content/uploads/yyyy/mm目录,同时会在数据库中wp_postmeta表插入两个值,分别是_wp_attached_file_wp_attachment_metadata,保存了图片位置和属性相关的序列化信息。

当我们修改图片属性(例如修改标题或者说明)的时候,admin-media-Edit more details 会调用wp-admin/includes/post.phpedit_post()方法,该方法的参数全部来自于$_POST,没有进行过滤

然后会调用到update_post_meta()方法,该方法根据$post_ID修改post meta field,接着调用update_metadata()更新meta数据,完成之后更新post数据,调用wp_update_post()方法

wp_update_post()方法中,如果post_type=attachment,则进入wp_insert_attachment(),接着调用wp_insert_post(),在wp_insert_post()方法中判断了meta_input参数,如果传入了该参数,就遍历数组用来更新post_meta

进入update_post_meta(),调用update_metadata(),在update_metadata()方法中对数据库进行更新操作,而在整个过程中对键值没有任何过滤,意味着我们可以传入指定的key来设置它的值,调用栈如下图所示

于是构造数据包更新数据库中_wp_attached_file的值,插入一个包含../的值,以便在下面触发目录遍历。

这是第一个漏洞——通过参数覆盖了数据库数据,在补丁处正是对meta_input这个参数做了过滤,如果包含则通过对比array舍弃该参数。

漏洞二:目录遍历

接着寻找一个获取_wp_attached_file的值并进行了文件操作相关的方法。

wordpress图片裁剪功能中,有这样的功能:

  1. 图片存在于wp-content\uploads\yyyy\mm目录,则从该目录读取图片,修改尺寸后另存为一张图片;
  2. 如果图片在该目录不存在,则通过本地服务器下载该图片,如从http://127.0.0.1/wordpress/wp-content/uploads/2019/02/admin.jpeg下载,裁剪后重新保存。

这个功能是为了方便一些插件动态加载图片时使用。

然而因为本地读取和通过url读取的差异性,导致可以构造一个带参数的url,如http://127.0.0.1/wordpress/wp-content/uploads/2019/02/admin.jpeg?1.png,在本地读取时会发现找不到admin.jpeg?1.png,而远程获取时会忽略?后面的参数部分,照样获取到admin.jpeg,裁剪后保存。如果构造的url包含路径穿越,例如http://127.0.0.1/wordpress/wp-content/uploads/2019/02/admin.jpeg?../../1/1.pngwordpress将裁减后的图片保存至指定的文件夹,当图片包含恶意代码被引用时,就可能造成代码执行。

图片裁剪功能在wp_crop_image()方法中,但是该方法不能在页面中触发,需要手动更改相应的action

首先在页面裁剪图片,并点击保存

抓取数据包:

action=image-editor&_ajax_nonce=4c354c778b&postid=5&history=%5B%7B%22c%22%3A%7B%22x%22%3A0%2C%22y%22%3A5%2C%22w%22%3A347%2C%22h%22%3A335%7D%7D%5D&target=all&context=edit-attachment&do=save

post body包含了相应的actioncontext,以及供还原文件的历史文件大小,此处需要修改actioncrop-image以便触发wp_crop_image()方法,相关调用如下

wp-admin/admin-ajax.php定义了裁剪图片的操作

判断了用户权限和action名称后调用do_action,最终在apply_filters()中进入wp_crop_image():

进入wp_ajax_crop_image()方法,在这个方法中进行了多项判断,全部符合才能进入裁剪图片方法,如下图注释所示

首先计算nonceexpected值并对比,如果不一致就验证不通过,相关方法是check_ajax_referer()-->wp_verify_nonce()。注意到传入check_ajax_referer()$attachment_id参数,该参数取自$_POST['id'],并参与后面的expected计算,因此当我们直接更改action=crop-image是无法通过校验的,需要传入id的,即为postid的值。

在进入wp_crop_image()时还需要传递裁剪后的图片宽度和高度信息,所以还需要增加cropDetails[dst_width]cropDetails[dst_height]两个参数。

wp_crop_image()方法如下

从数据库取出_wp_attached_file后并没有做检查,形如2019/02/admin.jpeg?../../1.png的文件无法被找到,于是进入_load_image_to_edit_path()通过wp_get_attachment_url()方法生成本地url

随后实例化一个WP_Image_Editor用来裁剪并生成裁剪后的图片,之后调用wp_mkdir_p()方法创建文件夹,含有../的参数进入该方法后同样没有经过过滤,最终执行到mkdir创建文件夹

mkdir( $target, $dir_perms, true)

此时的target值是这个样子,穿越目录后在2019目录下创建1文件夹,并生成cropped-1.png文件

D:\phpStudy\PHPTutorial\WWW\wordpress-4.9.8/wp-content/uploads/2019/02/admin.jpeg?../../../1

注意:此处有一个坑,我们观察上面的url,在mkdir的时候会把admin.jpeg?../作为一个目录,而在Windows下的目录不能出现?,所以上面的payload在Windows下无法成功,经过测试,#可以存在于Windows目录,因此在Windows下的payload如下所示:

meta_input[_wp_attached_file]=2019/02/admin.jpeg#../../../1/1.png

写入数据库中即为2019/02/admin.jpeg#../../../1/1.png

最终构造第二个数据包触发裁剪图片并保存:

最终在指定目录下生成裁剪后的图片文件,以cropped-作为前缀

这样子我们可以制作一张图片马,在主题文件夹下生成,或者指定任意目录,被include后即可造成代码执行。

0x04 PoC

见上面分析

0x05 总结

这个漏洞主要成因在于我们可以通过参数传递任意值覆盖数据库中的字段,从而引入../构成目录穿越,在裁剪图片后保存文件时并没有对文件目录做检查,造成目录穿越漏洞,最终可以写入恶意图片被包含或者通过Imagick漏洞触发远程代码执行,利用链挺巧妙,值得学习。

参考:

源链接

Hacking more

...