wordpress 5.0.0 RCE分析与复现
0x00 概述
RIPS于2月20日在博客上披露了一个关于WordPress 5.0.0的远程代码执行漏洞。此漏洞由Post Meta变量覆盖、目录穿越写文件、模板包含组合后构成远程代码执行漏洞。
1.漏洞触发条件
(1)存在漏洞的wordpress版本如下:
WordPress commit \<= 43bdb0e193955145a5ab1137890bb798bce5f0d2 (WordPress 5.1-alpha-44280)
(2)需要作者权限的账号
2.漏洞的影响范围
受影响的wordpress版本为:
1.WordPress 5.1-alpha-44280更新后
2.未更新的4.9.9~5.0.0的WordPress
服务端:windows、linux、mac
图片处理库:gd/imagick
0x01 环境配置
wordpress在某个版本以后,就增加了自动升级小版本的功能。所以安装好wp以后,需要手工在wp-config.php中加个define('AUTOMATIC_UPDATER_DISABLED',true);
,禁止其自动更新。
代码下载最好到对应的wordpress网站下载,因为官方github release版本都被patch了。此处的分析采用的是从wordpress中文网下载的4.9.4版本代码。
本文的环境搭建:macOS+php7+wordpress4.9.4+imagick6.9.7
0x02 漏洞分析
1.Post Meta变量覆盖
此处的漏洞点出现在wordpress 媒体功能在更新被编辑的图片处,我们上传图片之后,图片的保存路径是wp-content/uploads/years/month
,同时会在数据库的wp_postmeta
表中_wp_attached_file
和_wp_attachment_metadata
插入对应的值,第一个值是图片的路径+图片名,第二个是图片的相关信息被序列化后的值,具体信息如下图所示。
接下来我们定位到此处的漏洞点,在编辑并更新图片的时候会调用edit_post()函数,wp-admin/includes/post.php:187
从中可以看到该方法的参数来自$_POST
,并且此处也没有任何的过滤,在赋值给$post_data
之后,被带入到wp_update_post()
函数。我们动态调试跟一下这个函数的调用栈。首先我们直接让程序运行到update_post_meta()
这个函数,这个函数根据$post_ID
修改post meta field
,接着调用update_metadata()
更新meta数据,完成之后更新post数据。但是在此处并没有对post的数据进行过滤,我们的$post_data["meta_input"]["_wp_attached_file"]
的值也没有被过滤掉。
我们继续跟进wp_update_post()
函数,点击步进后进入wp_update_post()
函数,wp-includes/post.php:3611
我们可以看到在这个函数的末尾处,如果post_type的值是attachment类型就会调用wp_insert_attachment()
函数,wp-includes/post.php:4898。
我们继续步进跟一下这个函数,可以看到它接着调用了wp_insert_post()
函数,wp-includes/post.php:3044
在第3434行可以看到对于meta_input参数,此处遍历传入update_post_meta()
函数,我们继续跟进。update_post_meta()
函数,wp-includes/post.php:1799
发现其中调用了update_metadata()
函数来做进一步的处理,跟进后发现update_metadata()
使用来将数据库中对应的键值进行更新操作,而且在这个过程中没有对meta_input
的值做任何的过滤,所以我们可以传入指定的 key 来设置它的值。
在此我们回顾一下这个调用栈
因此我们构造POST数据包就可以覆盖掉_wp_attached_file
的值,覆盖效果及操作如下所示:1
2#将这个附加到POST数据后
&meta_input[_wp_attached_file]=2019/03/z3r0yu.jpg?/../../../../themes/z3r0yu.jpg
到此为止第处漏洞点的分析利用已经完成,漏洞触发最重要原因就是没有做好过滤,因此该处的补丁为:1
array_diff_key( $post_data, array_flip( array( 'meta_input', 'file', 'guid' ) ) );
从补丁上可以看到对meta_input
做了过滤(PS:你下载的代码中要是出现这个就说明是被patch过了)。
2.目录穿越造成文件写入
该问题是建立在之前我们Post Meta变量覆盖漏洞之上的。因为在之前我们是可以在_wp_attached_file
处写入任意值的,因此只需要一个方法将我们写入的值进行利用一下就好。
在wordpress的图片裁剪功能中,可以实现本地文件读取和远程文件读取。但是远程文件读取这个位置很有意思,如果目标图片在该目录不存在,则通过本地服务器下载该图片,如从http://127.0.0.1/wp494/wp-content/uploads/2019/03/2233.jpg
下载,裁剪后重新保存。此处我们可以构造一个带参数的url,比如http://127.0.0.1/wp494/wp-content/uploads/2019/03/2233.jpg?z3r0yu
,在远程读取的时候会忽略?号之后的内容,从而只对2233.jpg进行剪切后保存。目录穿越问题就存在于此,如果我们构造如下所示的urlhttp://127.0.0.1/wp494/wp-content/uploads/2019/03/2233.jpg?/../../../../themes/twentysixteen/z3r0yu.jpg
,wordpress将裁减后的图片保存至wp-content/themes/twentysixteen/
目录下,如果图片中包含恶意代码就可能被进一步的利用。
编辑图片会先调用do_action
通过apply_filters()
函数进入wp_ajax_crop_image()
函数,在wp-admin/includes/ajax-actions.php:3224
在函数中首先会调用check_ajax_referer()
函数来对用户的权限进行校验,之后调用absint()
函数将$_POST['cropDetails']
的值转换为非负值,之后将参数传入wp_crop_image()
函数对图片进行剪裁操作,wp-admin/includes/image.php:25。(PS:你如果想要动态调试经过这一步就要满足上面那些条件)
从数据库取出_wp_attached_file
后并没有任何过滤,所以我们之前设置的值在此处已经是完好的,如下图所示。
只有依据_wp_attached_file
的值做了判断,发现文件不存在开始去调用_load_image_to_edit_path()
函数,我们进行跟进,wp-admin/includes/image.php:649
继续跟进后发现调用了wp_get_attachment_url()
来拼接url链接。
之后上面拼接的url链接传输到wp_get_image_editor()
函数中,我们步进跟一下这个函数,wp-includes/media.php:2900
跟进后发现其中调用了_wp_image_editor_choose()
函数,继续跟进,wp-includes/media.php:2950
从这里可以看到Wordpress提供了两种方式来处理图片,优先使用Imagick,之后是GD。我们在此要特别注意,这俩对图片的处理方式是不同的:
- Imagick不会去除掉图片中的exif部分,所以我们可以将待执行payload代码加入到exif部分。
- GD会去除图片的exif部分,并且其中的phpcode很难存活。除非通过精心构造一张图片才可以。
我在此只谈复现分析,暂时不谈对图片的Fuzz,因此选择Imagick库。
PS:Imagick处理类的load函数中调用的是readImage函数,但在高版本的Imagick上该函数不支持远程图片链接,因此最好采用Imagick-6.9.7及其以下版本。
完成图片剪裁后就再次进入wp_crop_image()
函数中,$dst_file
的值是文件名,因此最终路径如下图所示:
之后就是未经任何过滤进入到wp_mkdir_p()
函数来创建目录,我们继续跟进后发现其中也没有任何过滤,直接执行到mkdir()
进行目录创建,此时$target
的值如下所示:
1 | $target:"/Applications/XAMPP/xamppfiles/htdocs/wp494/wp-content/uploads/2019/03/z3r0yu.jpg?/../../../../themes" |
创建完路径之后,调用save()
对图片进行保存,我们单步跟进save()
函数,wp-includes/class-wp-image-editor-gd.php:364
在save()
中调用了make_image()
函数,继续跟进到wp-includes/class-wp-image-editor.php:394
此处会用call_user_func_array
函数来调用Imagick的writeImage
函数,并将$filename
传递进去,但是在Linux平台上此函数是不支持不存在的目录跳转的。我们的z3r0yu.jpg?/
在这里就是不存在目录,这个函数如果被调用就会抛出错误,从而无法达到任意写的目的。如果想进行绕过只需要多次上传裁剪就可以。
接下来我们对此部分进行利用,我们接着Post Meta变量覆盖的利用之后进行,使用wordpress的剪切功能并在剪切完保存图片的时候进行burp抓包并将要POST的数据修改如下(PS:_ajax_nonce和id的值要与之前保持一致):1
action=crop-image&_ajax_nonce=0810f2d564&id=10&cropDetails[x1]=10&cropDetails[y1]=10&cropDetails[width]=10&cropDetails[height]=10&cropDetails[dst_width]=100&cropDetails[dst_height]=100
最终会在我们指定的目录下生成剪裁后的图片
既然可以成功在指定目录下写入图片文件,那么我们完全可以构造一个包含有shell的图片。但是文件只能是jpg,所以我们还需要结合本地文件包含做进一步的利用。
小结:此部分我们将POST的数据进行了修改,目的是修改action
为crop-image
达到触发存在漏洞的wp_crop_image()
函数。
3.本地文件包含(模板功能)
之前的工作我们已经达到了任意文件写入的目的,如果想对图片中的代码进行利用,我们必须结合本地文件包含漏洞。此处我们已经预先知道了文件包含点在wordpress的模板位置,它会根据需要加载的页面类型从当前主题下选择需要的模板,如果存在就会被包含。因此我们在此处主要关注与模板包含相关的函数。
之前我在数据库中关注到一个_wp_page_template
字段,而在wordpress中模板文件位置就是存储在数据库中。详查一下发现加载页面所需要的模板文件存储就在wp_postmeta数据库中的_wp_page_template
,这个值默认是default
。
所以我们先查看一下这个值的使用位置,可以看到在get_post_meta()
函数中对这个值进行了取出,wp-includes/post-template.php:1683。
继续全局检索一些get_page_template_slug()
函数的引用位置,可以看到在与模板相关的wp-includes/template.php文件中进行了调用。
继续看代码,发现只有两个函数对get_page_template_slug()
进行了调用,第一个是get_page_template()
函数,wp-includes/template.php:405;第二个是get_single_template()
函数,wp-includes/template.php:481
我们继续看一下get_single_template()
函数,看到在 get_page_template_slug()
取值之后赋值给了$template
,之后$template
经过简单的判断后赋值给$templates[]
,最终$templates
变量传入get_query_template()
函数,我们继续跟进一下个函数,wp-includes/template.php:23
从这段代码中我们可以看到$templates
变量的值又传入了locate_template()
函数,继续跟进这个函数,wp-includes/template.php:629
从代码中可以看到可控的变量$template_name
值经过拼接和判断处理,因此我们结合之前的目录穿越造成的任意文件写入问题,我们需要将新生成的图片放到theme-compat
目录下。
分析完加载的路径上的文件是我们可控之后,我们查看下载何处调用了get_single_template()
函数,并且对其返回的变量做了何种处理。从下图中的代码,我们可以看到在74行使用include
对$template
变量返回的值进行了包含,从而可以造成任意代码执行。
0x03 漏洞复现
首先上传一张图片,点击更新按钮抓包
在数据包中添加如下信息1
&meta_input[_wp_attached_file]=2019/03/2233.jpg?/../../../../themes/twentysixteen/z3r0yu.jpg
可以看到数据库中已经成功保存了我们设置的值
接下来对图片进行剪裁,并抓包
修改数据包内容如下1
action=crop-image&_ajax_nonce=1cc3d57951&id=15&cropDetails[x1]=10&cropDetails[y1]=10&cropDetails[width]=10&cropDetails[height]=10&cropDetails[dst_width]=100&cropDetails[dst_height]=100
最终我们可以看到图片已经在对应的路径下了
接下来进行文件包含,我们选择上传一个rce.txt,然后再次修改信息,与最初的方式一样,此处加上如下的键值1
&meta_input[_wp_page_template]=cropped-z3r0yu.jpg
可以看懂数据库中已经对应的值已经修改
最终成功RCE
(PS:此处有个小坑,如果你要是访问附件出现了404,那么设置一下固定连接即可)
0x04 总结
这个漏洞的分析和环境搭建的过程中坑点不少,先对坑点做一个小结:
- wordpress自动更新处理,在wp-config.php中加如一行代码
define('AUTOMATIC_UPDATER_DISABLED',true);
来禁止更新。 - Imagick和GD对图片的处理方式不同的问题,对GD的利用需要Fuzz出paylaod,Imagick直接修改exif部分即可。
- Windows 下的目录不能含有?,因此最好采用#。
- Linux下由于xxx.jpg#是个不存在的目录,因此调用Imagick的
writeImage
函数会调用失败抛出错误终止流程,进而无法达成第二个漏洞的利用,但是看到balisong师傅借助多次上传裁剪来绕过这个坑点(目前笔者还未成功对此测试成功)。 - 官网的所以release版本都修复了这个漏洞。
- 固定连接设置问题,默认配置下查看附件会出现404。
漏洞构成思路总结:首先是一个变量覆盖,将我们需要的../
引入数据库;之后是一个剪裁图片功能未对变量内容进行审查造成目录穿越写文件;最后是模板参数处理过程中的一个本地文件包含漏洞,最终构成RCE。每个漏洞独立出来危害都极低,但是组合后却可以导致RCE的出现,此攻击链的构造十分精妙。
0x05 参考
《WordPress 5.0.0 Remote Code Execution》–Simon Scannell
《WordPress 5.0 RCE 详细分析》–LoRexxar’
《Wordpress 5.0.0远程代码执行漏洞分析与复现》–balisong
《WORDPRESS IMAGE 远程代码执行漏洞分析》–诗与胡说
《Wordpress \< 4.1.2 存储型XSS分析与稳定POC》–phithon