iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Security

Beyond XSS:探索網頁前端資安宇宙系列 第 11

[Day11] 繞過你的防禦:Mutation XSS

  • 分享至 

  • xImage
  •  

之前在講 sanitization 的時候,有提醒大家不要輕易嘗試自己實作,而是去找一些 library 來用,因為這個水很深,坑很多。

那這些 library 會不會也有問題呢?當然有可能,而且事實上也曾經發生過。而有一種常拿來針對 sanitizer 的攻擊方式,叫做 mutation based XSS,也被稱做 mutation XSS 或是簡稱 mXSS。

在理解 mXSS 以前,我們要來看 sanitizer 通常是怎麼運作的。

Sanitizer 的基本運作方式

根據我們之前使用的經驗,sanitizer 的輸入是一個字串,裡面含有 HTML,而輸出也是一個含有 HTML 的字串,使用方式如下:

const inputHtml = '<h1>hello</h1>'
const safeHtml = sanitizer.sanitize(inputHtml)
document.body.innerHTML = safeHtml

那 sanitizer 內部又是怎麼運作的呢?其實內部的運作方式跟我們前面用 BeautifulSoup 實作出的 sanitizer 差不多:

  1. inputHtml 解析成 DOM tree
  2. 根據設定檔,刪除不合法的 node 以及 attribute
  3. 把 DOM tree 序列化(serialize)成字串
  4. 回傳

這流程看似也沒有什麼問題,但魔鬼藏在細節裡,萬一「看起來很安全的 HTML,其實並不安全」呢?咦?不是都已經 sanitize 過了嗎?怎麼個不安全法?讓我們先來看個例子。

瀏覽器的貼心服務

瀏覽器是一個貼心的程式,為了因應各種情況以及符合規格,你眼中的 HTML,不一定會是最後呈現出來的,例如說下面這個範例:

<!DOCTYPE html>
<html>
<body>
  <div id=content></div>
  <script>
    content.innerHTML = '<table><h1>hello</h1></table>'
  </script>
</body>
</html>

<table> 裡面放一個 <h1>,看起來沒什麼問題,但如果你打開了這個網頁,會發現:

https://ithelp.ithome.com.tw/upload/images/20230911/200913464k8Z9n3N2f.png

HTML 的結構變了!

變成了:

<h1>hello</h1>
<table></table>

原本應該在 <table> 中的 <h1>,從裡面「跳出來了」,會有這樣的結果是因為瀏覽器根據 HTML 規格,判定 <h1> 不該在 <table> 中,因此就很貼心地把它拿了出來。從網頁的發展史來看是很正常的,當 HTML 不合法時,瀏覽器總是會先試著修它,畢竟這樣總比拋出錯誤或是顯示白畫面好。

這種「HTML 字串 render 時會被瀏覽器改變」的行為,就稱作 mutation,而利用這個特性所達成的 XSS,自然而然就叫做 mutation XSS。

我們再看一個例子:

<!DOCTYPE html>
<html>
<body>
  <div id=content></div>
  <script>
    content.innerHTML = '<svg><p>hello</svg>'
  </script>
</body>
</html>

render 出來的結果為:

https://ithelp.ithome.com.tw/upload/images/20230911/20091346tg1WHcxjIX.png

瀏覽器認為 <p> 不應該在 <svg> 裡面,所以把在 <svg> 裡面的 <p> 給搬了出來,還順便修復了 HTML,幫忙加上 </p>

那這個更奇怪的例子呢?這次不是用 <p>,而是 </p>

<!DOCTYPE html>
<html>
<body>
  <div id=content></div>
  <script>
    content.innerHTML = '<svg></p>hello</svg>'
  </script>
</body>
</html>

結果為:

<svg><p></p>hello</svg>

瀏覽器自動修復了 </p>,幫前面加上了 <p>,但是標籤依舊在 <svg> 裡面。

(備註:現在 Chrome 瀏覽器的行為已經修復,會是 <svg></svg><p></p>hello,所以現在沒辦法測出來這個狀況,但讓我們先繼續看下去)

