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权限的账号并不苛刻。
在媒体库中上传文件,然后更新图片操作
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