在本文中,我們將重點分析如何繞過Firefox內容安全策略中的“Strict-Dynamic”限制。該漏洞詳情請參考: https://www.mozilla.org/en-US/security/advisories/mfsa2018-11/#CVE-2018-5175 。該漏洞將繞過內容安全策略(CSP)的保護機制,而在該機制中包含一個“嚴格動態限制”的Script-src策略。如果目標網站中存在HTTP注入漏洞,攻擊者可以將一個引用注入到require.js庫的一個副本中,這個庫位于Firefox開發人員工具之中,攻擊者隨后便可以使用已知技術,利用該庫繞過CSP限制,從而執行注入腳本。
各位讀者可能已經閱讀過內容安全策略的規范( https://www.w3.org/TR/CSP3/#strict-dynamic-usage ),但在這里,我還是有必要先對“Strict-Dynamic”(嚴格動態限制)進行解釋。如果讀者已經完全掌握相關知識,可以跳過本節的閱讀。
眾所周知的內容安全策略(CSP)限制,其原理是通過將域名列入白名單來限制資源的加載。舉例來說,下面的CSP設置僅允許從其自身的來源和trusted.example.com域名加載JavaScript:
Content-Security-Policy: script-src 'self' trusted.example.com
由于這個內容安全策略的存在,即使在頁面中存在XSS漏洞,該頁面也無法通過內聯腳本或evil.example.org的JavaScript文件來執行JavaScript腳本。這一策略看起來確實足夠安全,但是,如果在trusted.example.org中存在任何繞過內容安全策略的腳本,那么就仍然可以執行JavaScript。更具體地說,如果在trusted.example.com中存在一個JSONP端點,那么就有可能被繞過,如下所示:
<script src="http://trusted.example.com/jsonp?callback=alert(1)//"></script>
如果此端點直接將用戶輸入的參數傳遞給callback函數,那么就可以執行任意腳本,示例中的腳本如下:
alert(1)//({});
另外,目前已知AngularJS也可以用于繞過內容安全策略( https://github.com/cure53/XSSChallengeWiki/wiki/H5SC-Minichallenge-3:-%22Sh*t,-it%27s-CSP!%22#127-bytes )。這種繞過方式的利用可能會更為實際,特別適用于允許托管許多JavaScript文件(如CDN)的域名。
這樣一來,即使在白名單中,有時也很難通過內容安全策略來保障安全性。為了解決這一問題,就設計了“Strict-Dynamic”的限制。其用法示例如下:
Content-Security-Policy: script-src 'nonce-secret' 'strict-dynamic'
這就意味著白名單將被禁用,并且只有在nonce屬性中具有“secret”字符串的腳本才會被加載。
<!-- This will load -->
<script src="http://example.com/assets/A.js" nonce="secret"></script>
<!-- This will not load -->
<script src="http://example.com/assets/B.js"></script>
在這里,A.js可能想要加載并使用另一個JavaScript。為了實現這一點,內容安全策略規范中允許具有正確nonce屬性的JavaScript,在特定條件下加載沒有正確nonce屬性的JavaScript。使用規范中的關鍵詞,就可以允許非解析型腳本(Parser-Inserted Script)元素執行JavaScript。
示例如下:
/* A.js */
//This will load
var script=document.createElement('script');
script.src='//example.org/dependency.js';
document.body.appendChild(script);
//This will not load
document.write("<scr"+"ipt src='//example.org/dependency.js'></scr"+"ipt>");
當使用createElement()加載時,它是一個非解析型腳本元素,該加載動作被允許。另一個反例是,使用document.write()加載時,它是一個解析型腳本元素(Parser-Inserted Script Element),所以不會被加載。
到目前為止,我已經大致地解釋了“Strict-Dynamic”。順便要提一句,“Strict-Dynamic”在某些情況下是可以被繞過的。下面我就介紹一種已知的“Strict-Dynamic”的繞過方式。
如果在目標頁面中使用特定的庫,那么Strict-Dynamic就可以被繞過。
該繞過方式已經由Google的Sebastian Lekies、Eduardo Vela Nava、Krzysztof Kotowicz進行測試,受影響的庫請參見: https://github.com/google/security-research-pocs/blob/master/script-gadgets/bypasses.md 。
接下來,我們來看看這個列表中借助require.js實現Strict-Dynamic繞過的方法。
假設目標頁面使用了Strict-Dynamic的內容安全策略,并且加載require.js,同時具有簡單的XSS漏洞。在這種情況下,如果輸入以下腳本元素,攻擊者就可以在沒有正確的nonce的情況下執行任意JavaScript。
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret' 'strict-dynamic'">
<!-- XSS START -->
<script data-main="data:,alert(1)"></script>
<!-- XSS END -->
<script nonce="secret" src="require.js"></script>
當require.js找到一個具有data-main屬性的腳本元素時,它會加載data-main屬性中指定的腳本,其等效代碼如下:
var node = document.createElement('script');
node.url = 'data:,alert(1)';
document.head.appendChild(node);
如前所述,Strict-Dynamic允許從createElement()加載沒有正確nonce的JavaScript腳本。這樣一來,就可以借助某些已經加載的JavaScript代碼行為,在某種情況下繞過內容安全策略的Strict-Dynamic。而在Firefox中的漏洞,正是由于require.js的這種情況引起的。
Firefox使用一些傳統的擴展實現了部分瀏覽器功能。在Firefox 57版本中,移除了基于XUL/XPCOM的擴展,但沒有移除WebExtensions。即使是在最新的60版本中,瀏覽器內部仍然使用這種機制。
要利用這一漏洞,我們首先要借助瀏覽器內部使用的傳統擴展資源。在WebExtensions中,通過在manifest中設置web_accessible_resources項( https://developer.mozilla.org/en/Add-ons/WebExtensions/manifest.json/web_accessible_resources ),就可以從任何網頁中訪問所列出的資源。傳統擴展中有一個名為contentaccessible標志的類似選項( https://developer.mozilla.org/ja/docs/Mozilla/Chrome_Registration#contentaccessible )。我們這一漏洞,正是通過將contentaccessible標志設置為yes,從而讓瀏覽器內部資源的require.js可以被任意Web頁面訪問,最終實現內容安全策略的繞過。
接下來,我們具體分析一下manifest。如果是Windows環境下的64位Firefox,我們可以通過以下URL查看到manifest:
jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/chrome.manifest
content branding browser/content/branding/ contentaccessible=yes
content browser browser/content/browser/ contentaccessible=yes
skin browser classic/1.0 browser/skin/classic/browser/
skin communicator classic/1.0 browser/skin/classic/communicator/
content webide webide/content/
skin webide classic/1.0 webide/skin/
content devtools-shim devtools-shim/content/
content devtools devtools/content/
skin devtools classic/1.0 devtools/skin/
locale branding ja ja/locale/branding/
locale browser ja ja/locale/browser/
locale browser-region ja ja/locale/browser-region/
locale devtools ja ja/locale/ja/devtools/client/
locale devtools-shared ja ja/locale/ja/devtools/shared/
locale devtools-shim ja ja/locale/ja/devtools/shim/
locale pdf.js ja ja/locale/pdfviewer/
overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul
overlay chrome://browser/content/places/places.xul chrome://browser/content/places/downloadsViewOverlay.xul
overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul
overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul
override chrome://global/content/license.html chrome://browser/content/license.html
override chrome://global/content/netError.xhtml chrome://browser/content/aboutNetError.xhtml
override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties
override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd
override chrome://mozapps/locale/downloads/settingsChange.dtd chrome://browser/locale/downloads/settingsChange.dtd
resource search-plugins chrome://browser/locale/searchplugins/
resource usercontext-content browser/content/ contentaccessible=yes
resource pdf.js pdfjs/content/
resource devtools devtools/modules/devtools/
resource devtools-client-jsonview resource://devtools/client/jsonview/ contentaccessible=yes
resource devtools-client-shared resource://devtools/client/shared/ contentaccessible=yes
上面的倒數第2、3行,就是使文件可以從任意Web站點訪問的部分。這兩行用于創建一個resource: URI( https://developer.mozilla.org/en-US/docs/Mozilla/Chrome_Registration#resource )。倒數第三行中,resource devtools 會將devtools/modules/devtools/目錄映射到resource://devtools/,該目錄存在于jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/devtools/modules/devtools/ 。
現在,我們可以使用Firefox,通過resource://devtools/來訪問目錄下的文件。同理,倒數第二行是映射到resource://devtools-client-jsonview/ 。該URL可以通過contentaccessible=yes標志來實現Web訪問,我們現在可以從任意Web頁面加載放在該目錄下的文件。
在該目錄中,有一個用于繞過內容安全策略的require.js。只需要將該require.js加載到使用內容安全策略Strict-Dynamic的頁面中,即可實現Strict-Dynamic的繞過。
實際繞過操作如下:
https://vulnerabledoma.in/fx_csp_bypass_strict-dynamic.html
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret' 'strict-dynamic'">
<!-- XSS START -->
<script data-main="data:,alert(1)"></script>
<script src="resource://devtools-client-jsonview/lib/require.js"></script>
<!-- XSS END -->
在這段代碼中,我們看到,data:URL將作為JavaScript資源加載,并且會彈出一個警告對話框。
各位讀者可能會想,為什么會加載require.js?由于腳本元素沒有正確的nonce,理論上它應該會被內容安全策略所阻止。
實際上,無論對內容安全策略設置多么嚴格的規則,擴展程序的Web可訪問資源都會在忽略內容安全策略的情況下被加載。這種行為在內容安全策略的規范中也有所提及:
https://www.w3.org/TR/CSP3/#extensions
“Policy enforced on a resource SHOULD NOT interfere with the operation of user-agent features like addons, extensions, or bookmarklets. These kinds of features generally advance the user’s priority over page authors, as espoused in [HTML-DESIGN].”
“對資源執行的策略不應該干擾用戶代理功能(如插件、擴展或書簽)進行的操作。這些類型的功能通常會提高用戶的優先級,正如[HTML-DESIGN]中所提到的。”
Firefox的resource: URI也存在這一規則。受此影響,用戶甚至可以在設置了內容安全策略的頁面上使用擴展的功能,但另一方面,這一特權有時會被用于繞過內容安全策略,本文所提及的漏洞就是如此。
當然,這個問題不僅僅出現在瀏覽器內部資源。即使在通用瀏覽器擴展中,如果有可以用于繞過內容安全策略的Web可訪問資源,也會發生同樣的情況。
根據推測,Firefox的開發人員是通過將頁面的內容安全策略應用到resource: URI中,從而實現對這一漏洞的修復。
在本文中,我們對于Firefox的內容安全策略Strict-Dynamic漏洞進行了分析。該漏洞是我在Cure53 CNY XSS Challenge 2018競賽( https://github.com/cure53/XSSChallengeWiki/wiki/CNY-Challenge-2018 )的第三級題目解題過程中發現的。在該競賽中,我使用了另一個技巧來繞過Strict-Dynamic,如果各位讀者有興趣,可以詳細查看。此外,我還創建了這個XSS挑戰賽的另一個版本( https://twitter.com/kinugawamasato/status/984014228469280768 ),也期待有興趣的同學能夠參與。
最后,感謝Google團隊進行的研究,從而讓我關注到這一漏洞。謝謝!