By: RickGray (知道創宇404安全實驗室)
近日,WordPress 發布了新版本4.3.1,其中修復了幾個嚴重的安全問題,其中包含了由 Check Point 所提交的一個跨站腳本漏洞(CVE-2015-5714)和一個權限提升漏洞(CVE-2015-5715)。
8月初,Check Point 在其官方博客上發表了一篇關于 《WordPress漏洞三部曲》 系列文章的第一部,在這篇文章中,提及了 WordPress 在 4.2.3 版本中修復的一個越權漏洞,這里對此就不再做具體分析和說明,相關細節詳情可參考原文和 phithon 所寫的 《Wordpress4.2.3提權與SQL注入漏洞(CVE-2015-5623)分析》。
這里主要說明的是 "三部曲" 中的第三部,也就是 Check Point 在其博客上公開的關于 WordPress 4.3.1 版本中所修復的另一個越權漏洞和一個跨站腳本漏洞(原文在這里)。
首先來看看跨站腳本漏洞。WordPress 在編輯文章內容時允許使用簡碼(shorcodes)來表示資源(圖片,鏈接等)。WordPress 中開啟了白名單機制去過濾 HTML 標簽,只有在白名單規則里的標簽,才允許被使用,并且會使用專用腳本 "KSES" 去檢測和過濾這些 HTML 標簽。這里需要說明的是,WordPress 對 HTML 標簽的檢測和過濾發生在將內容插入數據庫時,而簡碼的解析渲染發生在將內容輸出到頁面時,下面簡單用例子說明一下兩個處理過程的差別,編輯文章插入內容為:
#!html
TEST!!![caption width="1" caption='<a href="' ">]</a><a>xxxxxx</a>
因插入的內容包含完整且符合白名單規則的 HTML 標簽,而簡碼 [caption]
(caption簡碼說明) 并不包含在 "KSES" 檢測的內容里,最后輸出內容到前臺時簡碼解析后會被渲染為:
#!html
<p>TEST!!!<figure style="width: 1px;" class="wp-caption alignnone"><figcaption class="wp-caption-text"><a href="</figcaption></figure></a><a>xxxxxx</a></p>
由于在 "KSES" 過濾檢測時只關 HTML 標簽,對簡碼并不進行檢測,又因簡碼中屬性都以 KEY=VALUE
的形式出現,用單引號'
或者雙引號"
包裹值Value
,因此在 TEST!!![caption width="1" caption='<a href="' ">]</a><a>xxxxxx</a>
這段內容中,簡碼 caption
有兩個屬性,分別為:
width: 1
caption: <a href="
而后半部分的 <a href="' ">]</a><a>xxxxxx</a>
又為正常的 HTML 標簽閉合形式,因此并不會被 "KSES" 檢測過濾后丟棄掉。最終在前臺輸出時,簡碼 caption
被解析,使得最后出現 <a>
標簽中 href
屬性值未閉合的情況。
因此利用前后處理的差異,可以構造出有利的 payload 形成 XSS:
#! html
TEST!!![caption width="1" caption='<a href="' ">]</a><a href="http://onMouseOver='alert(1)'">Click me</a>
將上面 payload 作為文章內容發布,前端渲染出來的結果為:
#!html
TEST!!!<figure style="width: 1px;" class="wp-caption alignnone"><figcaption class="wp-caption-text"><a href="</figcaption></figure></a><a href="http://onMouseOver='alert(1)'">Click me</a></p>
輸出的內容在瀏覽器中解析成 <a>
標簽部分,href
屬性值為 </figcaption></figure></a><a href=
,而 http://
由于雙斜杠(//)的原因與 onMouseOver='alert(1)
部分斷開,因此一個 onmouseover 屬性被解析出來,形成 XSS。
該漏洞(CVE-2015-5714)已經在 WordPress 新版 4.3.1 中修復,具體 patch 部分位于兩處,第一處在 wp-includes/shortcodes.php
中的 shortcode_parse_atts() 函數中:
--- wp-includes/shortcodes.php
+++ wp-includes/shortcodes.php
@@ -462,6 +462,15 @@
elseif (isset($m[8]))
$atts[] = stripcslashes($m[8]);
}
+
+ // Reject any unclosed HTML elements
+ foreach( $atts as &$value ) {
+ if ( false !== strpos( $value, '<' ) ) {
+ if ( 1 !== preg_match( '/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value ) ) {
+ $value = '';
+ }
+ }
+ }
} else {
$atts = ltrim($text);
}
新添加的處理過程,過濾了在簡碼屬性值中出現的未閉合 HTML 標簽的值。并且解析簡碼時使用 wp_kses() 函數進行了過濾,確保輸出的內容被編碼(代碼位于 wp-includes/media.php
):
--- wp-includes/media.php
+++ wp-includes/media.php
@@ -863,6 +863,8 @@
$content = $matches[1];
$attr['caption'] = trim( $matches[2] );
}
+ } elseif ( strpos( $attr['caption'], '<' ) !== false ) {
+ $attr['caption'] = wp_kses( $attr['caption'], 'post' );
}
/**
這樣一來就很難利用上面所說的 "KSES"和簡碼解析前后處理差異 成功構造出能夠進行 XSS 的 HTML 標簽了。
Check Point 在文章中還提到了另一個越權操作(與 part1 的越權不同),可以使得不具有文章發布權限的用戶通過 XMLRPC 操作將自己的文章狀態修改為 private
,并可將其置頂 (WordPress 4.3.0版本中已將其修復,未設密碼的私有文章不可置頂)。
越權操作位于 XMLRPC 文章編輯操作中,涉及文件 /wp-includes/class-wp-xmlrpc-server.php
(5042-5327) 其中關鍵代碼分析:
#!php
public function mw_editPost( $args ) {
$this->escape( $args );
$post_ID = (int) $args[0]; // 獲取需要編輯的文章ID (用戶所屬)
$username = $args[1]; // 從請求的xml中獲取用戶名
$password = $args[2]; // 從請求的xml中獲取用戶密碼
$content_struct = $args[3]; // 從請求的xml中獲取結構
$publish = isset( $args[4] ) ? $args[4] : 0;
(...省略)
if ( isset( $content_struct["{$post_type}_status"] ) ) {
switch( $content_struct["{$post_type}_status"] ) {
case 'draft':
case 'pending':
case 'private':
case 'publish':
$post_status = $content_struct["{$post_type}_status"]; // 數據庫中存儲的文章類型為post,所以取的是xml中 post_status 參數的值
break;
default:
$post_status = $publish ? 'publish' : 'draft';
break;
}
}
首先處理程序獲取提交參數并驗證當前用戶權限,對于草稿或者未審核的文章,其數據庫中存儲的文章類型為 post
,所以在取值 $content_struct["{$post_type}_status"]
時,獲取的是提交參數中 post_status
的值。
#!php
(...省略)
// 當用戶不具有文章發布權限時,`publish` 操作會被禁止
// 但是這里并沒有限制 `private` 的情況
// 所以若xml中 post_status 參數值為 private 則跳過檢查
if ( ('publish' == $post_status) ) {
if ( ( 'page' == $post_type ) && ! current_user_can( 'publish_pages' ) ) {
return new IXR_Error( 401, __( 'Sorry, you do not have the right to publish this page.' ) );
} elseif ( ! current_user_can( 'publish_posts' ) ) {
return new IXR_Error( 401, __( 'Sorry, you do not have the right to publish this post.' ) );
}
}
接著,程序會驗證其提交的需要更新的文章狀態。當用戶通過 XMLRPC 進行文章編輯時,若想發布一篇未發布的文章時,會檢查用戶是否具有文章發布的權限。但是該檢查判斷了將文章狀態變為發布狀態的情況下(post_status == publish),而針對將文章狀態變為私有狀態的情況代碼中并沒有進行檢查。程序上的判斷疏忽,致使我們可以使用一個不具有文章發布權限的帳號將自己一篇 未通過審核
或者 存于垃圾箱
的文章的轉臺通過該過程將其改為私有(private
),讓該文章以私文的形式在前臺顯示出來,管理員以及其他具有權限的用戶都能瀏覽到。
另一個需要說明的點就是,通過 XMLRPC 操作編輯文章時,可以將文章進行置頂,具體代碼為:
#!php
(...省略)
// 將文章置頂(4.3.0 版本后不能將未設密碼的私有文章置頂)
// Only posts can be sticky
if ( $post_type == 'post' && isset( $content_struct['sticky'] ) ) {
$data = $newpost;
$data['sticky'] = $content_struct['sticky'];
$data['post_type'] = 'post';
$error = $this->_toggle_sticky( $data, true );
if ( $error ) {
return $error;
}
}
但是該問題在 WordPress 4.3.0 版本中已經得到了限制:
#!php
private function _toggle_sticky( $post_data, $update = false ) {
$post_type = get_post_type_object( $post_data['post_type'] );
// Private and password-protected posts cannot be stickied.
if ( 'private' === $post_data['post_status'] || ! empty( $post_data['post_password'] ) ) {
// 如果需要置頂的文章為私有狀態,并且未設訪問密碼,不能將其置頂,并自動取消之前的置頂狀態
// Error if the client tried to stick the post, otherwise, silently unstick.
if ( ! empty( $post_data['sticky'] ) ) {
return new IXR_Error( 401, __( 'Sorry, you cannot stick a private post.' ) );
}
if ( $update ) {
unstick_post( $post_data['ID'] );
}
} elseif ( isset( $post_data['sticky'] ) ) {
// 如果需要置頂的文章為私有狀態,并且設有訪問密碼,且具有編輯其他文章的權限,則將其所置頂的文章置頂
if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) {
return new IXR_Error( 401, __( 'Sorry, you are not allowed to stick this post.' ) );
}
$sticky = wp_validate_boolean( $post_data['sticky'] );
if ( $sticky ) {
stick_post( $post_data['ID'] );
} else {
unstick_post( $post_data['ID'] );
}
}
}
未設密碼訪問的私有文章已經無法再通過 XMLRPC 編輯文章操作將文章置頂。
下面通過示例來說明如何通過 XMLRPC 編輯文章操作將文章狀態修改為 私有
。為了方便演示,這里事先注冊好一個用戶(投稿者權限,其投稿的文章狀態為 pending
),并提交一篇文章投遞申請:
查看一下待審文章在數據庫中的狀態:
然后根據上面所分析的權限提升細節,構造 payload ,將此待審文章狀態更改為 private
:
可以看到返回消息中提示置頂私有文章失敗,這是因為測試時使用的 WordPress 4.3.0 版本,該版本中已經修復了私有文章任意置頂的問題。
然后查看一下通過 XMLRPC 編輯文章后數據庫中待審核文章的狀態:
數據庫中文章狀態已經變為私有,再到前臺查看首頁:
由投稿用戶提交的待審核文章已經變為私有狀態顯示在前臺頁面中,并且管理員能看到所有的私有文章。
本文一開時已經分析過了如何通過利用 "KSES"與簡碼過濾差異化造成一個存儲型的前臺 XSS,加上第二節所演示越權編輯文章狀態的過程,結合這兩個漏洞,可以使得站點上具有一點權限的用戶(投稿即可),投遞包含惡意內容的文章,然后利用越權操作將文章顯示到前臺,對能夠瀏覽到該文章的用戶(包括管理員)進行 XSS 攻擊。
下面我們模擬一下上面敘述的流程。首先投遞一篇包含 XSS payload 的文章,利用 "KSES"與簡碼渲染操作的差異使得內容在前臺渲染后能夠形成 XSS,將文章內容設置為:
XSS LOL!!![caption width='1' caption='<a href="' ">]</a><a href="http://onMouseOver='alert(/xss/)' style='display:block;position:absolute;top:0px;left:0px;margin-left:-1000px;margin-top:-1000px;width:99999px;height:99999px;'"></a>
然后利用 XMLRPC 遍歷文章得到提交的待審核文章的 id,這里得到待審核文章 id 為:28
,在構造 payload 將其未發布狀態改為私有:
利用 XMLRPC 文章編輯成功修改文章狀態為私有后,訪問前臺查看結果:
回顧 Check Point 所發布的《WordPress漏洞三部曲》,可以知道 WordPress 在 4.2.2 版本中含有其提交的所有漏洞,包括了 競爭條件下權限提升
,文章恢復導致SQL注入
,"KSES"與簡碼過濾差異化導致的XSS
,權限檢查遺漏導致越權操作
等。通看起來,如果在 WordPress 4.2.2 版本下,這些漏洞都能在以 競爭條件下權限提升
作為起始,完成后面的攻擊,實現一個超低權限用戶下進行 SQL 注入、XSS 攻擊的操作。我將 Check Point 在 part1 和 part3 中所提到的漏洞利用方法綜合到一起,寫出了 all in one
的 PoC,其中 競爭條件下權限提升
的過程使用 phithon 文章中所提及的使用兩個訂閱用戶來解決 7 天攻擊周期的限制。
為了達到 all in one
的演示結果,將 WordPress 測試環境更換為 4.2.2 版本,并事先準備兩個訂閱用戶 guest:guest888
,test:test888
,然后運行 PoC:
PoC 提示成功后,管理員訪問前臺,文章成功置頂并包含惡意代碼:
這里不得不佩服洞主對 WordPress 熟悉程度和漏洞挖掘的思路。
雖然 WordPress 在幾個連續的版本中修復了這些漏洞,但在非最新版本中(< 4.3.1)中,這些漏洞還是能夠在特定場景下得以利用。尤其是在 4.2.2 版本中,能夠利用 "三部曲" 中所提及的漏洞進行一系列的攻擊操作。
這些看似雞肋的漏洞在我看來并不雞肋,雞肋只是因為還未找到合適的應用場景而已。
原文出處:http://blog.knownsec.com/2015/09/wordpress-vulnerability-analysis-cve-2015-5714-cve-2015-5715/