終于趕上個想寫點東西的日子,原本打算繼續更新那拖了好久的流量劫持系列的博客和工具。不過中午閑來無事去烏云逛了圈,發現百度的漏洞又雙叒叕上頭條了!而且還是那個 BDUSS 泄露的問題,記不得這已經是第幾回了。盡管這事和咱沒啥關系,但本著拯救世界和平的目的,還是打算去研究下:)
既然是 cookie 泄露,無非就兩種。要么就是硬傷,例如之前 Apache 就會因為太多的請求頭,導致 HttpOnly 也被輸出到錯誤頁里,當然這種嚴重的漏洞,隨著補丁的更新很快就能解決;另一個當然就是內傷,由于程序猿的粗心大意,把重要的數據不加掩蓋就放到頁面里了。
前者很好解決,把在線的服務器都掃描一遍,修復一個就少一個。而后者就不那么容易了,產品經常更新迭代,誰也不能保證每次升級之后是否出現新的隱患。
既然找不到一勞永逸的方案,不如就用最簡單最原始的土辦法 —— 暴力窮舉:把網頁一個個抓回來,看看里面是否出現隱私數據了。當然你會說這也太挫太低效了,不過事實上這種方案還是有意義的,至少能很快的找出一些明顯的問題。而且在此基礎上,我們還可以再做優化和改進,讓它變得更完善。
說到抓網頁,大家總是先想到蜘蛛。雖然全自動化的流程是我們的終極目標,但在目前遐想階段,就開始搞這么復雜的一套系統,還是不合適。而且如今遍地都是 AJAX 的交互網頁,蜘蛛也很難爬到一些動態數據。
所以,不如先從最簡單的開始:Chrome 插件。在我們平時看網頁的時候,同時對頁面進行分析。這樣既節省了蜘蛛服務,而且還充分利用了真實的用戶行為,覆蓋到更多的動態交互內容。
使用 Chrome 插件來實現這個基本功能,是非常簡單的。只要得到了帶 HttpOnly 的 cookie 值,在瀏覽頁面時掃描下 HTML 字符就可以了。
首先得獲取瀏覽器的 cookie 數據庫,得到我們想要的。例如,我們獲取所有百度域下的 cookie:
#!js
chrome.cookies.getAll({domain: 'baidu.com'}, function(cookies) {
console.dir(cookies);
});
稍加過濾即可獲得 HttpOnly 的數據。
詳細 API 可以參考這里。關于 Chrome 插件開發這里就不詳細介紹了。
值得注意的是,有些 cookie 值很簡單,例如 1、true、ok 之類的;或者很常見,例如用戶名、日期數字等,這些都得排除掉,否則會有太多的匹配。
有了關鍵字列表,我們就可以對頁面內容做分析了。
我們新建一個 content 腳本,向 background 腳本獲取列表。之后在頁面的 DOMContentLoaded 事件里,即可對文檔的 HTML 進行關鍵字掃描了:
#!js
// content.js
chrome.extension.sendRequest('GET_LIST', function(list) {
window.addEventListener('DOMContentLoaded', function() {
var html = document.documentElement.outerHTML;
list.forEach(function(item) {
if (html.indexOf(item) >= 0) {
alert('found: ' + item);
}
});
});
});
// background.js
chrome.extension.onRequest.addListener(function(message, sender, sendResponse) {
if (message == 'GET_LIST') {
sendResponse(list);
}
});
到此,一個最簡易的隱私嗅探器完成了。我們馬上就來試驗下,看看是否有效果。
先試試百度首頁上的幾個產品。不到 10 秒鐘,就逮到了一個:
打開源文件一搜,果然看見了傳說中帶 HttpOnly 的 BDUSS:
趕緊測試其他頁面。開始的幾分鐘時間里,不斷發現新的泄漏點:
。。。
不過十分鐘后,出現的越來越少了。我們是不是漏了些什么東西?
顯然,如果只掃描 HTML 內容,這和爬蟲有什么區別?
我們之所以做成瀏覽器插件,就是為了能得到真實的用戶行為操作。要是放棄了那些通過交互產生的動態數據,意義就大幅下降了。
遺憾的是,Chrome API 里并沒有獲得網絡數據的接口。即使是 HTML,我們也是通過??元素的 outerHTML 勉強得到的。
不過對于 AJAX 這類請求,我們有很簡單的解決方案:鉤子程序。
我們向頁面中里注入一段腳本,覆蓋原生 XMLHttpRequest 的方法,即可監控動態的數據了:
#!js
var _xhr_open = XMLHttpRequest.prototype.open;
function handlerXhrReady() {
check(this.responseText, 'XHR: ' + this._url);
}
XMLHttpRequest.prototype.open = function(method, url) {
if (!this._url) {
this._url = url;
this.addEventListener('readystatechange', handlerXhrReady);
}
_xhr_open.apply(this, arguments);
};
當頁面使用 AJAX 加載數據時,我們即可監控其中的數據了。
類似的,我們還可以跟蹤 URL 的變化,監控 HttpOnly 的數據有沒有輸出到地址欄里:
#!js
var address = location.href;
window.addEventListener('hashchange', function(e) {
check(location.hash, 'location.hash');
});
var _pushState = History.prototype.pushState,
_replaceState = History.prototype.replaceState;
History.prototype.pushState = function(data, title, url) {
check(url, 'pushState');
_pushState.apply(this, arguments);
};
History.prototype.replaceState = function(data, title, url) {
check(url, 'replaceState');
_replaceState.apply(this, arguments);
};
function scanAddress() {
check(location.href, 'location.href');
}
對于這些調用 DOM API 的接口,我們都可以通過事件監聽,或者鉤子程序進行捕捉。
我們再來測試下,很快就發現剛才遺漏的一例:
對于其他網站,同樣也存在這類問題:
多的不計其數。。。
由于 Chrome 插件以及 DOM 規范的限制,另一種常見的內容仍無法輕易獲取,那就是 JSONP 腳本。外鏈腳本的 text 內容是無法獲得的,而且使用 JSONP 大多是為了跨域,因此通過 xhr 去代理自然不可行。如果使用純前端解決這個問題的話,可以嘗試分析 JSONP 的回調接口,并勾住它。
到目前為止,我們只是用最簡單的字符串匹配,來檢驗是否泄露。現實中,泄露點的字符稍有轉義,我們就無法識別了。因此,我們需要更智能的匹配算法。
例如某個 HttpOnly 的值為?hello|1234567,現實中很有可能是以?hello%7C1234567?的形式存在的。腳本轉義、URL 轉義、HTML 實體轉義...所以無論在哪,符號往往是不靠譜的,但字母和數字則相對穩定的多。
因此,我們可以提取數據中的字符數字(\w) 部分,作為掃描項。隱私數據雖然很長,但大多都是冗余的,因此提取其中部分作為特征,仍然不會有錯失。
即使有少量誤報,也沒什么事。寧可誤報,也不漏報。
除了轉義,還有可能將隱私數據經過簡單的編碼之后輸出了,例如 Base64。這些弱算法可以輕易還原出原始數據,但通常很難發現。
因此,我們可以將所有的數據,事先做一些簡單的算法,同時將它們也作為檢測目標,這樣就能更深層次的挖掘泄漏點了。
事實上,除了 HttpOnly 的 cookie,我們還可以將其他數據也作為偵測目標。
我們可以創建一個測試賬號,將其手機號,密碼,身份證等各種私密信息都作為待檢測的隱私數據,這樣就能全方位的分析出,哪些信息遭到了泄露。
例如,曾經有網站為了偷懶,將找回密碼的答案直接輸出到了頁面腳本變量里,而不是在后端判斷。這時,我們就能很快追蹤到泄漏點。