XSS 的本質仍是一段腳本。和其他文檔元素一樣,頁面關了一切都銷毀。除非能將腳本蔓延到頁面以外的地方,那樣才能獲得更長的生命力。
慶幸的是,從 DOM 誕生的那一天起,就已為我們準備了這個特殊的功能,讓腳本擁有突破當前頁面的能力。
下面開始我們的續命黑魔法。
一個不合理的標準,往往會埋下各種隱患。
按理來說,只有通過腳本彈出的頁面,才能擁有 opener 屬性。然而事實上,通過超鏈接點開的頁面居然也有。這為 XSS 打開了一扇大門 —— XSS 不僅可以操控當前頁面,甚至還能傳染給同源的父頁面。
XSS 一旦感染到父頁面里,戰斗力就大幅提升了。
可以想象,只要看了一個帶有 XSS 的帖子,即使立即關了,那么帖子列表頁也會遭到感染。
更有趣的是,opener 這個屬性不受同源策略限制。即使父頁面不同源,但父頁面的 opener 仍然可以訪問。
我們可以順著 opener.opener.opener... 一直往上試探,只要是和當前頁面同源的,仍然能夠進行操控 —— 盡管中間隔著其他不同源的頁面。
網站的主頁面顯然比詳細頁更受用戶的信任,停留的時間也會更長,因此攻擊力可成倍的增加。
如果說反向注入是茍且偷生的話,那么正向注入就是當家做主翻身的機會了。
盡管我們能夠控制父頁面,但從父頁面點開的網頁仍然不受操控。如果具有控制子頁面的能力,那就更完美了。
不幸的是,我們無法控制超鏈接打開的新頁面。唯一能夠操控的新頁面,那就是 window.open 的彈框頁。幸運的是,在絕大多數瀏覽器上,它們看起來的效果是一樣的。
因此,我們可以在用戶的點擊瞬間,屏蔽掉默認的超鏈接行為,用彈框頁取而代之,即可把 XSS 注入到 window.open 返回的新頁面里了。
類似的,通過子頁面遞歸打開的新頁面,同樣也無法逃脫。于是子子孫孫盡在我們的掌控之中。
反向注入,讓我們占據已有的地盤;正向注入,把我們的勢力擴大蔓延出去。兩者結合,即可占據半壁江山了。
值得注意的是,正向注入中有個細節問題。并非所有的超鏈接都是彈出型的(_blank),也有不少是在當前頁面跳轉的。若是想劫持的狠點,可以忽略這個問題;如果不想被細心的用戶發現,那么可以判斷下當前超鏈接以及
<base>
的 target 屬性,決定是否劫持。
上面提到,如果是在當前頁面里跳轉,那么還能繼續感染嗎?或者說,某個頁面刷新之后,是否就丟失了?
答案是肯定的。如果我們不采取一些措施,任憑占據的地盤不斷丟失,那么我們的勢力范圍就會越來越小,直到消亡。
相比進攻,防守則更為困難。我們不知何時會失去,因此必須定時去檢查。
一旦發現對方已擺脫我們的控制,那么必須立即重新注入,以恢復我們的勢力。
對于新頁面的 XSS 來說,當然是注入的越早越好。越前面擁有越高的優先級,甚至可以攔截頁面的正常業務功能。
為了能盡快獲知頁面刷新、跳轉等行為,我們還可跟蹤 unload
事件,在頁面即將丟失的瞬間,將消息通知出去,讓對方盡快來拯救自己。
這樣,就不必等待定時器了,可以最快的速度恢復。甚至能趕在頁面的第一個腳本之前,運行我們的 XSS。
當然,并非任何情況都能收回的。如果跳轉到了不同源的頁面,那顯然是無能為力了 —— 不過,就此而放棄它嗎?回答是:決不妥協!
盡管頁面已經和我們分道揚鑣了,但所在的窗體仍然被我們掌控。我們可以跳轉、關閉它,甚至還有可能出現奇跡:只要頁面跳轉回我們的站點,又可被我們所收復!
不難發現,只要還有一個頁面存在,就有可能收回曾經被占領的地盤。因此,我們要將可控的頁面都聯結起來,讓每個頁面都知曉并監督所有成員。
當有新成員加入時,通知給大家,記錄在各自的頁面里。
這樣即使其中一個頁面意外關閉了,也不會丟失重要的信息 —— 信息已被分布儲存在各個頁面里了。
因此,頁面開的越多,相互聯結就越牢固。
如果只剩最后一個頁面,那么一旦刷新之后就沒人來拯救了,于是就會消亡。
所以,把超鏈接都變成新頁面中打開,還是有很大的優勢的。
一些網站為了方便通信,將 document.domain 降到根域。例如支付寶網站的絕大部分頁面,都是 alipay.com。這樣原本不同源的子站,這時也能夠相互操控了。
因此,遇到不同源的頁面,可以嘗試降低自身的域,再次發起操作,或許就能成功注入了。
之前說到正向注入,是通過劫持超鏈接點擊實現的。事實上,除了超鏈接外,還有個進入新頁面的方式,那就是表單提交。
相比超鏈接,表單顯得棘手一些。我們不僅得打開一個新頁面,還要把表單里的數據也提交上去。如果把整個表單的元素克隆到新頁面提交,一些數據又會丟失。
不過,仔細研究一下表單元素,會發現有一個非常簡單的方法:原來 window.open 第二個參數可以賦予新窗口一個 name,然后將 name 賦予表單的 target 屬性,即可在我們創建的新窗口里提交。這樣就可以把 XSS 注入進去了。
不同頁面之間可以正反注入。同個頁面中,可能存在多個框架頁,因此還可以嘗試框架頁之間的上下注入。
也許,XSS 位于頁面中某個小框架。如果只局限于自身頁面,那么是毫無發展空間的。因此得跳出圈子,向更廣闊的 parent 頁面注入。
類似的,如果主頁面僅僅是個外殼,實際內容運行在某個框架里,那么得注入到子框架中,才能獲取更有意義的信息。
同樣,我們還可以將上下注入結合,即可讓 XSS 從某個框架頁里破殼而出,感染到所有的框架頁里。
盡管這些特征從 DOM 誕生起就已存在了,不過要寫出一個完善的腳本并不容易。直到如今的 IE 11,不同窗體間的操作,仍有各種奇怪問題,更不用說那些非主流 IE 了。
不過好在除 IE 外的其他主流瀏覽器,都能很好的運行。下面分享一個 Demo,其中實現了上述部分功能:
http://www.etherdream.com/FunnyScript/XSSGhost/
當我們順著超鏈接往前點,一旦進入有 XSS 的頁面,先前的父頁面都遭到感染。更嚴重的是,被感染的頁面打開的子頁面,也都無一幸免。即使刷新,也會被其他頁面監控到,從而立即恢復。
正如幽靈鬼魂一般揮之不去。