看我如何分析並滲透WebSocket和Socket.io
Websocket簡介
Socket/">WebSocket是一種允許瀏覽器和伺服器建立單個TCP連線然後進行全雙工非同步通訊的技術。由於它允許實時更新,而瀏覽器也無需向後臺傳送數百個新的HTTP polling請求,所以對於web程式來說,WebSocket非常流行。這對於測試者來說是不好的,因為對WebSocket工具的支援不像HTTP那樣普遍,有時候會更加複雜。
除了BurpSuite之外,還有一些其他工具可用於處理WebSocket。不過經過測試,它們都不怎麼理想。
Zed Attack Proxy (ZAP) Pappy Proxy Man-in-the-Middle Proxy (mitmproxy) WebSocket/Socket.io (WSSiP)
如果你對使用Websocket進行滲透測試感興趣,那麼可以檢視這篇文章:
ofollow,noindex" target="_blank">https://www.blackhillsinfosec.com/command-and-control-with-websockets-wsc2/
而在這篇文章中主要會講socket.io,它是一個很流行的JavaScript WebSockets庫。在GitHub上它有多流行呢?—已經有超過41.4的star了。
在NPM上,它在WebSocket中排行第二和第三。
另外,OWASP Juice-Shop 這樣非常棒的專案也使用了socket.io庫,所以本篇文章中將使用websocket.io進行演示。
https://github.com/bkimminich/juice-shop/search?utf8=%E2%9C%93&q=socket.io&type=
在本文中,我們假設你已經熟悉使用BurpSuite測試Web應用程式,所涵蓋的所有內容都可以在其社群版本中完成。不用多說,現在開始吧。
如果我們在瀏覽器中訪問Juice-Shop,則可以在後臺快速檢視WebSocket流量。你也可以在BurpSuite中通過Proxy-> WebSockets歷史記錄找到。
由於協議的無狀態特性,HTTP需要始終傳送請求/響應對,而WebSocket是一種有狀態協議。這意味著你可以從伺服器獲得任意數量的傳出“請求”和任意數量的傳入“響應”。由於底層連線是保持開啟的TCP,因此客戶端和伺服器可以隨時傳送訊息而無需等待對方。這就是為什麼WebSocket歷史記錄與你習慣檢視的HTTP歷史記錄存在差異。
在此介面中,你可以看到傳送和接收的單位元組訊息。但是,當應用程式執行一些有趣的操作時,你就可以看到具有更大負載的訊息。
BurpSuite具有測試WebSockets的能力,你可以實時進行攔截和修改,但WebSocket沒有Repeater,Scanner或Intruder功能。預設情況下,如果要在BurpSuite中啟用WebSocket攔截,你只需要開啟主攔截就好了。
這樣一來,你就可以通過與HTTP相同的方式獲取所截獲的WebSocket訊息。同時也可以在攔截視窗中編輯它們。
在WebSockets歷史記錄選項卡中可以檢視已編輯的訊息。
將WebSocket降級為HTTP
方法一:使用Socket.io的HTTP回退機制
一個非常奇怪的點是,有時在HTTP歷史記錄中也能看到類似Websocket歷史記錄中的訊息,回想一下,這些比較有趣的WebSocket訊息需要解決記分板相關問題,下圖顯示了來自伺服器的相同響應,但這次是在HTTP歷史記錄中。由此可以看出socket.io能夠通過WebSocket或HTTP傳送訊息。
在所觀察的請求中,傳遞的引數值有些為“websockets”,而有些則是“polling”。那麼據推測,可能為了防止WebSockets在應用程式中不受支援或被阻止,才允許使用HTTP。
socket.io文件中解釋了“polling”和“websockets”如何作為兩個預設傳輸選項。它還介紹瞭如何通過將WebSockets指定為唯一傳輸方式來禁用polling。我認為反過來也是如此,我可以指定polling作為唯一的傳輸機制。
https://socket.io/docs/client-api/#with-WebSocket-transport-only
通過搜尋socket.io.js原始碼,我找到了以下內容:
this.transports=n.transports||["polling","WebSocket"]
這行程式碼會將一個名為transports的內部變數設定為傳入的值,如果傳入的值為false/empty,則為預設的[“polling”,“websocket”]。這很符合我們對polling和WebSocket的預設傳輸的推測。現在通過Burp中的Proxy->Options下設定匹配並替換規則來更改這些預設值,看看會發生什麼。
成功了!新增規則後,重新整理頁面(需要啟用Burp的內建規則“Require non-cached response”或執行強制重新整理),資料不再通過WebSockets進行通訊。進展不小,但是如果使用的應用程式已經提供了優先於我們的新預設值的傳輸選項呢?在這種情況下,我們可以修改匹配和替換規則。以下規則應適用於socket.io庫的不同版本,並忽略應用程式開發人員所指定的任何傳輸方式。
以下是要使用的字串,務必將其設定為正則表示式匹配:
this\.transports=.*?\.transports\|\|\["polling","websocket"] this.transports=["polling"]
方法二:中止Websocket升級
方法一隻能用於於socket.io,可能會擴充套件到其他客戶端庫。但是,以下方法應該更加通用,因為它以WebSockets協議本身為目標。
經過分析,我發現WebSockets首先通過HTTP進行通訊,以便與伺服器協商並“升級”為WebSocket。其中重要的部分是:
1)客戶端通過一些WebSocket特定header傳送升級請求。
2)伺服器響應狀態碼為101 Switching Protocols,以及WebSocket header。
3)通訊轉換到WebSocket,此特定會話不再使用HTTP。
WebSockets RFC文件第4.1節提供了有關如何中斷此工作流的各種資訊,以下是 https://tools.ietf.org/html/rfc6455#section-4.1 的摘錄,並附加了觀點。
1.如果從伺服器收到的狀態碼不是101,則客戶端響應HTTP [RFC2616] 。特別情況下,收到401狀態碼時,客戶端可能會執行身份驗證;伺服器也可能會通過3xx狀態碼重定向客戶端(但客戶不需要遵循)等。否則按以下步驟進行。
2.如果響應缺少Upgrade header,或Upgrade header包含的值與“WebSocket”的ASCII不匹配,則客戶端必須關閉WebSocket連線。
3.如果響應缺少Connection header,或Connection header包含的值與“WebSocket”的ASCII不匹配,則客戶端必須關閉WebSocket連線。
4.如果響應缺少Sec-WebSocket-Accept header,或Sec-WebSocket-Accept header的值並非是由Sec-WebSocket-Key(作為字串,未經base64解碼)與字串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″串聯起來的字串(忽略任何前導和尾隨空格)的base64編碼後的SHA-1值的話,則客戶端必須關閉WebSocket連線。
5.如果響應中包括Sec-WebSocket-Extensions header,並且header要求使用的擴充套件並沒有出現在客戶端的握手訊息中(伺服器指示的擴充套件並非是客戶端所請求的),則客戶端必須關閉WebSocket連線。(解析header以確定請求哪些擴充套件的問題,將在 第9.1節 中討論)
考慮到這些“連線必定被關閉”的條件,我想出了以下一套替換規則,這些規則應該包含了所有五個的失敗條件。
一旦使用這些規則,所有WebSocket升級請求都會失敗。由於socket.io預設情況下無法使用HTTP,因此已經達到所需的效果。其他庫的表現可能不同,並導致你正在測試的應用程式出錯。但我們的工作就是讓軟體做一些不應該做的事情!
原始響應看起來像這樣,並且會使客戶端和伺服器轉換到WebSocket進行通訊。
相反,客戶端從伺服器收到此修改後的響應,會關閉WebSocket連線。
我在測試中遇到的一件事是,在將這些匹配和替換規則加入後,客戶端在重試WebSocket連線時非常持久,並在我的HTTP歷史記錄中引起了大量不必要的流量。如果你正在處理socket.io庫,則最簡單的方法是使用上面的方法1。如果你有不同的庫或其他情況,則可能需要新增更多規則來使客戶端伺服器不支援WebSocket。
將Burp Repeater作為Socket.io客戶端
由於我們強制通過HTTP而非WebSockets進行通訊,所以現在可以新增自定義匹配並替換將應用於已經通過WebSockets流量的規則!接下來,可以使用Repeater,Intruder和Scanner等工具,這些更改將特定於socket.io庫。不過現在還有兩個問題:
1.每個請求都有一個會話號,任何無效請求都將導致伺服器終止該會話 2.每個請求的主體都有一個計算欄位,表示訊息的長度。如果這不正確,伺服器會將其視為無效請求並終止會話。
以下是應用程式中使用的幾個示例URL。
/socket.io/?EIO=3&transport=polling&t=MJJR2dr /socket.io/?EIO=3&transport=polling&t=MJJZbUa&sid=iUTykeQQumxFJgEJAABL
URL中的“sid”引數表示到伺服器的單個連線流。如果傳送了無效訊息(在嘗試破解時很常見),那麼伺服器將關閉整個會話,之後必須重新開始新會話。
給定請求的主體中含有一個欄位,其中存放有效載荷的位元組數。這類似於“Content-Length”HTTP header,只不過該欄位的值近針對socket.io。例如,如果你要傳送的有效載荷是“hello”,那麼,相應的主體將是“5:hello”,Content-Length頭部的值是7。其中,5表示字串“hello”中的字母數量,而7則表示字串“hello”中的字母數量以及socket.io新增到主體內的字串“5:”中的字母數量之和。與往常一樣,Burp將替我們更新Content-Length頭部,因此,這件事情我們無需擔心。但是,我還沒有找到能夠自動計算和包含有效載荷長度的好方法。更讓人頭疼的是,我發現socket.io竟然會在同一個HTTP請求中傳送多條訊息。由於每個訊息都是一個封裝後的WebSocket有效載荷,並且每個訊息都有自己的長度,因此,最終看起來就像這樣:“5:hello,4:john,3:doe”(實際的語法可能有所不同,這裡只是便於演示)。計算長度時一旦出錯,伺服器就會將其作為無效訊息拒絕,這樣,我們就要重新開始了。
這是body的示例。這是Juice-Shop應用程式中的響應,請求的格式相同。注意,這裡的“215”表示“:”之後的有效載荷的長度。
215:42[“challenge solved”,{“key”:”zeroStarsChallenge”,”name”:”Zero Stars”,”challenge”:”Zero Stars (Give a devastating zero-star feedback to the store.)”,”flag”:”e958569c4a12e3b97f38bd05cac3f0e5a1b17142″,”hidden”:false}]
巨集
使用Burp巨集能解決第一個問題。基本上,每次Burp在伺服器拒絕訊息時匹配,巨集將自動建立新會話並用有效的“sid”更新原始請求。通過轉到options->Sessions->Macros->Add來建立新巨集。
建立新會話的URL只需省略“sid”引數。例如:
/socket.io/?EIO=3&transport=polling&t=MJJJ4Ku
伺服器響應包含一個全新的“sid”值以供使用。
接下來,單擊“Configure item”按鈕,並將引數名稱命名為“sid”。然後,選擇“Extract from regex group”選項,並使用如下所示的正則表示式。
"sid"\:"(.*?)"
這時,配置視窗應如下所示:
會話處理規則
現在有了一個巨集,我們需要一種方法來觸發它。這就是Burp會話處理規則的用武之地。通過
Project options->Sessions->Session Handling Rules->Add
為“Check session is valid”建立新的規則動作:
配置新規則操作如下:
按如下方式配blackhillsinfosec置新規則操作:最後,在完成新規則操作後,還需修改規則的範圍。你可以在此處決定要應用此規則的位置。建議至少將它用於Repeater,這樣就可以手動重複請求。
以下是我配置範圍規則的方法。你可以更加具體地瞭解自己所需範圍,但下面的選項應該適用於大多數情況。
這是在沒有會話處理規則的情況下發出的請求:
這裡是在會話處理規則生效後發出的相同請求:
*參考來源: blackhillsinfosec ,FB小編Covfefe編譯,轉載請註明來自FreeBuf.COM