WordPress SQL隱碼攻擊漏洞與提權分析

發表於2015-08-25

<a href=/office/word/ target=_blank class=infotextkey>word</a>press漏洞 SQL隱碼攻擊漏洞 網站安全

WordPress核心功能SQL隱碼攻擊漏洞分析

威脅響應中心研究員對Wordpress核心功能SQL隱碼攻擊漏洞(編號為CVE-2015-5623和CVE-2015-2213)進行了詳細的分析

0x00 漏洞概述

在twitter上看到Wordpress核心功能出現SQL隱碼攻擊漏洞,想學習下,就深入的跟了下程式碼,結果發現老外留了好大的一個坑。雖然確實存在注入問題,但是卻沒有像他blog中所說的那樣可以透過訂閱者這樣的低許可權來觸發SQL隱碼攻擊漏洞。

這個Wordpress漏洞系列的文章目前更新了兩個部分,一個是透過許可權繞過實現訂閱者許可權寫一篇文章到回收站,另一個就是透過寫入的這篇文章來實現SQL隱碼攻擊漏洞。這兩個漏洞的描述,TSRC的phithon寫的文章其實已經很清楚了,我這裡從我分析的角度來介紹這兩個漏洞的形成、利用以及phithon省略掉的原文部分內容。

0x01 越權提交文章

_wpnonce的獲取

在講越權漏洞之前,我們需要介紹一下Wordpress後臺的_wpnonce引數,這個引數主要是用來防止CSRF攻擊的token。後臺大多數敏感功能都會透過,當前使用者資訊、功能名稱、操作物件id等內容生成token,所以我們很難在沒有token的時候進行某些功能的使用。這個CSRF的防護機制間接地導致之後SQL隱碼攻擊很難再低許可權情況下觸發(因為看不到token),後面講SQL隱碼攻擊漏洞時,我們會詳細的聊這個問題有多坑。

之所以要把_wpnonce的獲取放在前面講,是因為,我們需要一個讓我們可以使用編輯提交文章功能的token。這個功能的token我們可以透過訪問後臺post.php的post-quickdraft-save功能來獲取,嚴格的來說這個獲取方式也算是一個資訊洩露漏洞,官方也在新版本中進行了修補。下面我我們來看下這個token洩露的原因。部分程式碼如下:

case 'post-quickdraft-save':    // Check nonce and capabilities    $nonce = $_REQUEST['_wpnonce'];    $error_msg = false;     // For output of the quickdraft dashboard widget    require_once ABSPATH . 'wp-admin/includes/dashboard.php';     if ( ! wp_verify_nonce( $nonce, 'add-post' ) )        $error_msg = __( 'Unable to submit this form, please refresh and try again.' );     if ( ! current_user_can( 'edit_posts' ) )        $error_msg = __( 'Oops, you don’t have Access to add new drafts.' );     if ( $error_msg )        return wp_dashboard_quick_press( $error_msg );

