WordPress Remote Code Execution 分析

2019-03-19 13,976

2月19日,Rips披露了一个已经在WordPress核心代码中存达六年之久的远程代码执行漏洞。(https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/)

该漏洞的利用十分巧妙,所需权限不高, Author权限即可,包括数据注入,目录穿越,文件包含三部分技术,形成组合拳完成整个利用链。


漏洞概述

漏洞版本


Rips的披露中仅提到5.0.0,但是Wordpress版本众多,实际分析过程中发现 5.1.x, 5.0.x, 4.9.x旧版本均存在问题,请尽量以是否包含 _wp_get_allowed_postdata过滤函数确定存在漏洞。


漏洞构成


  • postmeta表数据注入。Wordpress没有对POST参数 meta_input进行严格过滤,可以通过该参数对数据库中的postmeta表进行数据注入,控制表中行记录的 meta_key和 meta_value。

  • 恶意图片目录穿越。利用Wordpress的图片裁剪功能,加上postmeta表数据注入设置恶意路径(控制 meta_key为 _wp_attached_file时 meta_value),让包含恶意代码的图片穿越到指定目录。

  • 模板文件包含。利用Wordpress的模板包含模块,加上postmeta表数据注入设置被包含名(控制 meta_key为 _wp_page_template时 meta_value),包含恶意图片完成代码执行。


漏洞分析


分析环境:

 macos+wordpress5.1.x+ImageMagick6.9.6

WordPress在安装后会自动更新补丁,断网安装后并在 wp-config.php中添加禁止更新的代码:

define('AUTOMATIC_UPDATER_DISABLED',true);

漏洞还需要有个具有 Author权限的账号,WordPress 默认的用户角色:

  • 管理员 -拥有所有的管理权限

  • 编辑 -发表文章,编辑文章,以及编辑其他人的文章,等等。

  • 作者-发布和编辑自己的文章

  • 投稿者 -撰写和编辑自己的文章,但不能发布

  • 订阅者 -查看评论/添加评论/查看文章,等等。

WordPress常被作为门户网站,因此 Author权限的账号并不苛刻。


postmeta数据注入

操作

在媒体库中上传文件,然后更新图片操作

Burp拦截数据包,在post实体中添加payload:

meta_input[_wp_attached_file]=rai4over.jpg

看数据库中的 wp_postmate表,数据注入成功。


分析

post请求中包含 action=editpost,进入 editpost分支。

文件路径: WordPress/wp-admin/post.php

进edit_post函数。

文件路径: 

WordPress/wp-admin/includes/post.php

函数中将 $_POST赋值给 $post_data,然后又将的 $post_data值传入 wp_update_post函数。

进 wp_update_post函数

文件路径: 

WordPress/wp-includes/post.php

函数的返回处又调用了 wp_insert_attachment函数,继续跟进。

文件位置:

WordPress/wp-includes/post.php

函数的返回处又调用了 wp_insert_post函数,继续跟进。

文件位置:

WordPress/wp-includes/post.php

函数中此处迭代 $postarr['meta_input'],也就是 $_POST['meta_input'],然后分别传入 update_post_meta函数。

传入函数的参数变量均可控, $meta_key就是post中 meta_input的键名 _wp_attached_file, $meta_value就是post中 meta_input的键值 rai4over.jpg。

update_post_meta函数根据 $post_id, $meta_key, $meta_value插入/修改 postmate表中的记录,完成数据注入,控制表中行记录的 meta_key和 meta_value。


图片目录穿越

操作

修改上一步的payload,重新进行postmeta 数据注入:

meta_input[_wp_attached_file]=2019/03/shell.jpg#/../../../../themes/twentynineteen/rai4over.jpg

留原始POST请求实体中的 _ajax_nonce和 id的值,并修改到新的POST请求实体中。

action=crop-image&_ajax_nonce=c880cff551&id=58&cropDetails[x1]=10&cropDetails[y1]=10&cropDetails[width]=10&cropDetails[height]=10&cropDetails[dst_width]=100&cropDetails[dst_height]=100

可完成目录穿越。

分析

Payload:

meta_input[_wp_attached_file]=2019/03/shell.jpg#/../../../../themes/twentynineteen/rai4over.jpg

