解决 DOM XSS 谜题
DOM 型跨站脚本(XSS)漏洞是我最喜欢利用的漏洞类型之一。这有点像解谜:有时你会遇到像 $.html() 这样的角落片段,其他时候则必须依赖试错。最近,我在漏洞赏金计划中遇到了两个有趣的 postMessage DOM XSS 漏洞,满足了我的解谜瘾。
注意:部分细节已匿名化。
谜题 A:邮差问题 🔗
postMessage 近年来成为 XSS 漏洞的常见来源。随着开发者转向客户端 JavaScript 框架,经典的服务器端渲染 XSS 漏洞消失了。相反,前端使用异步通信流(如 postMessage 和 WebSocket)来动态修改内容。
我使用 Frans Rosén 的 postmessage-tracker 工具来监控 postMessage 调用。这是一个 Chrome 扩展,每当检测到 postMessage 调用时,它会提醒你并枚举从源到接收器的路径。然而,尽管 postMessage 调用很多,但大多数往往是误报,需要手动验证。
在浏览公司 A 的网站 https://feedback.companyA.com/ 时,postmessage-tracker 通知我有一个特别有趣的调用,源自一个 iFrame https://abc.cloudfront.net/iframe_chat.html:
|
|
postMessage 处理程序检查消息数据(e.data)是否包含与 ChatSettings 匹配的 type 值。如果是,它将 window.settingsSync 设置为 e.data.iframeChatSettings。它没有执行任何来源检查——这对漏洞猎人来说总是一个好兆头,因为消息可以从任何攻击者控制的域发送。
window.settingsSync 用于什么?通过在 Burp 中搜索这个字符串,我发现了 https://abc.cloudfront.net/third-party.js:
|
|
如果 window.settingsSync.environment == "production",window.settingsSync.region 将被重新排列成 subdomain 并插入到 domain = 'https://'+subdomain+'.settingsSync.com 中。然后,这个 URL 将用于 POST 请求。响应将被解析为 JSON 并设置 window.settingsSync。接下来,window.settingsSync.versionNumber 用于构建一个 URL,加载一个新的 JavaScript 文件 var newScript = 'https://abc.cloudfront.net/module-v'+window.settingsSync.versionNumber+'.js'。
在典型场景中,页面会加载 https://abc.cloudfront.net/module-v2.js:
|
|
啊哈!eval 是一个简单的接收器,将其字符串参数作为 JavaScript 执行。如果我控制了 config,我就可以执行任意 JavaScript!
然而,我如何操纵 domain 以匹配我的恶意服务器而不是 *.settingsSync.com?我再次检查了代码:
|
|
我注意到,由于消毒不足和简单的拼接,像 .my.website/malicious.php?_bad 这样的 window.settingsSync.region 值将被重新排列成 https://bad-.my.website/malicious.php?.settingsSync.com!现在 domain 指向 bad-.my.website,一个有效的攻击者控制域,向 POST 请求提供恶意负载。
我在我的服务器上创建了 malicious.php,通过捕获来自原始目标的响应来发送有效响应。我将所选配置的名称修改为我的 XSS 负载:
|
|
基于这个响应,接收器现在将执行:
|
|
从我自己的域,我生成了包含易受攻击 iFrame 的页面,使用 var child = window.open("https://feedback.companyA.com/"),然后用 child.frames[1].postMessage(...) 发送 PostMessage 负载。就这样,弹出了警告框!
然而,我还需要最后一块拼图。由于 XSS 在 iFrame https://abc.cloudfront.net/iframe_chat.html 的上下文中执行,而不是在 https://feedback.companyA.com/ 中,因此没有实际影响;这就像在外部域上执行 XSS 一样。我需要以某种方式利用 iFrame 中的这个 XSS 来访问父窗口 https://feedback.companyA.com/。
幸运的是,https://feedback.companyA.com/ 包含了另一个有趣的 postMessage 处理程序:
|
|
https://feedback.companyA.com/ 创建了一个 PostMessage 监听器,验证消息来源为 https://abc.cloudfront.net。如果消息数据类型是 IframeLoaded,它会发送一个带有 credentialConfig 数据的 PostMessage 回传。
credentialConfig 包括一个会话令牌:
|
|
因此,通过发送 PostMessage 来触发 https://abc.cloudfront.net/iframe_chat.html 上的 XSS,XSS 将运行任意 JavaScript,从 https://abc.cloudfront.net/iframe_chat.html 发送另一个 PostMessage 到 https://feedback.companyA.com/,从而泄露会话令牌。
基于此,我修改了 XSS 负载:
|
|
XSS 从 https://feedback.companyA.com/ 的父 iFrame 接收会话数据,并将被盗的 sessionToken 外泄到攻击者控制的服务器(这里我简单地使用了 alert)。
谜题 B:用换行符开放重定向绕过 CSP 🔗
在探索公司 B 的 OAuth 流程时,我注意到其 OAuth 授权页面有些奇怪。通常,OAuth 授权页面会呈现某种确认按钮来链接账户。例如,这是 Twitter 的 OAuth 授权页面,用于登录 GitLab:
公司 B 的页面使用以下格式的 URL:https://accept.companyb/confirmation?domain=oauth.companyb.com&state=
|
|
通过玩弄这个响应数据,我意识到 introduction 被注入到页面中,没有任何消毒。如果我能控制 GET 请求的目的地以及随后的响应,就有可能引起 XSS。
幸运的是,似乎 domain 参数允许我控制 GET 请求的域。然而,当我将其设置为我自己的域时,请求未能执行并引发了内容安全策略(CSP)错误。我快速检查了页面的 CSP:
|
|
当进行动态 HTTP 请求时,它们遵循 connect-src CSP 规则。在这种情况下,default-src 规则意味着只允许对 *.companyb.com 和 *.googleapis.com 的请求。不幸的是,对于公司来说,*.googleapis.com 创建了一个大漏洞:由于 Google Cloud Storage 文件托管在 storage.googleapis.com 上,我仍然可以向我的攻击者控制桶发送请求!此外,CORS 不会成为问题,因为 Google Cloud 允许用户设置桶的 CORS 策略。
我快速在 https://storage.googleapis.com/myevilbucket/oauth_data.json 上托管了一个 JSON 文件,内容为 <script>alert()</script>,然后浏览到 https://accept.companyb/confirmation?domain=storage.googleapis.com/myevilbucket/oauth_data.json%3F&state=
还有一个问题:script-src 的 CSP 只允许 self 或 *.companyb.com 用于 HTTPS。幸运的是,我在 t.companyb.com 上有一个开放重定向,为这种情况保存。易受攻击的端点会重定向到 url 参数的值,但验证参数是否以 companyb.com 结尾。然而,它允许在子域部分使用换行符 %0A,这将被浏览器截断,使得 http://t.companyb.com/redirect?url=http%3A%2F%2Fevil.com%0A.companyb.com%2F 实际上重定向到 https://evil.com/%0A.companyb.com/ 而不是。
通过使用这个绕过来创建开放重定向,我将最终的 XSS 负载保存在我的 Web 服务器文档根目录中的 <NEWLINE CHARACTER>.companyb.com。然后,我注入了一个脚本标签,其 src 指向开放重定向,这通过了 CSP 但最终重定向到最终负载。
结论 🔗
两家公司都因我的 XSS 报告的复杂性和绕过强化执行环境的能力而授予了奖金。希望通过记录我的思考过程,你也能获得一些额外的技巧来解决 DOM XSS 谜题。