1. 程式人生 > >深入理解跨站點 WebSocket 劫持漏洞的原理及防範

深入理解跨站點 WebSocket 劫持漏洞的原理及防範

序言

WebSocket 作為 HTML5 的新特性之一格外吸引著開發人員的注意,因為它的出現使得客戶端(主要指瀏覽器)提供對 Socket 的支援成為可能,從而在客戶端和伺服器之間提供了一個基於單 TCP 連線的雙向通道。對於實時性要求比較高的應用而言,譬如線上證券、線上遊戲,以及不同裝置之間資訊同步。資訊實時同步一直是技術難題,在 WebSocket 出現之前,常見解決方案一般就是輪詢(Polling)和 Comet 技術,但這些技術增加了設計複雜度,也造成了網路和伺服器的額外負擔,在負載較大的情況下效率相對低下,導致應用的可伸縮行收到制約。對於此類應用的開發者來說,WebSocket 技術簡直就是神兵利器,讀者可以登陸 websocket.org 網站觀看特色案例,以及它提供的 WebSocket 和 Comet 的效能對比分析報告。最近幾年內 WebSocket 技術被開發人員廣泛應用到各類實際應用中。不幸的是,WebSocket 相關的安全漏洞也逐步被披露出來,其中最容易發生的就是跨站點 WebSocket 劫持漏洞。本文將深入淺出為讀者介紹跨站點 WebSocket 漏洞的原理、檢測方法和修復方法,希望能幫助廣大讀者在實際工作中避免這個已知安全漏洞。

WebSocket 協議握手和安全保障

為了便於闡述跨站點 WebSocket 劫持漏洞原理,本文將簡單描述 WebSocket 協議的握手和切換過程。建議有興趣的讀者閱讀參考文獻中提供的 RRFC 6455 規範,深入學習 WebSocket 協議。

瞭解過 WebSocket 技術的讀者都知道 ws://和 http://,那麼 WebSocket 和 HTTP 是什麼關係呢。筆者對這個問題的理解是,WebSocket 是 HTML5 推出的新協議,跟 HTTP 協議內容本身沒有關係。WebSocket 是持久化的協議,而 HTTP 是非持久連線。正如前文所述,WebSocket 提供了全雙工溝通,俗稱 Web 的 TCP 連線,但 TCP 通常處理位元組流(跟訊息無關),而 WebSocket 基於 TCP 實現了訊息流。WebSocket 也類似於 TCP 一樣進行握手連線,跟 TCP 不同的是,WebSocket 是基於 HTTP 協議進行的握手。筆者利用 Chrome 開發者工具,收集了 websocket.org 網站的 Echo 測試服務的協議握手請求和響應,如清單 1 和 2 所示。

清單 1. WebSocket 協議升級請求
GET ws://echo.websocket.org/?encoding=text HTTP/1.1
Host: echo.websocket.org
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://www.websocket.org
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) Chrome/49.0.2623.110
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6
Cookie: _gat=1; _ga=GA1.2.2904372.1459647651; JSESSIONID=1A9431CF043F851E0356F5837845B2EC
Sec-WebSocket-Key: 7ARps0AjsHN8bx5dCI1KKQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

熟悉 HTTP 的朋友可以發現 WebSocket 的核心了,對的,這就是 Connection:Upgrade 和 Upgrade:websocket 兩行。這兩行相當於告訴伺服器端:我要申請切換到 WebSocket 協議。

清單 2. WebSocket 協議升級響應
HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://www.websocket.org
Connection: Upgrade
Date: Sun, 03 Apr 2016 03:09:21 GMT
Sec-WebSocket-Accept: wW9Bl95VtfJDbpHdfivy7csOaDo=
Server: Kaazing Gateway
Upgrade: websocket

一旦伺服器端返回 101 響應,即可完成 WebSocket 協議切換。伺服器端即可以基於相同埠,將通訊協議從 http://或 https://切換到 ws://或 wss://。協議切換完成後,瀏覽器和伺服器端即可以使用 WebSocket API 互相傳送和收取文字和二進位制訊息。

這裡要解釋一些安全相關的重要頭部引數,Sec-WebSocket-Key 和 Sec-WebSocket-Accept。這涉及一個 WebSocket 安全特性,客戶端負責生成一個 Base64 編碼過的隨機數字作為 Sec-WebSocket-Key,伺服器則會將一個 GUID 和這個客戶端的隨機數一起生成一個雜湊 Key 作為 Sec-WebSocket-Accept 返回給客戶端。這個工作機制可以用來避免快取代理(caching proxy),也可以用來避免請求重播(request replay)。

