作者:Lucifaer

0x00 漏洞簡述

1. 漏洞簡介

REST API自動包含在Wordpress4.7以上的版本,WordPress REST API提供了一組易于使用的HTTP端點,可以使用戶以簡單的JSON格式訪問網站的數據,包括用戶,帖子,分類等。檢索或更新數據與發送HTTP請求一樣簡單。上周,一個由REST API引起的影響WorePress4.7.04.7.1版本的漏洞被披露,該漏洞可以導致WordPress所有文章內容可以未經驗證被查看,修改,刪除,甚至創建新的文章,危害巨大。

2. 漏洞影響版本

  • WordPress4.7.0
  • WordPress4.7.1

0x01 漏洞復現

Seebug上已經給出詳細的復現過程,在復現過程中可以使用已經放出的POC來進行測試。

0x02 漏洞分析

其實漏洞發現者已經給出了較為詳細的分析過程,接下來說說自己在參考了上面的分析后的一點想法。

WP REST API

首先來說一下REST API

控制器

WP-API中采用了控制器概念,為表示自愿端點的類提供了標準模式,所有資源端點都擴展WP_REST_Controller來保證其實現通用方法。

五種請求

之后,WP-API還有這么幾種請求(也可以想成是功能吧):

  • HEAD
  • GET
  • POST
  • PUT
  • DELETE

以上表示HTTP客戶端可能對資源執行的操作類型。

HTTP客戶端

WordPress本身在WP_HTTP類和相關函數中提供了一個HTTP客戶端。用于從另一個訪問一個WordPress站點。

資源

簡單來說,就是文章,頁面,評論等。

WP-API允許HTTP客戶端對資源執行CRUD操作(創建,讀取,更新,刪除,這邊只展示和漏洞相關的部分):

  • GET /wp-json/wp/v2/posts獲取帖子的集合:

  • GET /wp-json/wp/v2/posts/1獲取一個ID為1的單獨的Post:

可以看到ID為1的文章標題為Hello World,包括文章的路由也有。

路由

路由是用于訪問端點的“名稱”,在URL中使用(在非法情況下可控,就像這個漏洞一樣)。

例如,使用URLhttp://example.com/wp-json/wp/v2/posts/123:

  • 路由(route)是wp/v2/posts/123,不包括wp-json,因為wp-json是API本身的基本路徑。
  • 這個路由有三個端點:
    • GET觸發一個get_item方法,將post數據返回給客戶端。
    • PUT觸發一個update_item方法,使數據更新,并返回更新的發布數據。
    • DELETE觸發delete_item方法,將現在刪除的發布數據返回給客戶端。

靜態追蹤

知道了WP-API的路由信息以及其操作方式,可以根據其運行的思路來看一下具體實現的代碼。

我們看一下/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

根據上面的信息,我們可以知道這是注冊controller對象的路由,實現路由中端點方法。

在這里,如果我們向/wp-json/wp/v2/posts/1發送請求,則ID參數將被設置為1:

同時,注意一下這里:

register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
                'args'                => $get_item_args,
            ),
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'update_item' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
            ),
            array(
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => array( $this, 'delete_item' ),
                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                'args'                => array(
                    'force' => array(
                        'type'        => 'boolean',
                        'default'     => false,
                        'description' => __( 'Whether to bypass trash and force deletion.' ),
                    ),
                ),
            ),
            'schema' => array( $this, 'get_public_item_schema' ),
        ) );

可以看到在register_rest_route中對路由進行了正則限制:

也就是防止攻擊者惡意構造ID值,但是我們可以發現$_GET$_POST值優先于路由正則表達式生成的值:

這邊沒有找到ID為123hh的項目,所以返回rest_invalid

現在我們可以忽略路由正則的限制,來傳入我們自定義的ID。

接下來在審查各個端點方法中,找到了update_item這個方法,及其權限檢查方法update_item_permissions_check

public function update_item_permissions_check( $request ) {

        $post = get_post( $request['id'] );
        $post_type = get_post_type_object( $this->post_type );

        if ( $post && ! $this->check_update_permission( $post ) ) {
            return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
        }

        if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
            return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
        }

        if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
            return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
        }

        if ( ! $this->check_assign_terms_permission( $request ) ) {
            return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign the provided terms.' ), array( 'status' => rest_authorization_required_code() ) );
        }

        return true;
    }

可以看到,此函數通過檢查文章是否實際存在,以及我們的用戶是否有權限編輯這邊文章來驗證請求。但是當我們發送一個沒有響應文章的ID時,就可以通過權限檢查,并允許繼續執行對update_item方法的請求。

具體到代碼,就是讓$post為空,就可以通過權限檢查,接下來跟進get_post方法中看一下:

function get_post( $post = null, $output = OBJECT, $filter = 'raw' ) {
    if ( empty( $post ) && isset( $GLOBALS['post'] ) )
        $post = $GLOBALS['post'];

    if ( $post instanceof WP_Post ) {
        $_post = $post;
    } elseif ( is_object( $post ) ) {
        if ( empty( $post->filter ) ) {
            $_post = sanitize_post( $post, 'raw' );
            $_post = new WP_Post( $_post );
        } elseif ( 'raw' == $post->filter ) {
            $_post = new WP_Post( $post );
        } else {
            $_post = WP_Post::get_instance( $post->ID );
        }
    } else {
        $_post = WP_Post::get_instance( $post );
    }

    if ( ! $_post )
        return null;

從代碼中可以看出,它是用wp_posts中的get_instance靜態方法來獲取文章的,跟進wp_posts類,位于/wp-includes/class-wp-post.php中:

public static function get_instance( $post_id ) {
        global $wpdb;

        if ( ! is_numeric( $post_id ) || $post_id != floor( $post_id ) || ! $post_id ) {
            return false;
        }

可以看到,當我們傳入的ID不是全由數字字符組成的時候,就會返回false,也就是返回一個不存在的文章。從而get_post方法返回null,從而繞過update_item_permissions_check的權限檢測。

回頭再看一下可執行方法upload_item

public function update_item( $request ) {
        $id   = (int) $request['id'];
        $post = get_post( $id );

        if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
            return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
        }

        $post = $this->prepare_item_for_database( $request );

        if ( is_wp_error( $post ) ) {
            return $post;
        }

        // convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
        $post_id = wp_update_post( wp_slash( (array) $post ), true );

在這邊將ID參數裝換為一個整數,然后傳遞給get_post。而PHP類型轉換的時候回出現這樣的情況:

所以,也就是說,當攻擊者發起/wp-json/wp/v2/posts/1?id=1hhh請求時,便是發起了對ID為1的文章的請求。下面為利用[exploit-db][2]上的POC來進行測試:

  • 新建文章:

  • 測試:

  • 測試結果:

多想了一下

乍一看,感覺這個洞并沒有什么太大的影響,但是仔細想了一下,危害還是很大的。先不說WordPress頁面執行php代碼的各種插件,還有相當一部分的WordPress文章可以調用短代碼的方式來輸出特定的內容,以及向日志中添加內容,這是一個思路。

另一個思路就是可以進行對原來文章中的指定超鏈接進行修改,從而進行釣魚。

還有一個思路,就是利用WordPress文章中解析html以及JavaScript文件包含的做法,輔助其他方法,進行攻擊。

0x03 diff比較

對于該漏洞,關鍵的修改在/wp-includes/class-wp-post.php中:

更改了對于$post_id的參數的傳入順序和判斷條件,防止了我們傳入數字+字母這樣的格式進行繞過。

0x04 修補方案

將WordPress更新到最新版本。

0x05 參考鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/208/