Payload前部分为上传图片附件的URL,可以在附件详情查看:

部分利用 ../跳转到穿越目录,并指定文件名。

发送穿越目录的请求,请求中包含 action=crop-image,跟进文件分析。

文件位置:

WordPress/wp-admin/admin-ajax.php

传进 do_action函数,一路跟进至关键函数 wp_ajax_crop_image。

文件位置: 

WordPress/wp-admin/includes/ajax-actions.php

_POST['cropDetails']可控,将 $_POST['cropDetails']中的数据经过 absint后赋给 $data,然后 $data和 $attachment_id作为参数传入 wp_crop_image,跟进该函数。

文件位置: 

WordPress/wp-admin/includes/image.php

代表id的 $src传入 get_attached_file函数,跟进函数。

文件位置: 

WordPress/wp-includes/post.php

用get_post_meta函数,并传入$attachment_id, _wp_attached_file,继续跟进。

这里get_post_meta函数根据附件id $attachment_id,取出 meta_key为 _wp_attached_file的 meta_value,也就是上文数据注入的payload:

2019/03/shell.jpg#/../../../../themes/twentynineteen/rai4over.jpg

结果返回到 get_attached_file函数,然后拼接成为文件绝对路径,再层层返回到 wp_crop_image函数。

wp_crop_image函数根据文件绝对路径进行判断文件是否存在。

/Applications/MAMP/htdocs/WordPress/wp-content/uploads/2019/03/shell.jpg#/../../../../themes/twentynineteen/rai4over.jpg


不存在就调用 _load_image_to_edit_path函数,继续跟进。

文件位置: 

WordPress/wp-admin/includes/image.php

同样获取文件绝对路径,不存在就进入第二个 elseif分支,调用进入 wp_get_attachment_url函数,继续跟进。

文件位置:

WordPress/wp-includes/post.php

拼接形成附件的url并返回,附件url为:

http://127.0.0.1/WordPress/wp-content/uploads/2019/03/shell.jpg#/../../../../themes/twentynineteen/rai4over.jpg

url结果层层返回到 wp_crop_image函数

url作为参数传入 wp_get_image_editor函数,继续跟进。

文件位置:

WordPress/wp-includes/media.php

用 _wp_image_editor_choose函数,继续跟进。

wp_image_editor_choose函数用来选择处理图片的模块,优先选择 Imagick,然后是 GD。

  • imagick不会去除掉图片中的exif,可以使用exiftool将恶意代码加入Comment。

  • GD会去除图片的exif部分,需要费心构造。

两种模块的利用方法并不相同,但这里根据环境使用 imagick模块,返回 imagick处理对象。

imagick处理实例调用 load方法加载url图片, #使后面被看作书签,url能正确加载为:

http://127.0.0.1/WordPress/wp-content/uploads/2019/03/shell.jpg

加载后返回处理实例。

返回后对文件进行剪裁,确定剪裁后文件的名称 $dst_file,绝对路径为:

/Applications/MAMP/htdocs/WordPress/wp-content/uploads/2019/03/shell.jpg#/../../../../themes/twentynineteen/cropped-rai4over.jpg

然后传入 wp_mkdir_p函数,继续跟进。

文件位置:

WordPress/wp-includes/functions.php

该函数用于创建文件绝对路径的目录, shell.jpg#是一个不存在的目录, mkdir在linux下是支持不存在的目录通过 ../跳转的,因此创建的目录相当于:

/Applications/MAMP/htdocs/WordPress/wp-content/themes/twentynineteen

这个目录是wordpress原生存在的,就不需要创建,函数返回至 wp_crop_image,进入 $editor->save,保存剪裁后的图片到 $dst_file路径。

文件位置:

WordPress/wp-includes/class-wp-image-editor-imagick.php

继续跟进 $this->_save。

继续跟进 $this->make_image。

文件位置: 

WordPress/wp-includes/class-wp-image-editor.php

这里同样先创建文件目录,和上面步骤一样存在,然后利用 call_user_func_array函数执行 Imagick::writeImage进行图片写入,此时的路径为:

/Applications/MAMP/htdocs/WordPress/wp-content/uploads/2019/03/shell.jpg#/../../../../themes/twentynineteen/cropped-rai4over.jpg