從上面程式碼我們可以看出這個功能在發現出現錯誤時,會將錯誤資訊透過wp_dashboard_quick_press這個函式列印到頁面上,而這個函式生成的頁面的程式碼中有這樣一行:

  • PHP wp_nonce_field( 'add-post' ); 1 wp_nonce_field('add-post

生成了add-post功能的_wpnonce,也就是說,即使我們做了一些被禁止的操作,返回頁面中也會出現這個_wpnonce。

越權提交與條件競爭

如phithon文章所說,由於GP使用邏輯混亂而導致的驗證繞過。我們來看post.php檔案在開始檢驗文章是否存在的程式碼:

if ( isset( $_GET['post'] ) )    $post_id = $post_ID = (int) $_GET['post'];elseif ( isset( $_POST['post_ID'] ) )    $post_id = $post_ID = (int) $_POST['post_ID'];else    $post_id = $post_ID = 0; global $post_type, $post_type_object, $post; if ( $post_id )    $post = get_post( $post_id ); if ( $post ) {    $post_type = $post->post_type;    $post_type_object = get_post_type_object( $post_type );}

可以看到在先提取GET中的post引數作為文章id來提取文章資訊,如果沒有GET的post引數,再去用POST的post_ID引數來提取。而真正檢驗使用者是否對文章具有編輯許可權,則是在edit_post中進行的,而這裡的判斷引數是從POST中提取的:

function edit_post( $post_data = null ) {    global $wpdb;     if ( empty($post_data) )        $post_data = &$_POST;     // Clear out any data in internal vars.    unset( $post_data['filter'] );     $post_ID = (int) $post_data['post_ID'];    $post = get_post( $post_ID );    $post_data['post_type'] = $post->post_type;    $post_data['post_mime_type'] = $post->post_mime_type;     if ( ! empty( $post_data['post_status'] ) ) {        $post_data['post_status'] = sanitize_key( $post_data['post_status'] );         if ( 'inherit' == $post_data['post_status'] ) {            unset( $post_data['post_status'] );        }    }     $ptype = get_post_type_object($post_data['post_type']);    if ( !current_user_can( 'edit_post', $post_ID ) ) {        if ( 'page' == $post_data['post_type'] )            wp_die( __('You are not allowed to edit this page.' ));        else            wp_die( __('You are not allowed to edit this post.' ));    }

我們繼續看這段程式碼最後的if判斷,判斷當前使用者是否有編輯這篇文章的許可權。這個判斷最終的操作是在map_meta_cap這個函式中進行的:

case 'edit_post':    case 'edit_page':        $post = get_post( $args[0] );        if ( empty( $post ) )            break;         if ( 'revision' == $post->post_type ) {            $post = get_post( $post->post_parent );        }

可以看出如果文章是不存在的,那麼就會break出switch,在函式結束時返回$caps變數,而$caps在函式開始的時候已經被定義為一個空的陣列了,也就是說,這裡會返回一個空陣列。下面我們來向前走,返回到呼叫map_meta_cap的has_cap函式中,看看後續的操作

public function has_cap( $cap ) {        if ( is_numeric( $cap ) ) {            _deprecated_argument( __FUNCTION__, '2.0', 
__('Usage of user levels by plugins and themes is deprecated. Use roles and capabilities instead.') );            $cap = $this->translate_level_to_cap( $cap );        }         $args = array_slice( func_get_args(), 1 );        $args = array_merge( array( $cap, $this->ID ), $args );        $caps = call_user_func_array( 'map_meta_cap', $args );         // Multisite super admin has all caps by definition, Unless specifically denied.        if ( is_multisite() && is_super_admin( $this->ID ) ) {            if ( in_array('do_not_allow', $caps) )                return false;            return true;        }         $capabilities = apply_filters( 'user_has_cap', $this->allcaps, $caps, $args, $this );        $capabilities['exist'] = true; // Everyone is allowed to exist        foreach ( (array) $caps as $cap ) {            if ( empty( $capabilities[ $cap ] ) )                return false;        }         return true;    }

看最後那個foreach,它檢測$caps中的各個元素在$capabilities中是否有不存在的,如果有那麼就return false,但是$caps是個空陣列,這樣很容易我們就獲得了一個true,許可權校驗成功繞過。那麼這樣我們可以得到的一個資訊就是,我們可以透過這個缺陷去嘗試update一個並不存在的文章。

那麼現在問題來了,直接update一個不存在的文章是沒有意義的,因為資料庫執行SQL是肯定會報錯,我們要怎麼才能成功建立一篇文章呢?

在校驗許可權之後,資料庫執行操作之前,post.php中有這樣一段程式碼:

if ( isset( $post_data['tax_input'] ) ) {        foreach ( (array) $post_data['tax_input'] as $taxonomy => $terms ) {            // Hierarchical taxonomy data is already sent as term IDs, so no conversion is necessary.            if ( is_taxonomy_hierarchical( $taxonomy ) ) {                continue;            }             if ( ! is_array( $terms ) ) {                $comma = _x( ',', 'tag delimiter' );                if ( ',' !== $comma ) {                    $terms = str_replace( $comma, ',', $terms );                }                $terms = explode( ',', trim( $terms, " /n/t/r/x0B," ) );            }             $clean_terms = array();            foreach ( $terms as $term ) {                // Empty terms are invalid input.                if ( empty( $term ) ) {                    continue;                }                 $_term = get_terms( $taxonomy, array(                    'name' => $term,                    'fields' => 'ids',                    'hide_empty' => false,                ) );

如果在POST的資料中存在名為tax_input的陣列引數,那麼就對引數中的值使用逗號進行切割,然後對分割出來的每個內容進行一次select查詢。那麼這裡我們可以想象一下,如果我們post_ID中填寫的是對當前最新文章ID+1的數值,並且在tax_input這個引數新增特別多的內容,導致它不停地select查詢,我們是不是在這個停滯的時間當中插入一篇文章(這篇文章的ID剛好是最新文章ID+1),那麼後面的update是不是就有意義了?

相關文章