前几天,在油管上看到一个Goolge Search XSS的视频,感觉还是非常震惊的。都9102年了,竟然还能在Google首页找到XSS?

开门见山,XSS的payload是<noscript><p title="</noscript><img src=x onerror=alert(1)>">。看似平平无奇的payload,究竟是如何导致XSS的呢?下面,我们就一起来康康。

 

HTML Sanitization

为了防止XSS的发生,我们通常会对用户的输入进行sanitize(本意是消毒,净化。在此表示,对于用户输入中可能产生XSS的部分进行编码或过滤。既保证用户输入内容的完整性,同时也防止嵌入的JS脚本被执行)。目前,很多Web框架也能做到这些。但是,针对HTML的过滤,仍然存在一些问题。比如,在某些场景下,我们希望保留一些HTML标签。如下图的Gmail页面所示,b标签表示加粗,i标签表示倾斜。

Gmail content

服务端解析

针对这种需要过滤掉XSS的同时,保留部分HTML标签的情况。有人可能会想到,我们可以在实现一个HTML Sanitizer在服务端过滤掉可能导致XSS的输入。然而,实现这样的Sanitizer绝非易事。

通过下面这个例子,来康康为何困难?

下面是两个不完整的HTML代码片段,看起来,这两段代码有着相似的结构,divscript都有结束标签(</div></script>),但同时结束标签又是另一个标签的属性(title)的值。

1. <div><script title="</div>"> 2. <script><div title="</script>"> 

大家可以先思考一下,浏览器是如何解析他们的呢?

公布答案!

对于代码片段1,浏览器将其解析为一个div,内部包含一个script。并且自动补全了<head>, <body>, <html>等标签。

html fragment 1

看起来挺合理的。浏览器的解析顺序如下:

1.发现div开始标签。
2.发现script开始标签。
3.script标签内部有一个title属性,内容是</div>。其实,这里的值是什么都无所谓,浏览器都会把它看做一个字符串,并作为title的值。
4.自动补全script结束标签。
5.自动补全div结束标签。

对于代码片段2,浏览器的解析则完全不同。

html fragment 2

浏览器使用title属性里的</script>进行了闭合,并将div title="作为script标签的内容插入其中。</script>后面的">则作为普通的字符串插入到了<body>元素中。

浏览器在解析<script>标签的内容时,使用了JS Parser而不是HTML Parser,其内容<div title="被看作是JavaScript代码(当然,这段代码并不符合JavaScript语法规范)。

两段相同结构的HTML代码片段,却被解析成了完全不同的DOM结构。由此可见,浏览器解析HTML的规范非常复杂。而且,不同的浏览器在解析HTML的时候也可能存在不同的规范。所以,在服务器端实现一个HTML Sanitizer,并且兼容不同版本的不同浏览器对于HTML的解析规范是非常困难的。

客户端解析

聪明如你,可能已经想到。既然如此,我们不如在客户端对HTML进行sanitize。利用浏览器本身的Parser来解析HTML,然后我们只需要对解析后的结果进行sanitize即可。

浏览器正好提供了template标签,可以用来解析HTML,而且在解析过程中不会触发JavaScript脚本执行。

下面,通过对比普通的div标签,来说明template标签是如何工作的。

在浏览器的Console中执行下面这段代码

// 创建一个div元素
div = document.createElement('div');
// <img src=x onerror=alert(1) /> 作为XSS payload插入div
div.innerHTML = "<img src=x onerror=alert(1) />" 

如下图所示,毫无疑问,alert弹窗产生。

div alert

如果,我们将payload放到template元素中呢?

// 创建一个template元素 template = document.createElement('template'); // 将同样的payload插入template中 template.innerHTML = "<img src=x onerror=alert(1) />" 

没有alert弹窗产生。而且,我们能从template中拿到解析后的HTML,并进行sanitize。

// 查看解析后的HTML,结果为 <img src="x" rel="external nofollow"  onerror="alert(1)"> template.content.children[0] // sanitize HTML,删除可能导致XSS的危险属性 onerror template.content.children[0].removeAttribute("onerror"); // 将安全的HTML插入最终需要渲染的DOM节点上 div.innerHTML = template.content.children[0]; 

