Firefox 严格动态CSP绕过漏洞(CVE-2018-5175)深度解析

本文深入分析了Firefox 60中修复的一个通用内容安全策略(CSP)严格动态绕过漏洞(CVE-2018-5175),详细解释了`strict-dynamic`机制、已知绕过方式以及如何利用Firefox开发者工具中的require.js实现绕过。

CVE-2018-5175:Firefox中的通用CSP严格动态绕过漏洞

在这篇博客文章中,我将介绍一个在Firefox 60中修复的CSP strict-dynamic绕过漏洞。

https://www.mozilla.org/en-US/security/advisories/mfsa2018-11/#CVE-2018-5175

此漏洞是一个绕过内容安全策略(CSP)保护的机制,影响那些脚本源策略中包含'strict-dynamic'的网站。如果目标网站存在HTML注入漏洞,攻击者可以注入一个引用Firefox开发者工具内置的require.js库的链接,然后利用该库的已知技术来绕过CSP对执行注入脚本的限制。

什么是“严格动态”?

也许你应该去读一下CSP规范:)https://www.w3.org/TR/CSP3/#strict-dynamic-usage

但为了练习英语写作,我将解释一下strict-dynamic。如果你已经了解它,可以跳过本节。

众所周知,CSP通过白名单域名来限制资源的加载。

例如,以下CSP设置只允许从自身源和trusted.example.com加载JavaScript:

1
Content-Security-Policy: script-src 'self' trusted.example.com

得益于这个CSP,即使页面存在XSS漏洞,也能防止执行来自内联脚本或evil.example.org的JavaScript文件。这看起来足够安全,然而,如果trusted.example.com存在任何可以绕过CSP的脚本,仍然可能执行JavaScript。更具体地说,如果trusted.example.com有一个JSONP端点,它可能会被这样绕过:

1
<script src="//trusted.example.com/jsonp?callback=alert(1)//"></script>

如果此端点将用户输入到callback参数的内容直接反射为回调函数名,则可以被用作任意脚本,如下所示:

1
alert(1)//({});

此外,众所周知,AngularJS也可用于绕过CSP。如果允许像CDN这样托管许多JavaScript文件的域名,这种绕过可能性就变得更加现实。

这样一来,在白名单模式下,有时很难安全地操作CSP。为了解决这个问题,设计了strict-dynamic。这是一个使用示例:

1
Content-Security-Policy: script-src 'nonce-secret' 'strict-dynamic'

此CSP意味着白名单将被禁用,只有具有nonce属性且值为"secret"字符串的脚本才会被加载。

1
2
3
4
<!-- 这个会被加载 -->
<script src="//example.com/assets/A.js" nonce="secret"></script>
<!-- 这个不会被加载 -->
<script src="//example.com/assets/B.js"></script>

A.js可能想要加载并使用另一个JavaScript。为了允许这一点,CSP规范允许在没有正确nonce属性的情况下加载JavaScript,条件是拥有正确nonce的js在特定条件下加载另一个js。用规范中的话说,非"parser-inserted"的脚本元素可以被允许执行JavaScript。

以下是一些允许加载的JavaScript类型的具体示例:

1
2
3
4
5
6
7
/* A.js */
// 这个会被加载
var script=document.createElement('script');
script.src='//example.org/dependency.js';
document.body.appendChild(script);
// 这个不会被加载
document.write("<scr"+"ipt src='//example.org/dependency.js'></scr"+"ipt>");

当使用createElement()加载时,它是一个非"parser-inserted"脚本元素,允许加载。另一方面,当使用document.write()加载时,它是一个"parser-inserted"脚本元素,不会被加载。

至此,我大致解释了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的CSP,加载了require.js,并且有一个简单的XSS漏洞。在这种情况下,如果插入了以下脚本元素,攻击者可以在没有正确nonce的情况下执行任意JavaScript。

1
2
3
4
5
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret' 'strict-dynamic'">
<!-- XSS 开始 -->
<script data-main="data:,alert(1)"></script>
<!-- XSS 结束 -->
<script nonce="secret" src="require.js"></script>

当require.js发现一个带有data-main属性的脚本元素时,它会从等效于以下代码加载data-main属性中指定的脚本:

1
2
3
var node = document.createElement('script');
node.src = 'data:,alert(1)';
document.head.appendChild(node);

如前所述,strict-dynamic允许通过createElement()加载没有正确nonce的JavaScript。

这样,在某些情况下,你可以利用已加载JavaScript代码的行为来绕过CSP strict-dynamic。Firefox的漏洞正是由require.js的这种行为引起的。在下一节中,我将解释这个漏洞。

通用严格动态绕过(CVE-2018-5175)

Firefox使用旧式扩展来实现一些浏览器功能。旧式扩展指的是基于XUL/XPCOM的扩展,这些扩展在Firefox 57中被移除,而非WebExtensions。即使在最新的Firefox 60上,浏览器内部仍然使用这种机制。

在此次绕过中,我们使用了浏览器内部使用的旧式扩展的一个资源。在WebExtensions中,通过在清单中设置web_accessible_resources键,列出的资源就可以从任何网页访问。旧式扩展有一个类似的选项,名为contentaccessible标志。在此次绕过中,它被用于绕过CSP,因为浏览器内部资源的require.js由于contentaccessible=yes标志,可以从任何网页访问。

让我们看看清单。如果你在Windows上使用64位Firefox,可以从以下URL看到清单:

1
jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/chrome.manifest
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

黄色部分是使文件可以从任何网站访问的部分。这两行用于创建resource: URI。第一行的resource devtools devtools/modules/devtools/devtools/modules/devtools/目录(位于jar:file:///C:/Program%20Files%20(x86)/Mozilla%20Firefox/browser/omni.ja!/chrome/devtools/modules/devtools/)映射到resource://devtools/

现在,我们可以通过使用Firefox打开resource://devtools/来访问该目录下的文件。同样,下一行映射到resource://devtools-client-jsonview/。这个URL通过contentaccessible=yes标志变得可以从网络访问,我们现在可以从任何网页加载该目录下的文件。

该目录下有一个可用于绕过CSP的require.js。只需将此require.js加载到使用CSP strict-dynamic的页面,你就可以绕过strict-dynamic

实际的绕过方法如下: https://vulnerabledoma.in/fx_csp_bypass_strict-dynamic.html

1
2
3
4
5
<meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-secret' 'strict-dynamic'">
<!-- XSS 开始 -->
<script data-main="data:,alert(1)"></script>
<script src="resource://devtools-client-jsonview/lib/require.js"></script>
<!-- XSS 结束 -->

从这段代码中,data: URL将作为JavaScript资源被加载,并弹出一个警告对话框。

你可能会想,“嗯,为什么require.js被加载了?它应该被CSP阻止,因为脚本元素没有正确的nonce。”

实际上,无论你设置多么严格的CSP规则,扩展的web-accessible资源都会忽略CSP而被加载。这种行为在CSP规范中有所提及: https://www.w3.org/TR/CSP3/#extensions

对资源实施的策略不应干扰用户代理功能(如附加组件、扩展或书签工具)的操作。这些功能通常将用户的优先级置于页面作者之上,正如[HTML-DESIGN]中所阐述的那样。

Firefox的resource: URI也遵循此规则。多亏了这一点,即使在设置了CSP的页面上,用户也可以按预期使用扩展的功能,但另一方面,这种特权有时可用于绕过CSP,就像这个bug的情况一样。

当然,这个问题不仅限于浏览器内部资源。即使在普通的浏览器扩展上,如果存在可用于绕过CSP的web-accessible资源,也会发生同样的事情。

似乎Firefox团队通过将页面的CSP应用于resource: URI来修复了这个bug。

文章结尾

我写到了Firefox的一个CSP strict-dynamic绕过漏洞。

顺便提一下,我是在寻找我自己制作的Cure53 CNY XSS Challenge 2018第三关的另一种解决方案时发现了这个问题。在那个挑战中,我使用了另一种技巧来绕过strict-dynamic。如果你感兴趣,请查看一下。

此外,我创建了一个不同版本的XSS挑战,并且仍然在等待你的答案:)

最后,我要感谢Google的研究,它让我注意到了这个bug。谢谢!

发布者:Masato Kinugawa 时间:凌晨2:02

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计