在本文中,我们将重点分析如何绕过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="//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="//example.com/assets/A.js" nonce="secret"></script>
<!-- This will not load -->
<script src="//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团队进行的研究,从而让我关注到这一漏洞。谢谢!