shell.jpg#是一个不存在的目录,但是 Imagick::writeImage在linux下是不支持不存在的目录通过 ../跳转的,因此这里写入图片流程会报错。

可以发现前面会调用 wp_mkdir_p函数进行创建文件夹,利用此函数创建 shell.jpg#目录。(Windows可以省略次此步骤)

修改第一步postmeta注入的payload,修改数据库内容:

meta_input[_wp_attached_file]=2019/03/shell.jpg#/rai4over.jpg

再次发送目录穿越的请求, shell.jpg#目录创建成功,可以利用 ../调转到需要的目录。

再次修改第一步postmeta注入的payload,修改数据库内容:

meta_input[_wp_attached_file]=2019/03/shell.jpg#/../../../../themes/twentynineteen/rai4over.jpg

再次发送目录穿越的请求,成功传送裁剪图片到指定目录。


模板文件包含

操作

上传txt文件, 编辑按钮

更新按钮

Burp拦截数据包,在post实体中添加payload:

meta_input[_wp_page_template]=cropped-rai4over.jpg&page_template=0

Postmeta数据注入,注入键名为 _wp_page_template,查看数据库中的 wp_postmate表,数据注入成功。

触发模板包含

RCE成功

分析

Postmeta数据注入操作中调用了 wp_update_post函数。

文件位置:

WordPress/wp-includes/post.php

将id传给 get_post函数,该函数根据id获取数据库中的字段信息,返回给 $post。

然后 $post和 $postarr进行数组合并到 $postarr,如果 $postarr和 $post中原本有相同的键名,那么 $postarr将会覆盖,此时 $postarr就是 $_POST,可控变量,传入 wp_insert_attachment,然后一路跟进 wp_insert_post函数。

 if ( ! empty( $postarr['meta_input'] ) ) {        foreach ( $postarr['meta_input'] as $field => $value ) {            update_post_meta( $post_ID, $field, $value );        }    }    .....................    .....................    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注入数据库后,还判断了 $postarr['page_template'],如果不为空,数据库中 _wp_page_template就再次修改为 default。

post的payload中添加 page_template=0,在上一步数组合并的时候覆盖,此时就能不走此分支,数据库就不会被再更改,更加方便调试,数据注入完成。

包含位置: 

WordPress/wp-includes/template-loader.php

基本条件判断Tag

  • is_home() :是否为主页

  • is_single() :是否为内容页(Post)

  • is_page() :是否为内容页(Page)

  • is_category() :是否为Category/Archive页

  • is_tag() :是否为Tag存档页

  • ………………...

进入 get_single_template函数,跟进。

文件位置: 

WordPress/wp-includes/template.php

进入 get_page_template_slug函数

文件位置: 

WordPress/wp-includes/post-template.php

这里会根据ID从postmeta表中返回 meta_key为 _wp_page_template记录的 meta_value,就是我们注入的剪裁后后的图片名称 cropped-rai4over.jpg,然后返回为 $templates。

$templates作为参数传递进入 get_query_template函数。

$templates又传入 locate_template函数。

这里路径和文件进行拼接并判断,进入第一个 if分支就 break, $located为:

/Applications/MAMP/htdocs/WordPress/wp-content/themes/twentynineteen/cropped-rai4over.jpg


这就是我们上面图片穿越的目录,然后层层返回到 WordPress/wp-includes/template-loader.php文件。

完成文件包含,成功代码执行。

补充

经过测试,在低版本下(如 WordPress4.9.9),利用方式和高版本存在少许差别:

还可以利用文章模块进行RCE,不过需要注意因为模板图片包含发生变化,因此穿越的目录也发生变化:

wp-includes/theme-compat/cropped-rai4over.jpg


【本文来自 ChaMd5安全团队,文章内容以思路为主。

   如需转载,请先联系ChaMd5安全团队授权。

  未经授权请勿转载。】


本文作者:ChaMd5安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/101789.html

Tags:
评论  (0)
快来写下你的想法吧!

ChaMd5安全团队

文章数:85 积分: 181

www.chamd5.org 专注解密MD5、Mysql5、SHA1等

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号