WordPress 5.0.0远程代码执行漏洞分析

wordpress 5.0.0 远程代码执行漏洞分析

简介

wordpress 5.0.0 远程代码执行漏洞于二月十九日由RIPS披露,博客地址:https://blog.ripstech.com/2019/wordpress-image-remote-code-execution/ ,
漏洞编号为:CVE-2019-8942和 CVE-2019-8943, 主要针对版本:WordPress before 4.9.9 and 5.x before 5.0.1

postmeta覆盖

当编辑post时, 调用wp-admin/post.php 中的以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
case 'editpost':
check_admin_referer('update-post_' . $post_id);

$post_id = edit_post();

// Session cookie flag that the post was saved
if ( isset( $_COOKIE['wp-saving-post'] ) && $_COOKIE['wp-saving-post'] === $post_id . '-check' ) {
setcookie( 'wp-saving-post', $post_id . '-saved', time() + DAY_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, is_ssl() );
}

redirect_post($post_id); // Send user on their way while we keep working

exit();

而edit_post()函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function edit_post( $post_data = null ) {
global $wpdb;

if ( empty($post_data) )
$post_data = &$_POST;

// Clear out any data in internal vars.
unset( $post_data['filter'] );

$post_ID = (int) $post_data['post_ID'];
$post = get_post( $post_ID );
$post_data['post_type'] = $post->post_type;
$post_data['post_mime_type'] = $post->post_mime_type;
...
...

$success = wp_update_post( $post_data ); //漏洞点
// If the save failed, see if we can sanity check the main fields and try again
...
...
...
return $post_ID;
}

这段代码将POST传入的数据赋给$post_data,而$post_data又传入wp_updata_post()中,wp_updata_post()的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function wp_update_post( $postarr = array(), $wp_error = false ) {
if ( is_object($postarr) ) {
// Non-escaped post was passed.
$postarr = get_object_vars($postarr);
$postarr = wp_slash($postarr);
}

// First, get all of the original fields.
$post = get_post($postarr['ID'], ARRAY_A);

if ( is_null( $post ) ) {
if ( $wp_error )
return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) );
return 0;
}

// Escape data pulled from DB.
$post = wp_slash($post);

// Passed post category list overwrites existing category list if not empty.
if ( isset($postarr['post_category']) && is_array($postarr['post_category'])
&& 0 != count($postarr['post_category']) )
$post_cats = $postarr['post_category'];
else
$post_cats = $post['post_category'];

// Drafts shouldn't be assigned a date unless explicitly done so by the user.
if ( isset( $post['post_status'] ) && in_array($post['post_status'], array('draft', 'pending', 'auto-draft')) && empty($postarr['edit_date']) &&
('0000-00-00 00:00:00' == $post['post_date_gmt']) )
$clear_date = true;
else
$clear_date = false;

// Merge old and new fields with new fields overwriting old ones.
$postarr = array_merge($post, $postarr);
$postarr['post_category'] = $post_cats;
if ( $clear_date ) {
$postarr['post_date'] = current_time('mysql');
$postarr['post_date_gmt'] = '';
}

if ($postarr['post_type'] == 'attachment')
return wp_insert_attachment($postarr);

return wp_insert_post( $postarr, $wp_error ); //漏洞点
}

在函数末尾$postarr的值被传入wp_insert_post()中,该函数中有如下代码:

1
2
3
4
5
if ( ! empty( $postarr['meta_input'] ) ) {
foreach ( $postarr['meta_input'] as $field => $value ) {
update_post_meta( $post_ID, $field, $value ); //漏洞点
}
}

该处会将meta_input参数取出,调用updata_post_meta()最后修改wp_postmeta表.

目录遍历

目录遍历时需要前面修改的postmeta表中的_wp_attached_file字段的值,在wp-admin/includes/ajax-actions.php的wp_ajax_crop_image()函数中:

1
$cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );

此处调用wp_crop_image()函数,而该函数中有:

1
$src_file = get_attached_file( $src );

该处获取postmeta表中_wp_attached_file的值,然后赋给$src_filez,之后检查是否存在该文件,不存在的话调用_load_image_edit_path()函数,该函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function _load_image_to_edit_path( $attachment_id, $size = 'full' ) {
$filepath = get_attached_file( $attachment_id );

if ( $filepath && file_exists( $filepath ) ) {
if ( 'full' != $size && ( $data = image_get_intermediate_size( $attachment_id, $size ) ) ) {


$filepath = apply_filters( 'load_image_to_edit_filesystempath', path_join( dirname( $filepath ), $data['file'] ), $attachment_id, $size );
}
} elseif ( function_exists( 'fopen' ) && true == ini_get( 'allow_url_fopen' ) ) {


$filepath = apply_filters( 'load_image_to_edit_attachmenturl', wp_get_attachment_url( $attachment_id ), $attachment_id, $size );


return apply_filters( 'load_image_to_edit_path', $filepath, $attachment_id, $size );
}
}

调用wp_get_attachment_url()后会生成一个url链接,所以如果meta_value被改为2019/03/1.jpg?/../../../1.jpg时生成的url是:http://127.0.0.1/wp-content/uploads/2019/03/1.jpg?/../../../1.jpg ,
这个url会一直返回到wp_crop_image()函数中,该函数中有:

1
$editor = wp_get_image_editor( $src );

此处将url传入 wp_get_image_editor()函数中进行处理,该函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function wp_get_image_editor( $path, $args = array() ) {
$args['path'] = $path;

if ( ! isset( $args['mime_type'] ) ) {
$file_info = wp_check_filetype( $args['path'] );

// If $file_info['type'] is false, then we let the editor attempt to
// figure out the file type, rather than forcing a failure based on extension.
if ( isset( $file_info ) && $file_info['type'] )
$args['mime_type'] = $file_info['type'];
}

$implementation = _wp_image_editor_choose( $args );

if ( $implementation ) {
$editor = new $implementation( $path );
$loaded = $editor->load();

if ( is_wp_error( $loaded ) )
return $loaded;

return $editor;
}

return new WP_Error( 'image_no_editor', __('No editor could be selected.') );
}

最后依然会返回路径,然后在wp_crop_image()函数中:

1
$result = $editor->save( $dst_file );

图片会被保存到该路径,因此可以进行目录遍历。将图片保存到主题目录下可以通过包含执行恶意代码。

总结

这个漏洞分析了好几天,思路非常清奇,涉及的函数比较多,而我安装在本地的wordpress与Xdebug会发生冲突,因此不能用phpstorm进行动态分析,这个问题还在解决中。通过这个漏洞的分析,我学习到:
数据库中存储的文件数据在存入取出过程中可能会产生漏洞,今后要多加关注。