作者:LoRexxar'@知道創宇404實驗室

0x01 前言

WordPress是一個以PHP和MySQL為平臺的自由開源的博客軟件和內容管理系統。WordPress具有插件架構和模板系統。Alexa排行前100萬的網站中有超過16.7%的網站使用WordPress。到了2011年8月,約22%的新網站采用了WordPress。WordPress是目前因特網上最流行的博客系統。

在zoomeye上可以搜索到的wordpress站點超過500萬,毫不夸張的說,每時每刻都有數不清楚的人試圖從wordpress上挖掘漏洞...

由于前一段時間一直在對wordpress做代碼審計,所以今天就對wordpress做一個比較完整的架構安全分析...

0x02 開始

在分析之前,我們可能首先需要熟悉一下wordpress的結構

├─wp-admin
├─wp-content
│  ├─languages
│  ├─plugins
│  ├─themes
├─wp-includes
├─index.php
├─wp-login.php
  • admin目錄不用多說了,后臺部分的所有代碼都在這里。
  • content主要是語言、插件、主題等等,也是最容易出問題的部分。
  • includes則是一些核心代碼,包括前臺代碼也在這里

除了文件目錄結構以外,還有一個比較重要的安全機制,也就是nonce,nonce值是wordpress用于防御csrf攻擊的手段,所以在wordpress中,幾乎每一個請求都需要帶上nonce值,這也直接導致很多類似于注入的漏洞往往起不到預期的效果,可以說這個機制很大程度上減少了wordpress的漏洞發生。

0x03 nonce安全機制

出于防御csrf攻擊的目的,wordpress引入了nonce安全機制,只有請求中_wpnonce和預期相等,請求才會被處理。

我們一起來從代碼里看看

當我們在后臺編輯文章的時候,進入/wp-admin/edit.php line 70

進入check_admin_referer,這里還會傳入一個當前行為的屬性,跟入/wp-includes/pluggable.php line 1072

傳入的_wpnonceaction進入函數wp_verify_nonce,跟入/wp-includes/pluggable.php line 1874

這里會進行hash_equals函數來比對,這個函數不知道是不是wp自己實現的,但是可以肯定的是沒辦法繞過,我們來看看計算nonce值的幾個參數。

$expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), -12, 10 );
  • i:忘記是什么了,是個定值
  • action:行為屬性名,可以被預測,在代碼里的不同部分都是固定的
  • uid:當前用戶的id,由1自增,可以算是可以被預測
  • token:最重要的就是這部分

當我們登陸后臺時,我們會獲得一個cookie,cookie的第一部分是用戶名,第三部分就是這里的token值。

我們可以認為這個參數是無法獲得的。

當我們試圖通過csrf攻擊后臺,添加管理員等,我們的請求就會被攔截,因為我們沒辦法通過任何方式獲得這個_wpnonce值。

但事實上,在wordpress的攻擊思路上,很多攻擊方式都受限于這個wpnonce,比如后臺反射性xss漏洞,但可能是通過編輯文件、提交表單、提交查詢等方式觸發,那么我們就沒辦法通過簡單的點擊鏈接來觸發漏洞攻擊鏈,在nonce這步就會停止。

這里舉兩個例子

Loginizer CSRF漏洞(CVE-2017-12651)

Loginizer是一個wordpress的安全登陸插件,通過多個方面的設置,可以有效的增強wp登陸的安全性,在8月22日,這個插件爆出了一個CSRF漏洞。

我們來看看代碼

/loginizer/tags/1.3.6/init.php line 1198

這里有一個刪除黑名單ip和白名單ip的請求,當后臺登陸的時候,我們可以通過這個功能來刪除黑名單ip。

但是這里并沒有做任何的請求來源判斷,如果我們構造CSRF請求,就可以刪除黑名單中的ip。

這里的修復方式也就是用了剛才提到的_wpnonce機制。

這種方式有效的防止了純CSRF漏洞的發生。

UpdraftPlus插件的SSRF漏洞

