详解前端安全之JavaScript防http劫持与XSS(5)

我们定义了一个installHook方法,参数是一个window,在这个方法里,我们将重写传入的window下的 setAttribute ,并且安装一个MutationObserver,并对此窗口下未来可能创建的iframe进行监听,如果未来在此window下创建了一个 iframe ,则对新的iframe也装上installHook方法,以此进行层层保护。

重写 document.write

根据上述的方法,我们可以继续挖掘一下,还有什么方法可以重写,以便对页面进行更好的保护。

document.write是一个很不错选择,注入攻击者,通常会使用这个方法,往页面上注入一些弹窗广告。

我们可以重写document.write,使用关键词黑名单对内容进行匹配。

什么比较适合当黑名单的关键字呢?我们可以看看一些广告很多的页面:

详解前端安全之JavaScript防http劫持与XSS

这里在页面最底部嵌入了一个 iframe ,里面装了广告代码,这里的最外层的 id 名id="BAIDU_SSP__wrapper_u2444091_0"就很适合成为我们判断是否是恶意代码的一个标志,假设我们已经根据拦截上报收集到了一批黑名单列表:

// 建立正则拦截关键词 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ];

接下来我们只需要利用这些关键字,对document.write传入的内容进行正则判断,就能确定是否要拦截document.write这段代码。 

// 建立关键词黑名单 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ]; /** * 重写单个 window 窗口的 document.write 属性 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function resetDocumentWrite(window) { var old_write = window.document.write; window.document.write = function(string) { if (blackListMatch(keywordBlackList, string)) { console.log('拦截可疑模块:', string); return; } // 调用原始接口 old_write.apply(document, arguments); } } /** * [黑名单匹配] * @param {[Array]} blackList [黑名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function blackListMatch(blackList, value) { var length = blackList.length, i = 0; for (; i < length; i++) { // 建立黑名单正则 var reg = new RegExp(whiteList[i], 'i'); // 存在黑名单中,拦截 if (reg.test(value)) { return true; } } return false; }

我们可以把resetDocumentWrite放入上文的installHook方法中,就能对当前 window 及所有生成的 iframe 环境内的document.write进行重写了。

锁死 apply 和 call

接下来要介绍的这个是锁住原生的 Function.prototype.apply 和 Function.prototype.call 方法,锁住的意思就是使之无法被重写。

这里要用到Object.defineProperty,用于锁死 apply 和 call。

Object.defineProperty

Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

Object.defineProperty(obj, prop, descriptor)

其中:

obj – 需要定义属性的对象

prop – 需被定义或修改的属性名

descriptor – 需被定义或修改的属性的描述符

我们可以使用如下的代码,让 call 和 apply 无法被重写。

// 锁住 call Object.defineProperty(Function.prototype, 'call', { value: Function.prototype.call, // 当且仅当仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变 writable: false, // 当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除 configurable: false, enumerable: true }); // 锁住 apply Object.defineProperty(Function.prototype, 'apply', { value: Function.prototype.apply, writable: false, configurable: false, enumerable: true }); 

为啥要这样写呢?其实还是与上文的重写 setAttribute有关。

虽然我们将原始 Element.prototype.setAttribute 保存在了一个闭包当中,但是还有奇技淫巧可以把它从闭包中给“偷出来”。

试一下:

(function() {})( // 保存原有接口 var old_setAttribute = Element.prototype.setAttribute; // 重写 setAttribute 接口 Element.prototype.setAttribute = function(name, value) { // 具体细节 if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {} // 调用原始接口 old_setAttribute.apply(this, arguments); }; )(); // 重写 apply Function.prototype.apply = function(){ console.log(this); } // 调用 setAttribute document.getElementsByTagName('body')[0].setAttribute('data-test','123'); 

猜猜上面一段会输出什么?看看:

详解前端安全之JavaScript防http劫持与XSS

居然返回了原生 setAttribute 方法!

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygzgf.html