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:
|
|
得益于这个 CSP,即使页面存在 XSS 漏洞,也能防止页面执行来自内联脚本或 evil.example.org 的 JavaScript 文件。这看起来足够安全,然而,如果 trusted.example.com 有任何用于绕过 CSP 的脚本,仍然可能执行 JavaScript。更具体地说,如果 trusted.example.com 有一个 JSONP 端点,可能会像这样被绕过:
|
|
如果该端点将传递给 callback 参数的用户输入直接反射为回调函数名,它就可以被用作任意脚本:
|
|
此外,已知 AngularJS 也可用于绕过 CSP。这种绕过可能性变得更加现实,特别是当允许托管许多 JavaScript 文件的域名(例如 CDN)时。
这样一来,在白名单模式下,有时难以安全地操作 CSP。为了解决这个问题,strict-dynamic 被设计出来。以下是使用示例:
|
|
这个 CSP 意味着白名单将被禁用,只有在其 nonce 属性中包含 “secret” 字符串的脚本才会被加载。
|
|
A.js 可能想要加载并使用另一个 JavaScript。为了允许这种情况,CSP 规范允许在特定条件下,由具有正确 nonce 的脚本加载的另一个脚本可以不需要正确的 nonce 属性。用规范中的话说,非 “parser-inserted” 的 script 元素可以被允许执行 JavaScript。
以下是一些具体示例,说明什么类型的 JavaScript 是被允许的:
|
|
当使用 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。
|
|
当 require.js 找到一个带有 data-main 属性的 script 元素时,它会从等同于以下代码的方式加载 data-main 属性中指定的脚本:
|
|
如前所述,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
|
|
高亮部分是使文件可从任何网站访问的部分。这两行用于创建 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
|
|
在这段代码中,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 的研究让我注意到了这个漏洞。谢谢!