細心的讀者可能也注意到很多其他“Sec-”開頭的 WebSocket 相關的 Header。這其實也是 WebSocket 設計者為了安全的特意設計,以“Sec-”開頭的 Header 可以避免被瀏覽器指令碼讀取到,這樣攻擊者就不能利用 XMLHttpRequest 偽造 WebSocket 請求來執行跨協議攻擊,因為 XMLHttpRequest 介面不允許設定 Sec-開頭的 Header。


跨站點 WebSocket 劫持漏洞原理

儘管 WebSocket 協議設計時充分考慮了安全保障機制,但隨著 WebSocket 技術推廣,安全工作者們慢慢還是發現了一些 WebSocket 相關的安全漏洞,譬如 Wireshark 的漏洞 CVE-2013-3562 (Wireshark 1.8.7 之前的 1.8.x 版本中的 Websocket 解析器中的 epan/dissectors/packet-websocket.c 中的‘tvb_unmasked’函式中存在多個整數符號錯誤,遠端攻擊者可通過惡意的資料包利用這些漏洞造成拒絕服務)。Asterisk WebSocket Server 的 DoS 漏洞 CVE-2014-9374(該 WebSocket Server 某模組中存在雙重釋放漏洞,遠端攻擊者可通過傳送零長度的幀利用該漏洞造成拒絕服務)。這兩個 DDoS 漏洞跟 WebSocket 協議本身以及 WebSocket 應用程式相關性不大。但 2015 年來自 Cisco 的 Brian Manifold 和 Nebula 的 Paul McMillan 報告了一個 OpenStack Nova console 的 WebSocket 漏洞(CVE-2015-0259),這個漏洞得到廣泛關注,並且被在很多 WebSocket 應用中發現。事實上,這種漏洞早在 2013 年就被一個德國的白帽黑客 Christian Schneider 發現並公開,Christian 將之命名為跨站點 WebSocket 劫持 Cross Site WebSocket Hijacking(CSWSH)。跨站點 WebSocket 劫持相對危害較大,也更容易被開發人員忽視。

什麼是跨站點 WebSocket 劫持漏洞呢,前文已經提及,為了建立全雙工通訊,客戶端需要基於 HTTP 進行握手切換到 WebSocket 協議,這個升級協議的過程正是潛在的阿喀琉斯之踵。大家仔細觀察上文的握手 Get 請求,可以看到 Cookie 頭部把域名下的 Cookie 都發送到伺服器端。如果有機會閱讀 WebSocket 協議(10.5 章客戶端身份認證)就發現,WebSocket 協議沒有規定伺服器在握手階段應該如何認證客戶端身份。伺服器可以採用任何 HTTP 伺服器的客戶端身份認證機制,譬如 cookie,HTTP 基礎認證,TLS 身份認證等。因此,對於絕大多數 Web 應用來說,客戶端身份認證應該都是 SessionID 等 Cookie 或者 HTTP Auth 頭部引數等。熟悉跨站點請求偽造攻擊 Cross Site Request Forgery(CSRF)的朋友到這裡應該就可以聯想到黑客可能偽造握手請求來繞過身份認證。

因為 WebSocket 的客戶端不僅僅侷限於瀏覽器,因此 WebSocket 規範沒有規範 Origin 必須相同(有興趣的讀者可以閱讀規範 10.2 章節瞭解對於 Origin 的規範)。所有的瀏覽器都會發送 Origin 請求頭,如果伺服器端沒有針對 Origin 頭部進行驗證可能會導致跨站點 WebSocket 劫持攻擊。譬如,某個使用者已經登入了應用程式,如果他被誘騙訪問某個社交網站的惡意網頁,惡意網頁在某元素中植入一個 WebSocket 握手請求申請跟目標應用建立 WebSocket 連線。一旦開啟該惡意網頁,則自動發起如下請求。請注意,Origin 和 Sec-WebSocket-Key 都是由瀏覽器自動生成,Cookie 等身份認證引數也都是由瀏覽器自動上傳到目標應用伺服器端。如果伺服器端疏於檢查 Origin,該請求則會成功握手切換到 WebSocket 協議,惡意網頁就可以成功繞過身份認證連線到 WebSocket 伺服器,進而竊取到伺服器端發來的資訊,抑或傳送偽造資訊到伺服器端篡改伺服器端資料。有興趣的讀者可以將這個漏洞跟 CSRF 進行對比,CSRF 主要是通過惡意網頁悄悄發起資料修改請求,不會導致資訊洩漏問題,而跨站點 WebSocket 偽造攻擊不僅可以修改伺服器資料,還可以控制整個讀取/修改雙向溝通通道。正是因為這個原因,Christian 將這個漏洞命名為劫持(Hijacking),而不是請求偽造(Request Forgery)。

