Wordpress安全架構分析
作者:LoRexxar'@知道創宇404實驗室
發表時間:2017年10月25日
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
傳入的_wpnonce
和action
進入函式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漏洞
這是一個很精巧的漏洞,具體的漏洞分析可以看文章
這裡不討論這個,直接跳過前面的步驟到漏洞核心原理的部分
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
事實上,在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的外掛上多做注意可能最重要的一點。