作者:
EtherDream
·
2014/11/11 10:15
0x00 前言
之前介紹了 HTTPS 前端劫持 的方案,雖然很有趣,然而現實卻並不理想。其唯一、也是最大的缺陷,就是無法阻止指令碼跳轉。若是沒有這個缺陷,那就非常完美了 —— 當然也就沒有必要寫這篇文章了。
說到底,還是因為無法重寫 location
這個物件 —— 它是指令碼跳轉的唯一渠道。儘管也流傳一些 Hack 能勉強實現,但終究是不靠譜的。
事實上,在最近封稿的 HTML5 標準裡,已非常明確了 location 的地位 —— Unforgeable。
這是個不幸的訊息。不過也是件好事,讓我們徹底打消各種偏門邪道的念頭,尋求一條全新的出路。
0x01 替換明文 URL
上回也提到,可以參考 SSLStrip 那樣,把指令碼里的 HTTPS URL 全都替換成 HTTP 版本,即可滿足部分場合。
當然,缺陷也是顯而易見的。只要 URL 不是以明文出現 —— 例如透過字串拼接而成,那就完全無法識別了,最終還是無法避免跳轉到 HTTPS 頁面上。
這種情況並不少見,所以我們需要更先進的解決方案。
0x02 替換 location
儘管我們無法重寫 location,但要山寨一個和 location 功能一樣的玩意,還是非常容易的。我們只需定義幾個 getter 和 setter,即可模擬出一個功能完全相同的 location2
。但如何將原先的 location 對映過來呢?
這時,後端的作用就發揮出來了。類似替換 HTTPS URL,這次我們只關注指令碼里的 location 字元,把它們都改成 location2 —— 於是所有和位址列相關的讀寫,都將落到我們的代理上面。之後能做什麼,不用說大家也都明白吧。
相比之前的 URL 替換,這個方案完美太多 —— URL 是動態建立的非常普遍,但 location 不是明文出現的,及其罕見。
除非指令碼是加密過的,否則即使用 Uglify 那樣的壓縮工具,也不會把全域性變數給混淆。至於人為刻意去轉義它,更是無稽之談了。
#!js
if (window['loc\ation'].protocol != 'https:') {
// ...
}
到此,我們的目標已經明確了:
0x03 處理外鏈指令碼
雖然替換頁面指令碼的內容並不困難,但對於外鏈指令碼,那就不容樂觀了。
現實中,不少頁面外鏈了 HTTPS 絕對路徑 的指令碼。這時,我們的中間人就無能為力了。為了避免這種情況,我們仍需替換頁面裡的 HTTPS URL,讓中間人能掌控更多的資源。
要替換 URL 倒也不難,一個簡單的正則就能實現 —— 但既然使用正則,我們面對的只能是字串了。
然而事實上,收到的都是最原始的二進位制資料,甚至未必都是 UTF-8 的。在上一篇文章裡,我們為了簡單,直接使用二進位制的方式注入。但在如今,這個方法顯然不可行了。
使用二進位制,不僅難以控制,而且很不嚴謹。我們很難得知匹配到的是獨立的字元,還是一個寬字元的部分位元組。因此,我們還是得用傳統可靠的方式來處理字串。
0x04 處理字集編碼
我們得藉助字集轉換庫,例如大名鼎鼎的 iconv,來協助完成這件事:
首先將二進位制資料轉換成 UTF-8 字串
有了標準的字串,我們的正則即可順利執行了
將處理完的字串,重新換回先前的編碼
儘管這一來一回得折騰兩次,效能又得耗費不少,但這仍是必須的。
事實上,這個過程也不是想象的那麼順利。有相當多的伺服器,並沒有在返回的 Content-Type
裡指定編碼字集,於是我們只能嘗試從頁面的 <meta>
中獲取。
但這個標籤相容諸多規範,例如過去的:
#!html
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=GBK">
以及如今流行的:
#!html
<meta charset="GBK" />
儘管透過正則很容易獲取,但用正則的前提還是得先有字串,於是我們陷入了僵局。
不過好在標籤、屬性、字集名,基本都是純 ASCII 字元,所以可先將二進位制轉成預設的 UTF-8 字串,從中取出字集資訊,然後再進行轉碼。
0x05 處理資料分塊
得益於豐富的第三方擴充套件,上述問題都不難解決。
然而,之前提到過『前端劫持』的一個巨大優勢 —— 無需處理所有資料,只需在第一個 chunk 裡注入程式碼即可。但現在,這項優勢面臨著嚴峻的考驗。
我們要替換頁面裡的 HTTPS 資源、location 變數等等,它們會出現在頁面的各個位置。如果我們對每個 chunk 進行單獨過濾、轉發,這樣會有問題嗎?
現實中,未必都是這樣理想的 —— 總會有那麼一定的機率,替換的關鍵字正好跨越兩個 chunk:
這時候,殘缺的首尾都無法匹配到,於是就會出現遺漏。關鍵字越長,出現的機率也就越大。對於 URL 這樣長的字串來說,這是一個潛在的隱患。
要完美解決這個問題,是比較麻煩的。不過有個簡單的辦法:我們可以扣留下 chunk 末尾部分字元,拼接到下個 chunk 的之前,從而降低遺漏的可能。
當然,如果不考慮使用者體驗的話,還是收集完所有資料,最後一次性處理,最省事了。
事實上還有更好的方案:中間人開啟一個緩衝區,將收到資料暫時快取其中。當資料積累到一定量、或者超過多久沒有資料時,才開始批次處理快取佇列。
這樣就可以避免 頻繁的 chunk 上下文處理,同時也 不會長時間阻塞使用者的響應時間,自然是兩全其美的。
這是不是有點類似 TCP nagle 的味道呢。
0x06 前端 location 代理
講完了後端的相關細節,我們繼續回到前端的話題上。
實現一個 location 的代理很簡單,不過值得留意的細節倒是不少:
location 不僅存在於 window
,其實 document
裡也有個相同的。
location 物件本身也是可以被賦值的,效果等同於 location.href。([PutForwards=href, ...]
已經很好的解釋了)
同理,location 的 toString
返回的也是 href
屬性。
如果帶有 location2 的指令碼被快取住了,那麼使用者在沒有劫持的頁面裡,也許就會報錯。所以還得留一條相容的後路。
......
只要考慮充分,實現一個 location 的切面還算是比較容易的。
0x07 動態指令碼劫持
前面談到替換頁面的 HTTPS URL,以確保外鏈指令碼明文傳輸。
然而現實中,並非所有指令碼都是靜態的。如今這個指令碼氾濫的時代,動態載入模組是很常見的事。如果引入的是一個 HTTPS 的指令碼,那麼我們的中間人又無從下手了。
不過值得慶幸的是,模組攔截不像 location 那樣無法實現。現實中,有非常多的方法可以攔截動態模組。在之前寫的《XSS 前端防火牆 —— 可疑模組攔截》 一文裡,已經詳細討論過各種方法和細節,這裡正好派上用場。
事實上,除了指令碼外,框架頁同樣也存在這個問題。上一篇文章裡,我們採用 CSP 來阻擋 HTTPS 的框架頁。但那僅僅是遮蔽,並不是真正意義的攔截。只有加上如今這套鉤子系統,才算一個完善的攔截系統。
0x08 演示
說了那麼多,真正的核心無非就是改變指令碼里的 location 變數而已,其他的一切都只是為了輔助它。
下面我們找幾個之前無法成功的網站,試驗下這個加強版的劫持工具。
上一篇文章裡提到京東登入,就是透過指令碼跳轉的。我們首先就拿它測試:
當流量經過中間人代理,頁面和指令碼里的 location 都變成了我們的變數名。於是之後和位址列相關的一切,盡在我們的掌控之中了:
注意位址列裡有一個 zh_cn
的標記,那正是 URL 向下轉型後的識別暗號。
透過 location2
獲取到的一切屬性,看起來就像在 HTTPS 頁面上一模一樣。即使指令碼里有自檢功能,也會被我們的虛擬環境所欺騙。
點選登入,自然是成功的。
畢竟,HTTPS 和 HTTP 只是傳輸上的差異。在應用層上,頁面是無法知曉的 —— 除了詢問指令碼的 location,但它已被我們劫持了。
除了京東的指令碼跳轉,財付通網站則是透過非主流的 <meta http-equiv="refresh">
進行的。
好在我們對頁面裡的 HTTPS URL 都替換了,所以仍然能夠跳轉到降級後的頁面:
值得注意的是,如果是從 QQ 圖示裡點進來的,那麼頁面就直接進入 HTTPS 版本,就不會被劫持了。但從第三方過來那就聽天由命了。
由於一般開發人員的思維,是不可能轉義 location 這個變數的。因此這套方案几乎可以通殺所有的安全站點。
當然,外國的網站也是一樣的。只要之前沒有被 HSTS
所快取,劫持依舊輕鬆自如。
......
所以,只要發揮無盡的想象,實現一個工程化的通用劫持方案,依然是可行的。
0x09 防範措施
如果你是仔細看完本文的話,應該早就想到如何應對了。
事實上,由於 JS 具有超強的靈活性,幾乎無法從靜態原始碼推測執行時的行為。
因此,只要將涉及 location
相關操作,進行簡單的轉義混淆,就能躲過中間人的劫持了。畢竟,要在劫持流量的同時,還要對指令碼進行語法分析,這個代價不免有點大了。
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!