清單 3. 篡改過的 WebSocket 協議升級請求
GET ws://echo.websocket.org/?encoding=text HTTP/1.1
Host: echo.websocket.org
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://www.malicious
website.com
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6
Cookie: _gat=1; _ga=GA1.2.290430972.14547651; JSESSIONID=1A9431CF043F851E0356F5837845B2EC
Sec-WebSocket-Key: 7ARps0AjsHN8bx5dCI1KKQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

讀到這裡,熟悉 JavaScript 跨域資源訪問的讀者可能會懷疑以上觀點。如果 HTTP Response 沒有指定“Access-Control-Allow-Origin”的話,瀏覽器端的指令碼是無法訪問跨域資源的啊,是的,這就是眾所周知的跨域資源共享 Cross-Origin Resource Sharing(CORS),這確實也是 HTML5 帶來的新特性之一。但是很不幸,跨域資源共享不適應於 WebSocket,WebSocket 沒有明確規定跨域處理的方法。


如何檢測跨站點 WebSocket 劫持漏洞

明白跨站點 WebSocket 劫持漏洞原理後,大家就很容易聯想到這個漏洞的檢測方法了,重點就在於重播 WebSocket 協議升級請求。簡單來說就是使用能攔截到 WebSocket 握手請求的工具,修改請求中的 Origin 頭資訊,然後重新發送這個請求,看看伺服器是否能夠成功返回 101 響應。如果連線失敗,那麼說明這個 WebSocket 是安全的,因為它可以正確拒絕來自不同源(Origin)的連線請求。如果連線成功,通常就已經證明伺服器端沒有執行源檢查,為了嚴謹起見,最好進一步測試是否可以傳送 WebSocket 訊息,如果這個 WebSocket 連線能夠傳送/接受訊息的話,則完全證明跨站點 WebSocket 劫持漏洞的存在。

為了便於演示如何測試及修復這個漏洞,筆者編寫了一個簡單的 WebSocket 應用,這個應用基於 JAAS 實現了 HTTP BASIC 身份認證,讀者可以將這個程式下載部署到 Tomcat 中進行測試。開啟客戶端網頁後首先進行登入,然後點選“連線”按鈕通過 JavaScript 建立 WebSocket 連線,然後點選“傳送”按鈕提交一個問題到伺服器端,伺服器端實時確認收到查詢請求,5 秒後再將結果推送給客戶端。

測試工具方面有很多選擇,由於許可證原因,筆者採用了開源的 OWASP ZAP v2.4.3。這裡要簡單說一下,測試過程主要基於測試工具的代理,攔截到 WebSocket 握手請求以及 WebSocket 訊息通訊,然後通過工具修改 Origin 後重發請求,如果連線成功後,重發 WebSocket 客戶端訊息。以上功能各個商業安全測試工具都可以做到。

1. 首先在 Firefox 中配置好 ZAP 的代理,然後探索整個 WebSocket 應用。下圖可以看到請求頭部有 HTTP Basic Authorization 資訊,表示已經登入成功。

圖 1. WebSocket 協議升級請求
圖 1. WebSocket 協議升級請求

2. 右鍵選擇重發 WebSocket 協議升級請求,將其中的 Origin 修改為任意其他網址後點擊發送。

圖 2. 篡改 WebSocket 協議升級請求
圖 2. 篡改 WebSocket 協議升級請求

3. 點選響應標籤,可以看到伺服器端返回了 101,即協議握手成功。

圖 3. WebSocket 協議握手成功
圖 3. WebSocket 協議握手成功

4. 進一步測試 WebSocket 訊息是否可以重發。如下圖所示,右鍵點選第一條客戶端發出的 WebSocket 訊息,選擇重發,輸入測試訊息”www”後點擊發送,可以看到 ZAP 陸續收到兩條伺服器返回的訊息。這充分證明被測試應用站點存在跨站點 WebSocket 劫持漏洞。

圖 4. 重發客戶端 WebSocket 訊息
圖 4. 重發客戶端 WebSocket 訊息

防範跨站點 WebSocket 劫持攻擊

前文介紹了跨站點 WebSocket 劫持漏洞原理和檢測,相信讀者已經明白它的危害,接下來我們談談如何防範這個漏洞。這個漏洞的原理聽起來略微複雜,但幸運的是測試起來相對比較簡單,那麼修復會不會也很簡單。很多讀者會想到,不就是在伺服器程式碼中檢查 Origin 引數嘛。是的,檢查 Origin 很有必要,但不充分。筆者推薦大家要在伺服器端的程式碼中增加 Origin 檢查,如果客戶端發來的 Origin 資訊來自不同域,建議伺服器端拒絕這個請求,發回 403 錯誤響應拒絕連線。

WebSocket 伺服器端 Origin 檢查

