一個 Chrome XSS Filter Bypass 的分析

wyzsk發表於2020-08-19
作者: kamael · 2015/07/02 10:39

前幾天,在微博上看到一條關於最近的 Chrome XSS Filter Bypass 的連結:webo,原始補丁在這裡:補丁。在補丁中還提供了 PoC 用於後續的單元測試。

攻擊者用一種巧妙的方法繞過了 Chrome 的 XSSAuditor 的過濾。不過微博裡的那篇短文並沒有對這個漏洞的緣由作分析。碰巧前段時間筆者仔細讀過 Chrome 的 XSSAuditor 的程式碼,因此趁此機會自己分析了一下,如果有錯誤的地方還望指教。

0x00 漏洞描述


原始 PoC :

https://localhost/<svg><script>/<1/>alert(0)</script>

補丁如下:

#!diff
Index: Source/core/html/parser/XSSAuditor.cppt a/Source/core/html/parser/XSSAuditor.cpp b/Source/core/html/parser/XSSAuditor.cpp
index a1e1852201d23ac858c3b5065a2e26f52d128f4d..e73259145366c12c821a710fe83d3637529478ee 100644
--- a/Source/core/html/parser/XSSAuditor.cpp
+++ b/Source/core/html/parser/XSSAuditor.cpp
@@ -471,15 +471,18 @@ bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request)
     if (m_state == PermittingAdjacentCharacterTokens)
         return false;

-    if ((m_state == SuppressingAdjacentCharacterTokens)
-        || (m_scriptTagFoundInRequest && isContainedInRequest(canonicalizedSnippetForJavaScript(request)))) {
+    if (m_state == FilteringTokens && m_scriptTagFoundInRequest) {
+        String snippet = canonicalizedSnippetForJavaScript(request);
+        if (isContainedInRequest(snippet))
+            m_state = SuppressingAdjacentCharacterTokens;
+        else if (!snippet.isEmpty())
+            m_state = PermittingAdjacentCharacterTokens;
+    }
+    if (m_state == SuppressingAdjacentCharacterTokens) {
         request.token.eraseCharacters();
         request.token.appendToCharacter(' '); // Technically, character tokens can't be empty.
-        m_state = SuppressingAdjacentCharacterTokens;
         return true;
     }
-
-    m_state = PermittingAdjacentCharacterTokens;
     return false;
 }

補丁中的描述對這一漏洞進行了介紹,大意是,當過濾器過濾 script 標籤的內容時,第一個區塊的過濾結果將會影響後續區塊。如果第一個區塊被處理為空時,過濾匹配將會失敗。

其實漏洞原理這一句話就介紹明白了。不過,如果對瀏覽器解析 HTML 的流程以及 XSS Filter 的實現沒有了解的話,可能看不大懂上面這句話。

0x01 背景知識


首先,瀏覽器是如何解析 HTML 的?

這裡只簡單的介紹一下和該漏洞相關的部分。

實際上,在 HTML5 中, HTML 的詞法解析和語法解析是被寫進規範裡的,這個可以直接在 WHATWG 主頁上查到。從筆者讀原始碼的瞭解上看,Chrome(Chromium) 幾乎完全遵循了該規範。

其中詞法解析部分,將 HTML 原始碼解析成一個個 token ,比如

#!html
<div>aaa<img src=x><script>x=1;</script>

將被解析成

[Start Tag (div)][Characters (aaa)][Start Tag (img)(attr: {src: x})][Start Tag (script)][Characters x=1;][End Tag (script)]

每個中括號即為一個 token ,每一個 token 都有一堆自己的屬性,比如 token 的名稱,屬性值等。

SVG 標籤有什麼特殊的性質?

實際上,一般的 HTML 標籤屬於 HTML namespace 範疇下,而 SVG 標籤屬於 SVG namespace 範疇。在 SVG 內部,HTML 的解析是按照 XML 模式進行的(與此類似的有 MATHML 標籤)。而 SVG 內部也支援 SCRIPT 標籤,不過這裡的 SCRIPT 標籤的解析模式和 HTML 環境下有著很大的區別。某些時候,可以藉助 SVG 中 SCRIPT 標籤的特殊解析模式,構造一些特殊的攻擊向量。比如:

#!html
<svg><script>alert&#40/1/&#41</script> 

在 XML 中實體符號會被解析成對應的字元。而在 HTML 環境中,SCRIPT 標籤內部不會被做任何處理。

類似的,下面這種用法 Javascript 也會成功執行:

#!html
<svg><script>0<a></a>;alert(1)</script></svg>

因為在 SVG 內,進入SCRIPT 標籤內部後,不必等到出現 </script> 才退回標籤解析模式,而是一旦遇到了新的標籤,即可退出,因此在 SCRIPT 標籤內依然可以插入其他標籤。

Chrome 是如何過濾反射型 XSS 的?

Chrome 的 XSS 過濾並沒有用到正則。而且,這個過濾是在詞法解析階段進行的。也就是說,Chrome 實際上是根據 token 來做過濾的。過濾器會審查每一個 token ,如果發現 token 中存在危險的屬性或欄位,就對該欄位進行處理,然後拿去和 URL 比對。如果發現 URL 中出現了該欄位,即將該欄位清空,並報告惡意指令碼的插入。

比如上面的

#!html
<div>aaa<img src=x><script>x=1;</script>

[Start Tag (div)][Characters (aaa)][Start Tag (img)(attr: {src: x})][Start Tag (script)][Characters x=1;][End Tag (script)]

