2月20日,RIPS披露了Wordpress
内核Image
模块相关的一个高危漏洞,该漏洞由目录穿越和文件包含组成,最终可导致远程代码执行,目前还没有PoC披露。
从RIPS
描述的细节来看,漏洞出现在wordpress
编辑图片时,由于没有过滤Post Meta
值导致可以修改数据库中wp_postmeta
表的任意字段,而在加载本地服务器上的文件时没有对路径进行过滤,导致可以传递目录穿越参数,最终保存图片时可以保存至任意目录。当某个主题include了某目录下的文件时,便可以造成代码执行。
该漏洞影响4.9.9
版本以下的wordpress
程序,4.9.9
引入了过滤函数,对用户输入的post data
进行了检查,不合法的参数被过滤,主要修改如下图:
值得注意的是,在安装低版本时,安装过程中会自动更新核心文件,因此旧版本的wp-admin/includes/post.php
会更新至最新版本,所以安装过程中可以删除自动更新相关模块,或者离线安装。
漏洞出现在wordpress媒体库裁剪图片的过程,当我们上传图片到媒体库时,图片会被保存至wp-content/uploads/yyyy/mm
目录,同时会在数据库中wp_postmeta表插入两个值,分别是_wp_attached_file
和_wp_attachment_metadata
,保存了图片位置和属性相关的序列化信息。
当我们修改图片属性(例如修改标题或者说明)的时候,admin-media-Edit more details
会调用wp-admin/includes/post.php
的edit_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
的图片裁剪
功能中,有这样的功能:
wp-content\uploads\yyyy\mm
目录,则从该目录读取图片,修改尺寸后另存为一张图片;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.png
,wordpress
将裁减后的图片保存至指定的文件夹,当图片包含恶意代码被引用时,就可能造成代码执行。
图片裁剪功能在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
包含了相应的action
和context
,以及供还原文件的历史文件大小,此处需要修改action
为crop-image
以便触发wp_crop_image()
方法,相关调用如下
在wp-admin/admin-ajax.php
定义了裁剪图片的操作
判断了用户权限和action
名称后调用do_action
,最终在apply_filters()
中进入wp_crop_image()
:
进入wp_ajax_crop_image()
方法,在这个方法中进行了多项判断,全部符合才能进入裁剪图片方法,如下图注释所示
首先计算nonce
和expected
值并对比,如果不一致就验证不通过,相关方法是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
后即可造成代码执行。
见上面分析
这个漏洞主要成因在于我们可以通过参数传递任意值覆盖数据库中的字段,从而引入../
构成目录穿越,在裁剪图片后保存文件时并没有对文件目录做检查,造成目录穿越漏洞,最终可以写入恶意图片被包含或者通过Imagick
漏洞触发远程代码执行,利用链挺巧妙,值得学习。
参考: