whip1ash

Wordpress-CVE-2017-14723

2017-11-11

Introduction

在这个漏洞上浪费了很多时间 这个漏洞发生在一段前后不接的代码上 无法触发 利用困难 但是构造很精巧
偶然在seebug上看见了一篇讲wordpress安全架构的文章 在后面引用了这个漏洞 翻了翻网上的文章 感觉与我所遇到的情况有一点区别
简单的来说就是$wpdb->prepare()这个底层数据库的函数在特殊的情况处理上出了问题

Environment

由于本人是个菜鸡 所以代码审计的时候还是比较喜欢在ide中跑起来 打断点去debug 所以这里用 phpstorm + Xdebug 本地环境用的MAMP PRO

关于phpstorm + Xdebug环境怎么配

https://www.cnblogs.com/xujian2016/p/5548921.html

Content

下面是这个漏洞的调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   $post_ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type='attachment' AND post_status = 'trash'" );

$post_ids => $post_id_del

wp_delete_attachment( $post_id_del )
// wp_delete_attachment( $post_id, $force_delete = false )
$post_id_del => $post_id

$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) )

delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
//delete_metadata($meta_type, $object_id, $meta_key, $meta_value = '', $delete_all = false);

$meta_key=> '_thumbnail_id'
$post_id => $meta_value

if ( $delete_all ) {
$value_clause = '';
if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
$value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
}

$object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
}

##在这里我主要说说我的疑问

###1. exit;
我的wordpress版本是4.8.1

在wp-admin/upload.php 的96行有一个exit;
按照seebug给的poc
http://localhost/wp-admin/upload.php?action=delete&media[]=22%20%251%24%25s%20hello&_wpnonce=bbba5b9cd3
根本执行不到出现问题的函数处啊! 直接就exit了啊!

###2.$post_ids不可控
出问题的函数wp_delete_attachment() 在switch($doaction) case ‘delete’ : 里

1
2
3
4
5
6
7
8
9
10
11
12
case 'delete':
if ( !isset( $post_ids ) )
break;
foreach ( (array) $post_ids as $post_id_del ) {
if ( !current_user_can( 'delete_post', $post_id_del ) )
wp_die( __( 'Sorry, you are not allowed to delete this item.' ) );

if ( !wp_delete_attachment( $post_id_del ) )
wp_die( __( 'Error in deleting.' ) );
}
$location = add_query_arg( 'deleted', count( $post_ids ), $location );
break;

$doaction在103行被赋值

1
2
3
4
5
$wp_list_table = _get_list_table('WP_Media_List_Table');
$pagenum = $wp_list_table->get_pagenum();

// Handle bulk actions
$doaction = $wp_list_table->current_action();

先看怎么初始化$wp_list_table对象
wp-admin/includes/list-table.php

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
function _get_list_table( $class, $args = array() ) {
$core_classes = array(
//Site Admin
'WP_Posts_List_Table' => 'posts',
'WP_Media_List_Table' => 'media',
'WP_Terms_List_Table' => 'terms',
'WP_Users_List_Table' => 'users',
'WP_Comments_List_Table' => 'comments',
'WP_Post_Comments_List_Table' => array( 'comments', 'post-comments' ),
'WP_Links_List_Table' => 'links',
'WP_Plugin_Install_List_Table' => 'plugin-install',
'WP_Themes_List_Table' => 'themes',
'WP_Theme_Install_List_Table' => array( 'themes', 'theme-install' ),
'WP_Plugins_List_Table' => 'plugins',
// Network Admin
'WP_MS_Sites_List_Table' => 'ms-sites',
'WP_MS_Users_List_Table' => 'ms-users',
'WP_MS_Themes_List_Table' => 'ms-themes',
);

if ( isset( $core_classes[ $class ] ) ) {
foreach ( (array) $core_classes[ $class ] as $required )
require_once( ABSPATH . 'wp-admin/includes/class-wp-' . $required . '-list-table.php' );

if ( isset( $args['screen'] ) )
$args['screen'] = convert_to_screen( $args['screen'] );
elseif ( isset( $GLOBALS['hook_suffix'] ) )
$args['screen'] = get_current_screen();
else
$args['screen'] = null;

return new $class( $args );
}

return false;
}

所以去wp-admin/includes/class-wp-media-list-table.php 查看current_action()的定义

1
2
3
4
5
6
7
8
9
10
11
12
13

public function current_action() {
if ( isset( $_REQUEST['found_post_id'] ) && isset( $_REQUEST['media'] ) )
return 'attach';

if ( isset( $_REQUEST['parent_post_id'] ) && isset( $_REQUEST['media'] ) )
return 'detach';

if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) )
return 'delete_all';

return parent::current_action();
}

所以想让$doaction的值为'delete'就先使$doaction = 'delete_all' 就得post或者get过去delete_all or delete_all2

110行有一个判断

1
2
3
4
5
6
7
8
if ( 'delete_all' == $doaction ) {
$post_ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type='attachment' AND post_status = 'trash'" );
$doaction = 'delete';
} elseif ( isset( $_REQUEST['media'] ) ) {
$post_ids = $_REQUEST['media'];
} elseif ( isset( $_REQUEST['ids'] ) ) {
$post_ids = explode( ',', $_REQUEST['ids'] );
}

显而易见,$post_ids是从数据库中取出的值,并不可控.
通过全局搜索wp_delete_attachment()函数,除了此处(upload.php),其他调用此函数的地方都已经进行强制类型转换.

##seebug上的分析很详细了 漏洞原理不再赘述了

1
2
3
4
5
6
7
8
if ( $delete_all ) {
$value_clause = '';
if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
$value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
}

$object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
}

主要问题出在wp_includes/meta.php上了 从上面代码可以看到$meta_value在符合条件的情况下进入$wpdb_prepare()两次
$meta_value可控的时候,通过精心构造的payload会造成单引号逃逸
出问题的代码等同于以下代码

1
2
3
4
5
6
7
8
9
10
$input = addslashes("%1$' and 1=1#");
echo $input;
echo "<br>";
$b = sprintf("AND b='%s'", $input);
echo "<br>";
echo $b ;
echo "<br>";
$sql = sprintf("SELECT * FROM t WHERE a='%s' $b", 'admin');
echo $sql;
echo "<br>";


依然不太理解%1$’ 是如何构造的 我估计是fuzz出来的 以及为什么能够绕过Too few arguments报错

Summry

这个漏洞不好用是真的 存在的问题挺多的 条件也比较苛刻 需要的权限也比较高 并且有wp_nonce验证卡在那里


就这样吧

扫描二维码,分享此文章