UpdraftPlus是一個wordpress里管理員用于備份網站的插件,在UpdraftPlus插件中存在一個CURL的接口,一般是用來判斷網站是否存活的,但是UpdraftPlus本身沒有對請求地址做任何的過濾,造成了一個SSRF漏洞。

當請求形似

wp-admin/admin-ajax.php?action=updraft_ajax&subaction=httpget&nonce=2f2f07ce90&uri=http://127.0.0.1&curl=1

服務器就會向http://127.0.0.1發起請求。

正常意義上來說,我們可以通過構造敏感鏈接,使管理員點擊來觸發。但我們注意到請求中帶有nonce參數,這樣一來,我們就沒辦法通過欺騙點擊的方式來觸發漏洞了。

wordpress的nonce機制從另一個角度防止了這個漏洞的利用。

0x04 Wordpress的過濾機制

除了Wordpress特有的nonce機制以外,Wordpress還有一些和普通cms相同的的基礎過濾機制。

和一些cms不同的是,Wordpress并沒有對全局變量做任何的處理,而是根據不同的需求封裝了多個函數用于處理不同情況下的轉義。

對于防止xss的轉義

wordpress對于輸出點都有著較為嚴格的輸出方式過濾。

/wp-includes/formatting.php

這個文件定義了所有關于轉義部分的函數,其中和xss相關的較多。

esc_url()
用于過濾url可能會出現的地方,這個函數還有一定的處理url進入數據庫的情況(當$_context為db時)

esc_js()
用于過濾輸出點在js中的情況,轉義" < > &,還會對換行做一些處理。

esc_html()
用于過濾輸出點在html中的情況,相應的轉義

esc_attr()
用于過濾輸出點在標簽屬性中的情況,相應的轉義

esc_textarea()
用于過濾輸出點在textarea標簽中的情況,相應的轉義

tag_escape()
用于出現在HTML標簽中的情況,主要是正則

在wordpress主站的所有源碼中,所有會輸出的地方都會經過這幾個函數,有效的避免了xss漏洞出現。

舉個例子,當我們編輯文章的時候,頁面會返回文章的相關信息,不同位置的信息就會經過不同的轉義。

對于sql注入的轉義

在Wordpress中,關于sql注入的防御邏輯比較特別。

我們先從代碼中找到一個例子來看看

/wp-admin/edit.php line 86

$post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type=%s AND post_status = %s", $post_type, $post_status ) );

這里是一個比較典型的從數據存儲數據,wordpress自建了一個prepare來拼接sql語句,并且拼接上相應的引號,做部分轉義。

當我們傳入

$post_type = "post";
$post_status = "test'";

進入語句

$wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type=%s AND post_status = %s", $post_type, $post_status )

進入prepare函數

/wp-includes/wp-db.php line 1291

    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 );
    }

這個函數會讀取參數值,然后會在字符串處加上相應的單引號或者雙引號,并且在拼接之前,調用escape_by_ref轉義參數。

public function escape_by_ref( &$string ) {
    if ( ! is_float( $string ) )
        $string = $this->_real_escape( $string );
}

這里的_real_escape函數,就是一些轉義函數的封裝。

    function _real_escape( $string ) {
        if ( $this->dbh ) {
            if ( $this->use_mysqli ) {
                return mysqli_real_escape_string( $this->dbh, $string );
            } else {
                return mysql_real_escape_string( $string, $this->dbh );
            }
        }

        $class = get_class( $this );
        if ( function_exists( '__' ) ) {
            /* translators: %s: database access abstraction class, usually wpdb or a class extending wpdb */
            _doing_it_wrong( $class, sprintf( __( '%s must set a database connection for use with escaping.' ), $class ), '3.6.0' );
        } else {
            _doing_it_wrong( $class, sprintf( '%s must set a database connection for use with escaping.', $class ), '3.6.0' );
        }
        return addslashes( $string );
    }

這樣在返回前,調用vsprintf的時候,post_status的值中的單引號就已經被轉義過了。

當然,在代碼中經常會不可避免的拼接語句,舉個例子。

