Firefox CSP strict-dynamic 通用绕过漏洞详解 (CVE-2018-5175)

本文详细分析了CVE-2018-5175漏洞,该漏洞允许攻击者在Firefox浏览器中普遍绕过内容安全策略(CSP)的'strict-dynamic'指令。文章探讨了漏洞原理、已知的strict-dynamic绕过方法,以及Firefox内部遗留扩展资源如何导致这一通用绕过。作者还分享了其发现漏洞的背景及与Cure53 XSS挑战赛的关联。

CVE-2018-5175: Firefox 中的通用 CSP strict-dynamic 绕过

本文中,我想写一个关于 CSP strict-dynamic 绕过漏洞的文章,该漏洞已在 Firefox 60 中修复。

Mozilla 安全公告链接:MFSA2018-11/#CVE-2018-5175

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

什么是 “strict-dynamic”?

你可能应该阅读 CSP 规范 :) https://www.w3.org/TR/CSP3/#strict-dynamic-usage

但为了练习英语写作,我来解释一下 strict-dynamic。如果你已经了解 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。这种绕过可能性变得更加现实,特别是当允许托管许多 JavaScript 文件的域名(例如 CDN)时。

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

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

这个 CSP 意味着白名单将被禁用,只有在其 nonce 属性中包含 “secret” 字符串的脚本才会被加载。

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

<!-- 这个脚本不会加载 -->
<script src="//example.com/assets/B.js"></script>

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

以下是一些具体示例,说明什么类型的 JavaScript 是被允许的:

1
2
3
4
5
6
7
8
9
/* 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” 的 script 元素,加载被允许。另一方面,当使用 document.write() 加载时,它是一个 “parser-inserted” 的 script 元素,不会被加载。

至此,我粗略地解释了 strict-dynamic

顺便说一下,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。在这种情况下,如果插入了以下 script 元素,攻击者可以在没有正确 nonce 的情况下执行任意 JavaScript。

1
2
3
4
5
<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 属性的 script 元素时,它会从等同于以下代码的方式加载 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 的这种行为引起的。在下一节中,我将解释这个漏洞。

通用 strict-dynamic 绕过 (CVE-2018-5175)

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

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

让我们看一下 manifest 文件。如果你在使用 Windows 64 位的 Firefox,可以从以下 URL 看到这个 manifest:

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
31
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/ contentaccessible=yes
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/。通过 contentaccessible=yes 标志,此 URL 变得可从网页访问,我们现在可以从任何网页加载放置在此目录下的文件。

此目录下有一个可用于绕过 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 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 被加载了?它应该被 CSP 阻止,因为 script 元素没有正确的 nonce。”

实际上,无论你设置多么严格的 CSP 规则,扩展的网页可访问资源都会忽略 CSP 而被加载。CSP 规范中提到了这种行为:https://www.w3.org/TR/CSP3/#extensions

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

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

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

看起来 Firefox 团队通过将页面的 CSP 应用到 resource: URI 来修复了这个漏洞。

文章结尾

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

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

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

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

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