這時候有趣的事情來了,如果我們把 <svg><p></p>hello</svg> 再拿去丟給 innerHTML,結果會變什麼呢?

<!DOCTYPE html>
<html>
<body>
  <div id=content></div>
  <script>
    content.innerHTML = '<svg><p></p>hello</svg>'
    console.log(content.innerHTML)
  </script>
</body>
</html>

結果為:

<svg></svg>
<p></p>
hello

不只是 <p>,連後面的 hello 也一起跳了出來,等於是所有原本在 <svg> 裡的東西,現在都在 <svg> 外了。

那這一連串的變化,對我們想要繞過 sanitizer 有什麼幫助呢?這就要再結合前面提過的 sanitizer 流程了。

假設我們的 inputHtml 是這樣:<svg></p>hello</svg>,而 sanitizer 的第一步就是把這個解析成 DOM tree,根據之前的實驗,會變成:

<svg>
  <p></p>
  hello
</svg>

看起來完全沒問題,沒有需要過濾的東西,下一步就是把 DOM tree 序列化成字串,變為:<svg><p></p>hello</svg>

接著我們自己的前端拿到了 safeHtml,執行 document.body.innerHTML = safeHtml,最終的 HTML 就是上面寫過的:

<svg></svg>
<p></p>
hello

對 sanitizer 來說,<p> 跟 hello 是在 svg 裡面的,但是最終的結果卻不是這樣,會是在外面。因此透過這個 mutation,我們可以讓任意元素從 <svg> 裡面跳出來。

接下來你可能會問說:「所以勒?那有什麼用?」,這就是有趣的地方了。

神奇的 HTML

<style> 是個神奇的標籤,因為在這標籤裡面的東西都會被解讀為文字,例如說:

<!DOCTYPE html>
<html>
<body>
  <style>
    <a id="test"></a>
  </style>
</body>
</html>

會被解析為:

https://ithelp.ithome.com.tw/upload/images/20230911/20091346GtI3zUPovp.png

黑字就代表是純文字的節點。

但有趣的是,如果在外面加上一層 <svg>,瀏覽器解析的方式就會不同,一切就都不一樣了,現在的 HTML 原始碼是:

<!DOCTYPE html>
<html>
<body>
  <svg>
    <style>
      <a id="test"></a>
    </style>
  </svg>
</body>
</html>

解析的結果為:

https://ithelp.ithome.com.tw/upload/images/20230911/20091346g700Q1Fw64.png

<style> 裡面的 <a> 變成了真的 HTML 元素,而不是純文字。

更有趣的是,你可以構造這樣一串 HTML:

<svg>
  <style>
    <a id="</style><img src=x onerror=alert(1)>"></a>
  </style>
</svg>

呈現出來是:

https://ithelp.ithome.com.tw/upload/images/20230911/2009134622jSkBJg4Y.png

這邊只是幫 <a> 加上了一個 id,內容為 </style><img src=x onerror=alert(1)>,雖然說有 </style> 但是並不會閉合之前的 <style>,而是會被當作 id 屬性的一部分。後面的 <img> 也是,它不是一個新的標籤,只是屬性內容的一部分。

但如果把 svg 給拿掉,變成:

<style>
  <a id="</style><img src=x onerror=alert(1)>"></a>
</style>

因為 <a> 現在不是一個元素了,只是單純的純文字,所以也沒有什麼屬性可言,因此這邊的 </style> 就會關閉前面的 <style>,變成:

https://ithelp.ithome.com.tw/upload/images/20230911/20091346UgESbsbLSP.png

原本在 <a> 的 id 裡面的 <img> 只是屬性的內容,現在卻因為前面的 </style> 而跑了出來,變成了一個真的 HTML 元素。

從上面的實驗中我們可以得知,<style> 外層有沒有 <svg> 是重要的,因為會影響瀏覽器的解析。

全部加在一起

一開始我們提到了瀏覽器的 mutation,讓我們能做到「讓所有元素從 <svg> 中跳出來」,剛剛又提到了「<style> 外層有沒有 <svg> 是重要的」,把這兩個加在一起,就變成了最後的終極大魔王:mXSS。