template alert

这样,我们就实现了通过浏览器的Parser来解析HTML,然后对解析后的HTML进行sanitize,以保证最终输出安全的HTML。而且,在解析过程中,浏览器也不会执行嵌入的JavaScript脚本。

看起来,似乎大功告成了?!

我们尝试用同样的方法来解析文章开头提供的XSS payload。

// 插入payload
template.innerHTML = '<noscript><p title="</noscript><img src=x onerror=alert(1)>">'
// 查看解析后的HTML
template.content.children[0]
// 返回的结果如下 <noscript> <p title="</noscript><img src=x onerror=alert(1)"></p> </noscript> 

template payload

解析后的HTML看起来非常安全,能够执行XSS的img标签被解析成了p标签的title属性的值,以字符串的形式存在。

接下来,我们将这段”安全”的HTML插入到div

div.innerHTML = template.innerHTML; 

template payload alert

竟然,弹窗了?!我们打印一下插入payload的div,看看究竟是个什么鬼?

div // 返回的结果如下 <div> <noscript><p title="</noscript>
    <img src="x" onerror="alert(1)"> "">" <p></p> </div> 

div template

</noscript>闭合了noscript标签,后面紧跟的img标签变成了一个合法的标签,而不再是title属性的值,从而导致了XSS的执行。

这就非常诡异了。同样是noscript标签,在divtemplate中为什么会出现差异呢?答案其实就在HTML规范中。

The noscript element represents nothing if scripting is enabled, and represents its children if scripting is disabled. It is used to present different markup to user agents that support scripting and those that don’t support scripting, by affecting how the document is parsed.

所以说,noscript允许JavaScript禁止JavaScript环境下的解析是不同的。普通的浏览器环境是允许JavaScript执行的,而template中是禁止JavaScript执行的。这也就解释了为什么对于noscript标签的解析会出现差异。

 

Google Search XSS

其实,Google Search XSS产生的原因,和前文demo中所展示的类似。由于Google早已修复了此问题,无法亲自验证。下文中出现的部分图片来源于油管视频的截图。

我们改动一下payload,在XSS执行处设置断点。

<noscript><p title="</noscript><img src=x onerror=debugger;>"> 

断点后,通过查看调用栈发现,在某处执行了a.innerHTML = b。将b打印出来,内容为<noscript><p title="</noscript><img src=x onerror=debugger;alert(1);>"></p></noscript>。依据前文中的经验,我们知道这段代码是有危害的。

google search xss 1

通过对比修复前和修复后的JavaScript文件,发现某处的a.innerHTML被修改为带有sanitizer的实现。看来问题就出在这里。

google search xss 2

同样,在a = a.innerHTML处添加断点,查看调用栈。发现Google的sanitizer也是采用了和前文中demo类似的方式,即template标签来解析HTML。

google search xss 3

Google使用的JavaScript Library叫做Google Closure。这是一个开源的JavaScript框架。我找到了修复XSS的commit,发现这个commit实际上对之前某个commit的rollback。

closure commit 1

然后找到之前导致XSS的commit,发现正是这个commit删除了某些sanitize语句,并且直接使用了innerHTML。

closure commit 2

closure commit 3

这个commit是2018年9月26日提交的,Google在2019年2月22日rollback。也就是说这个问题在Google Closure里存在了长达5个月之久,而很多Google产品本身也在使用Google Closure。

因吹斯汀!!!

 

总结

1.Google在HTML sanitization时,使用了templatetemplateJavaScript Disabled环境。
2.noscript标签在JavaScript EnabledJavaScript Disabled环境中的解析不一致,给XSS创造了可能。
3.Google本身有对输入进行额外的sanitization,但是,在某个修复其他问题的commit中被删掉了。

当前流行的Web框架对XSS防御的支持已经相当完善,XSS漏洞挖掘也变得越来越困难。但是,看完了这位的Google Search XSS漏洞之后,我才发现,挖不到XSS,本质上,菜是原罪!

参考:
Youtube: https://www.youtube.com/watch?v=lG7U3fuNw3A
漏洞发现者Twitter:https://twitter.com/kinugawamasato