筆者採用了 Java EE 技術編寫的 WebSocket 測試應用,Java EE 的 WebSocket API 中提供了配置器允許開發人員重寫配置用來攔截檢查協議握手過程。筆者在文章附錄的原始碼中已經包含了這部分程式碼,下面簡單介紹一些核心類和配置。如果對 Java EE WebSocket API 不太熟悉的讀者,建議可以先查閱相關規範。

1. 首先編寫一個 WebSocket 伺服器終端的配置器,如清單 4 所示繼承並重寫 checkOrigin 方法。注意,筆者忽略了沒有 Origin 的場景,這一點要視各個應用的實際情況而定,如果有非瀏覽器客戶端的話,則需要加上這一個檢查。同時建議非瀏覽器客戶端參見下文的令牌機制。

清單 4. WebSocket 源檢查配置器
public class CustomConfigurator extends ServerEndpointConfig.Configurator {

 private static final String ORIGIN = "http://jeremy.laptop:8080";

 @Override
 public boolean checkOrigin(String originHeaderValue) {
 if(originHeaderValue==null || originHeaderValue.trim().length()==0)
 return true;
 return ORIGIN.equals(originHeaderValue);
 }
}

2. 然後將該配置器關聯到 WebSocket 伺服器程式碼中。

清單 5. 配置 WebSocket 源檢查
@ServerEndpoint(value = "/query", configurator = CustomConfigurator.class)
public class WebSocketTestServer {
 @OnMessage
 public void onMessage(String message, Session session) 
 throws IOException, InterruptedException {
 session.getBasicRemote().sendText("We got your query: " + message 
 + "\nPlease wait for a while, we will response to you later.");
 Thread.sleep(5000);
 session.getBasicRemote().sendText("Sorry, we did not find the answer.");
 }
}

3. 重新打包釋出 WebSocket 應用程式。

有興趣的讀者可以自己嘗試,如果補上以上程式碼後,重播篡改的 WebSocket 握手協議請求會收到 403 錯誤。

WebSocket 令牌機制

以上看起來很美好,但是僅僅檢查 Origin 遠遠不夠,別忘記了,如果 WebSocket 的客戶端不是瀏覽器,非瀏覽器的客戶端發來的請求根本就沒有 Origin。除此之外,我們要記得,惡意網頁是可以偽造 Origin 頭資訊的。更徹底的解決方案還是要借鑑 CSRF 的解決方案-令牌機制。

鑑於篇幅原因,筆者就不詳細貼出整個設計和程式碼,建議讀者參照以下概要設計提高 WebSocket 應用的安全。

1. 伺服器端為每個 WebSocket 客戶端生成唯一的一次性 Token;

2. 客戶端將 Token 作為 WebSocket 連線 URL 的引數(譬如 ws://echo.websocket.org/?token=randomOneTimeToken),傳送到伺服器端進行 WebSocket 握手連線;

3. 伺服器端驗證 Token 是否正確,一旦正確則將這個 Token 標示為廢棄不再重用,同時確認 WebSocket 握手連線成功;如果 Token 驗證失敗或者身份認證失敗,則返回 403 錯誤。

這個方案裡的 Token 設計是關鍵,筆者推薦的方案是為登入使用者生成一個 Secure Random 儲存在 Session 中,然後利用對稱加密(譬如 AES GCM)加密這個 Secure Random 值作為令牌,將加密後的令牌傳送給客戶端用來進行連線。這樣每個 Session 有一個唯一的隨機數,每個隨機數可以通過對稱加密生成若干份一次性令牌。使用者即便通過不同終端通過 WebSocket 連線到伺服器,伺服器可以在保障令牌唯一且一次性使用的前提下,依然能將不同通道中的資訊關聯到同一使用者中。

可能存在另外一個設計思路,在 WebSocket 訊息中增加令牌和身份資訊,但筆者覺得這樣的設計有悖於 WebSocket 的設計思想,而且增加了不必要的網路負載。拋磚引玉,歡迎讀者提供更好的設計方案。


總結

本文筆者跟讀者分享了對 WebSocket 協議握手的理解,並在此基礎上闡述了跨站點 WebSocket 劫持漏洞的原理。正如文中所提,已知的各類 WebSocket 漏洞中,只有這個是廣泛存在於 Web 應用程式碼中的漏洞。筆者同時分享了檢測跨站點 WebSocket 劫持漏洞的方法,並且基於 Java EE 技術介紹了漏洞的修復辦法,以及更全面的基於令牌機制的安全解決方案。

下載


參考資料

學習


原作者: 何健, 軟體架構師, 甲骨文(中國)軟體系統有限公司  2016 年 5 月 10 日

原文連結: https://www.ibm.com/developerworks/cn/java/j-lo-websocket-cross-site/index.html