首先解析器讀取到 [Start Tag (div)],判斷沒有危險;接下來讀取 [Characters (aaa)] ,也沒有危險;然後讀取 [Start Tag (img)(attr: {src: x})] ,過濾器將會檢查 src 屬性是否以 javascript: 開頭,這裡不是,同樣沒有危險;之後檢查 [Start Tag (script)],這裡過濾器將會到 URL 中找 script 字樣,來確認 URL 是否有引入指令碼的可能;並且從這裡開始進入了 script 標籤內部,過濾器會對此進行標識;之後到了 [Characters x=1;] ,注意到這裡已經到了 SCRIPT 標籤內部了,因此,過濾器將會拿 "x=1;" 在 URL 中搜尋,這也是文章開頭的 Patch 中出現的程式碼:

#!diff
 -    if ((m_state == SuppressingAdjacentCharacterTokens)
 -        || (m_scriptTagFoundInRequest && isContainedInRequest(canonicalizedSnippetForJavaScript(request)))) {

0x02 漏洞分析


這裡我們先不管這個 Patch 是如何修復的,我們先來分析漏洞的成因。

PoC 中的注入程式碼將會解析成怎樣的 token 序列呢?

#!html
<svg><script>/<1/>alert(0)</script>

是這樣麼?

  [Start Tag (svg)][Start Tag (script)][Characters /<1/>alert(0)][End Tag (script)]

如果是這樣, XSS Filter 是無法繞過的。

在這裡,由於是在 SVG 標籤內部,詞法解析器將會考察每一個 "<" ,當在字元狀態中出現 "<" 時,即意味著進入了一個新的標籤,詞法解析器會將之前的字串作為一個單獨的 token 提取出來處理掉,再來處理這個新的標籤。但是這裡, <*/> 並不是一個合法的標籤,當解析器處理時,不得不退回,將其當作新的字串處理。

因此真正的 token 序列是這樣的:

  [Start Tag (svg)][Start Tag (script)][Characters /][Characters <1/>alert(0)][End Tag (script)]

重複上述的過濾器過濾流程,在考察過 SCRIPT 開始標籤後,過濾器將會處理隨後的字串。處理函式(補丁前)如下:

#!js
bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request)
{
    ASSERT(m_scriptTagNestingLevel);
    ASSERT(m_state != Uninitialized);
    if (m_state == PermittingAdjacentCharacterTokens)
        return false;

    if ((m_state == SuppressingAdjacentCharacterTokens)
        || (m_scriptTagFoundInRequest && isContainedInRequest(canonicalizedSnippetForJavaScript(request)))) {
        request.token.eraseCharacters();
        request.token.appendToCharacter(' '); // Technically, character tokens can't be empty.
        m_state = SuppressingAdjacentCharacterTokens;
        return true;
    }

    m_state = PermittingAdjacentCharacterTokens;
    return false;
}

先處理 "/",處理函式為 canonicalizedSnippetForJavaScript ;然後看處理後的結果是否在 URL 中(isContainedInRequest)並且是否 URL 中存在 script 標籤(m_scriptTagFoundInRequest);如果不在,則設定當前狀態為 PermittingAdjacentCharacterTokens ,即認為它是沒有危險的。

接下來處理 "<1/>alert(0)",但是注意到處理函式中,首先檢查了狀態是否為 PermittingAdjacentCharacterTokens ;這裡因為兩個字串 token 是連續的,因此,如果第一個字串沒有檢測到在 URL 中,那麼第二個字串根本就不會被過濾!因為當前狀態已經被設定為 PermittingAdjacentCharacterTokens 了!

而這個 PoC 則正是利用了這一點。對於 "/", 在被 canonicalizedSnippetForJavaScript 處理後,將會變成空字串!而空字串是不會被認為與 URL 中的子串重複的。

為什麼會變空呢? canonicalizedSnippetForJavaScript 會對傳入的 Javascript 程式碼作一系列處理,其中會呼叫 isNonCanonicalCharacter 函式檢查字串,將匹配到的字元刪去。匹配程式碼如下:

#!js
return (c == '\\' || c == '0' || c == '\0' || c == '/' || c == '?' || c >= 127);

“/” 正在其中。

兩個字串結合起來,即/<1/>alert(0)是一個合法的 Javacript ,可以成功執行。

類似的,我們可以自己構造其他 PoC :

#!html
<svg><script>0<1>alert(1)</script>

這裡利用了 "0" 也會被處理為空字串的特性。

如果構造成

#!html
<svg><script>0<a></a>alert(1)</script>

能否繞過呢?

答案是否定的,當進入其他標籤時,當前狀態將會設定為 FilteringTokens ,無法繞過檢查。

因此,這個繞過利用了 SVG 內標籤的特殊解析模式,過濾器的連續過濾的機制,以及 isNonCanonicalCharacter 函式對幾個特定字元的匹配後清除。顯然,攻擊者是看著 XSSAuditor 的原始碼構造的 PoC。

:)

其實,這裡 canonicalizedSnippetForJavaScript 是很複雜的,因此,可能存在其他方法使得第一個字串被認為是允許的(即不在 URL 中)。也正是因為這個,在 Patch 的說明中,最後有一句 "Keep looking in that case.",有可能還會出現類似的繞過。

0x03 補丁分析


補丁其實很簡單,如果某個處理後的字串為空,則既不設定為允許也不設定為禁止。這樣既不會干涉後面的處理,也不會增加誤判。

具體補丁程式碼不再分析,其實就只有上面這一點差別。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章