原文链接:https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/
这篇博文讲述了利用目录穿越和本地文件包含漏洞的“组合拳”在WordPress核心功能实现远程代码执行。过去6年,该WordPress漏洞一直未被揭示。
影响
【POC视频1】
攻击者需要在目标WordPress网站拥有至少作者
级权限的账户,然后就能在后台服务器执行任意的php代码,从而远程攻陷目标。我们向WordPress安全团队报告了另一个漏洞的详情,该漏洞可以让攻击者入侵任何的WordPress站点,目前尚在修复中。
受影响版本
受一个安全补丁的限制,文中描述的漏洞在这4.9.9和5.0.1版本中无法利用。但目录穿越漏洞依然存在并且未被修补。本漏洞可利用在安装了对Post Meta记录处理不当的插件的WordPress网站上。在之前的WordPress安全月活动中,我们已经见识过一些存在问题的安装量过百万的插件。
据WordPress网站的下载页面称,全网超过33%的网站在使用WordPress。考虑到插件可能会重新引入漏洞,以及其他因素如网站WordPress版本过旧等,受影响的网站数仍然是百万级。
技术分析
目录穿越和本地文件包含漏洞都是由我公司的主打的SAST(静态应用安全测试)解决方案RIPS自动检测到的,你只需要点击按钮,它就会在3分钟内完成扫描。不过,这些bug一开始看上去无法利用。事实证明,利用这些漏洞要复杂的多,好在有可能利用。
【POC视频2】
背景知识——WordPress图片管理
当图片上传到WordPress网站时,它先被存储到上传目录(wp-content/uploads)
下。WordPress会在数据库中生成一条关于图片的引用,来记录图片属主、上传时间等元信息。这些元信息在库中以Post Meta记录的形式存在,每条记录都是键值对,并被分配一个确定ID。
如上图,已为图片分配的post_ID
号为50,如果用户以后想使用或修改ID所属的图片,WordPress将查找匹配_wp_attached_file
的元记录,并用它的值去wp-content/uploads
目录下寻找文件。
核心问题——Post Meta记录被覆盖
在WordPress4.9.9和5.0.1之前的版本中,主要问题在于可以修改任意Post Meta记录并将设置它们的值。当图片更新时(例如,它的描述发生变化)会调用edit_post()
函数。该函数直接操作$ _POST
数组。
1 function edit_post( $post_data = null ) {
2
3 if ( empty($postarr) )
4 $postarr = &$_POST;
5 ⋮
6 if ( ! empty( $postarr['meta_input'] ) ) {
7 foreach ( $postarr['meta_input'] as $field => $value ) {
8 update_post_meta( $post_ID, $field, $value );
9 }
10 }
如上所示,我们可以注入任意的Post Meta记录。因为没有关于记录修改得检查,攻击者可以更新_wp_attached_file
元记录并把它设置为任何值。这并不会重命名真实文件,但它会更改WordPress在编辑图像时要寻找的文件名。这将导致之后的目录穿越漏洞。
修改Post Meta造成目录穿越
当用户裁剪图片时会调用wp_crop_image()
函数,目录穿越漏洞就发生在这里。
该函数获取要裁剪图片的ID($attachment_id
)并从数据库中提取对应的_wp_attached_file
Post Meta记录。
还记得么,由于edit_post()
函数的缺陷,$src_file
可以被设定成任意值。
1 function wp_crop_image( $attachment_id, $src_x, ...) {
2
3 $src_file = $file = get_post_meta( $attachment_id, '_wp_attached_file' );
4 ⋮
接下来,WordPress必须确保图片实际存在并加载它。 WordPress有两种加载指定图片的方法。第一种是简单地查找wp-content/uploads
目录中的由_wp_attached_file
Post Meta记录提供的文件名(下一代码段的第2行)。
如果这一方法失败,WordPress将尝试从其自己的服务器下载图片以作备用。为此,它将生成一个下载URL,该URL由wp-content/uploads
目录的URL和存储在_wp_attached_file
Post Meta记录中的文件名构成(下一代码段的第6行)。
举个栗子:如果存储在_wp_attached_file
Post Meta记录中的值是evil.jpg,那么WordPress将首先尝试检查文件wp-content/uploads/evil.jpg
是否存在。如果不存在,它将从以下URL下载该文件:https://targetserver.com/wp-content/uploads/evil.jpg
。
之所以选择下载图片而不是在本地查找,是因为有时某些插件会在用户访问URL时动态生成图像。
请注意,这里没有任何过滤。WordPress将简单地拼接上传目录的基础URL和用户输入的$src_file
。
一旦WordPress通过wp_get_image_editor()
函数成功加载了有效图片,它便开始裁剪图片。
1 ⋮
2 if ( ! file_exists( "wp-content/uploads/" . $src_file ) ) {
3 // If the file doesn't exist, attempt a URL fopen on the src link.
4 // This can occur with certain file replication plugins.
5 $uploads = wp_get_upload_dir();
6 $src = $uploads['baseurl'] . "/" . $src_file;
7 } else {
8 $src = "wp-content/uploads/" . $src_file;
9 }
10
11 $editor = wp_get_image_editor( $src );
12 ⋮
接着,裁剪后的图片会被保存回文件系统(无论是下载的还是本地的)。生成的文件名是get_post_meta()
返回的$src_file
的值,它正好可以由攻击者控制。生成的文件名由一条规则生成:在文件的原始名称前添加cropped-
(下一代码段的第4行)。在本例中,生成的文件名为cropped-evil.jpg
。
然后WordPress就会通过wp_mkdir_p()
函数(第6行)在结果路径中创建任何之前不存在的目录。
最后,它使用图像编辑器对象的save()
方法将其写入文件系统。save()
方法也不对给定的文件名做目录穿越漏洞检查。
1 ⋮
2 $src = $editor->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs );
3
4 $dst_file = str_replace( basename( $src_file ), 'cropped-' . basename( $src_file ), $src_file );
5
6 wp_mkdir_p( dirname( $dst_file ) );
7
8 $result = $editor->save( $dst_file );
利用思路
到目前为止,我们发现由于没有做检查,可以确定哪个文件被加载到图像编辑器中。但是,如果文件不是张有效图像,图像编辑器对象将抛出异常。这里可以假设:WordPress只能裁剪上传目录之外的图片。
但是,如果WordPress未找到图片,会尝试下载图片,将会导致远程执行代码漏洞。
本地文件 | HTTP下载 | |
---|---|---|
已上传文件 | evil.jpg | evil.jpg |
_wp_attached_file | evil.jpg?shell.php | evil.jpg?shell.php |
加载时结果文件 | wp-content/uploads/evil.jpg?shell.php | https://targetserver.com/wp-content/uploads/evil.jpg?shell.php |
实际位置 | wp-content/uploads/evil.jpg | https://targetserver.com/wp-content/uploads/evil.jpg |
生成的文件名 | None-image loading fails | evil.jpg?cropped-shell.php |
我们的思路是将_wp_attached_file
设置为evil.jpg?shell.php
,这将导致对以下URL发出HTTP请求:https://targetserver.com/wp-content/uploads/evil.jpg?shell.php
,这个请求会返回一个有效的图像文件。因为在此处的上下文中,文件名?
之后的部分会被忽略。生成的文件名将是evil.jpg?shell.php
。
图像编辑器的save()
方法虽然不会检查目录穿越漏洞,但它会添加加载图片的mime类型的扩展名到生成的文件名中。在本例中,生成的文件名是evil.jpg?cropped-shell.php.jpg
。这使得新创建的文件再次无法利用。
但是,仍然可以使用诸如evil.jpg?/../../evil.jpg
之类的有效攻击载荷将生成的图像植入任何目录。
在主题目录下利用目录穿越&LFI
每个WordPress主题都是wp-content/themes
目录下的一个目录,并为不同的情形提供模板。例如,如果博客的访问者想看帖子,WordPress会在当前活动主题的目录中查找post.php
文件。如果该PHP文件找到了模板,就会include()
它。
WordPress中允许为某些帖子选择自定义模板。用户需要将数据库中的_wp_page_template
Post Meta记录设定为特定的文件名。这里的唯一限制是被include()
的文件必须位于当前活动主题的目录中。
通常,该目录无法访问,也无法上载文件。但是,通过利用上述的目录穿越漏洞,可以将恶意制作的图像植入到当前使用的主题的目录中。然后,攻击者可以利用同一漏洞创建一个新帖子,使他能够更新_wp_attached_file
的Post Meta记录来include()
图像。通过将PHP代码注入图片数据中,攻击者可以拿到远程执行代码漏洞。
构造恶意图片——GD和Imagick
WordPress的PHP支持两种图像编辑扩展:GD和Imagick。它们的区别在于Imagick不会删除图像的exif元数据,而exif中可以存储PHP代码。 GD会压缩它编辑的每个图像并删除所有exif元数据。
但是,通过精心构造图片的像素仍然可以实现利用。这些像素将在GD处理完图像后以某种方式翻转,最终执行PHP代码。在研究PHP GD扩展的内部结构时,我们在libgd中发现了可利用的内存损坏漏洞。(CVE-2019-69772)
时间线
日期 | 事件 |
---|---|
2018/10/16 | 在Hackerone上向WordPress安全团队报告了漏洞。 |
2018/10/18 | WordPress安全团队成员确认该报告,并表示他们将在报告得到验证后返回。 |
2018/10/19 | 另一位WordPress安全团队成员要求提供更多信息。 |
2018/10/22 | 我们为WordPress提供了更多信息,并提供了完整的270行漏洞利用脚本来帮助验证漏洞, |
2018/11/15 | WordPress对漏洞进行了分类,并表示他们能够复制漏洞。 |
2018/12/06 | WordPress 5.0已发布,没有针对该漏洞的补丁。 |
2018/12/12 | WordPress 5.0.1已发布,是一个安全更新。其中一个补丁通过阻止攻击者设置任意设置元记录,使漏洞无法利用。但是,目录穿越仍然可以使用,如果安装了错误处理Post Meta记录的插件,则可以利用它。 WordPress 5.0.1不解决路径遍历或本地文件包含漏洞。 |
2018/12/19 | WordPress 5.0.2发布了。没有针对漏洞的补丁。 |
2019/01/09 | WordPress 5.0.3发布了。没有针对漏洞的补丁。 |
2019/01/28 | 我们要求WordPress提供下一个安全版本的ETA,以方便我们安排博客日程,并在WordPress新版本发布后发表博客。 |
2019/02/14 | WordPress提出了一个补丁。 |
2019/02/14 | 我们提供有关补丁的反馈,并验证它是否可以防止利用。 |
总结
本文详细介绍了WordPress核心功能中的远程执行代码,该漏洞已存在超过6年。根据RIPS的报告,在5.0.1和4.9.9版本中由于另一个漏洞的补丁,它变得不可利用。但是,目录穿越仍然可以使用,如果安装了仍允许任意覆盖Post Data的插件,则可以利用该漏洞。由于在攻击目标WordPress站点时需要通过身份认证,因此我们决定在首次报告漏洞的4个月后公开此漏洞。
感谢WordPress安全团队的志愿者,他们在这个问题上与我们合作得十分友好,也很专业。