1. 程式人生 > 其它 >Wordpress安全架構分析

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

傳入的_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漏洞

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

https://paper.seebug.org/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

https://paper.seebug.org/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的外掛上多做注意可能最重要的一點。