在 2019 年 9 月 19 號,DOMPurify 釋出了 2.0.1 版本,目的是修正一個利用 mutation 來繞過檢查的 mXSS 漏洞,當時有問題的完整 payload 是這樣:

<svg></p><style><a id="</style><img src=1 onerror=alert(1)>">

把這個解析成 DOM tree 之後會變成底下的結構:

<svg>
  <p></p>
  <style>
    <a id="</style><img src=1 onerror=alert(1)>"></a>
  </style>
</svg>

瀏覽器在這邊做了幾件事:

  1. </p> 變成 <p></p>
  2. 自動閉合 <svg><style><a> 這些標籤

接著 DOMPurify 根據這個 DOM tree 檢查,因為 <svg><p><style><a> 都是允許的標籤,id 也是允許的屬性,所以都沒問題,於是就回傳序列化後的結果:

<svg>
  <p></p>
  <style>
    <a id="</style><img src=1 onerror=alert(1)>"></a>
  </style>
</svg>

然後使用者的程式把上面這個字串丟給 innerHTML,此時前面一再提到的 mutation 發生,所有的標籤都從 <svg> 裡面跳了出來,最終變成:

<svg></svg>
<p></p>
<style><a id="</style>
<img src=1 onerror=alert(1)>
"></a>
</style>

因為 <style> 也跳了出來,導致 <a> 元素不復存在,變成了純文字,因此 </style> 就提前閉合,讓原本隱藏著的 <img> 也從屬性內容變成了標籤,最後演變成 XSS。

問題的修復

為了修復這個問題,DOMPurify 在程式碼裡面特別加上了檢查,來避免受到 mXSS 的影響。

而同時這個問題也回報給了 Chromium,因為是 parser 的行為錯誤才導致了這個奇怪的 mutation:Issue 1005713: Security: Parser bug can introduce mXSS and HTML sanitizers bypass,結果在討論中開發者們發現這樣做是符合規格的,也就是說,這其實是一個 HTML 規格的 bug!

所以這個問題就變成規格也要修了,因此他們在規格的 repo 開了一個 issue:Unmatched p or br inside foreign context needs a special parser rule #5113

最後的結果就是在規格中補上了新的規則,而 Chromium 也根據新的規則修復了這個漏洞。

於是之後就再也沒有產生過類似的漏洞,所有人都過著幸福快樂的生活⋯⋯嗎?

並沒有,後來 DOMPurify 又被發現了一個更複雜一點的繞過方式,但修復之後變得更加強壯,在那之後基本上都沒有發生問題。

當時發現這個問題的人叫做 Michał Bentkowski,是個非常熟悉前端資安的前輩,回報過各種大大小小的問題,對於 HTML parsing 還有各種機制都很了解,我們之後還會看到他所回報過的經典漏洞。

想要更深入研究這個問題的話,可以參考他以前寫過的文章,我的 mXSS 知識都是從他那邊學來的:

  1. Write-up of DOMPurify 2.0.0 bypass using mutation XSS
  2. Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass
  3. HTML sanitization bypass in Ruby Sanitize < 5.2.1

小結

當初在接觸 mXSS 的時候只覺得懵懵懂懂的,好像瞭解了又好像沒有,為了寫這篇文章重新再順一遍脈絡跟自己試了一遍,才覺得好像有理解到底在幹嘛。要理解它的概念不難,但要理解所有的細節就需要多花一點時間,而且當初發現的問題已經修復了,所以在現在的瀏覽器上面沒辦法重現,也是比較麻煩的地方。

但總之呢,我覺得 mXSS 算是 XSS 裡面比較進階的主題了,牽涉到了 HTML 的 spec、瀏覽器的解析以及 sanitizer 的運作方式,要多花一點時間理解是正常的。


上一篇
[Day10] 繞過你的防禦:常見的 CSP bypass
下一篇
[Day12] 最強的 XSS:Universal XSS
系列文
Beyond XSS:探索網頁前端資安宇宙31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言