/wp-includes/class-wp-query.php line 2246~2282

面對這種大批量的拼接問題,一般會使用esc_sql函數來過濾

這里esc_sql最終也是會調用上面提到的escape函數來轉義語句

function esc_sql( $data ) {
    global $wpdb;
    return $wpdb->_escape( $data );
}

其實一般意義上來說,只要拼接進入語句的可控參數進入esc_sql函數,就可以認為這里不包含注入點。

但事實就是,總會有一些錯誤發生。

Wordpress Sqli漏洞

這是一個很精巧的漏洞,具體的漏洞分析可以看文章

http://www.bjnorthway.com/386/

這里不討論這個,直接跳過前面的步驟到漏洞核心原理的部分

wp-includes/meta.php line 365行

這里我們可以找到漏洞代碼

我們可以注意到,當滿足條件的時候,字符串會兩次進入prepare函數。

當我們輸入22 %1$%s hello的時候,第一次語句中的占位符%s會被替換為'%s',第二次我們傳入的%s又會被替換為'%s',這樣輸出結果就是meta_value = '22 %1$'%s' hello'

緊接著%1$'%s會被格式化為$_thumbnail_id,這樣就會有一個單引號成功的逃逸出來了。

這樣,在wordpress的嚴防死守下,一個sql注入漏洞仍然發生了。

0x05 Wordpress插件安全

其實Wordpress的插件安全一直都是Wordpress的安全體系中最最薄弱的一環,再加上Wordpress本身的超級管理員信任問題,可以說90%的Wordpress安全問題都是出在插件上。

我們可以先了解一下Wordpress給api開放的接口,在wordpress的文檔中,它推薦wordpress的插件作者通過hook函數來把自定義的接口hook進入原有的功能,甚至重寫系統函數。

也就是說,如果你愿意,你可以通過插件來做任何事情。

從幾年前,就不斷的有wordpress的插件主題爆出存在后門。

http://www.freebuf.com/articles/web/97990.html
http://www.bjnorthway.com/140/

事實上,在wordpress插件目錄中,wordpress本身并沒有做任何的處理,當你的用戶權限為超級管理員時,wordpress默認你可以對自己的網站負責,你可以修改插件文件、上傳帶有后門的插件,這可以導致后臺幾乎可以等于webshell。

也正是由于這個原因,一個后臺的反射性xss就可以對整個站進行利用。

而Wordpress的插件問題也多數出現在開發者水平的參差不齊上,對很多接口都用了有問題的過濾方式甚至沒做任何過濾,這里舉個例子。

Wordpress Statistics注入漏洞

Wordpress Statistics在v12.0.7版本的時候,爆出了一個注入漏洞,當一個編輯權限的賬戶在編輯文章中加入短代碼,服務端在處理的時候就會代入sql語句中。

短代碼是一個比較特殊的東西,這是Wordpress給出的一個特殊接口,當文章加入短代碼時,后臺可以通過處理短代碼返回部分數據到文章中,就比如文章閱讀數等...

當我們傳入

[wpstatistics stat="searches" time="today" provider="sss' union select 1,sleep(5),3,4,5,6#" format="1111" id="1"]

跟入代碼/includes/functions/funstions.php 725行

然后進入 /includes/functions/funstions.php 622行

這里直接拼接,后面也沒有做任何處理。

這個漏洞最后的修復方式就是通過調用esc_sql來轉義參數,可見漏洞的產生原因完全是插件開發者的問題。

0x06 總結

上面稀里嘩啦的講了一大堆東西,但其實可以說Wordpress的安全架構還是非常安全的,對于Wordpress主站來說,最近爆出的漏洞大部分都是信任鏈的問題,在wordpress小于4.7版本中就曾爆出過儲存型xss漏洞,這個漏洞產生的很大原因就是因為信任youtube的返回而導致的漏洞。

https://www.seebug.org/vuldb/ssvid-92845

而在實際生活中,wordpress的漏洞重點集中在插件上面...在wordpress的插件上多做注意可能最重要的一點。


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