黑客是如何攻擊 WebSockets 和 Socket.io的


WebSockets概述


WebSockets是一種允許瀏覽器和服務器建立單個TCP連接,並進行雙向異步通信的技術。這種技術非常適合Web應用程序,采用該技術之后,瀏覽器無需在后台發送數百個新的HTTP輪詢請求,也照樣能夠實時更新。然而,對於測試者來說,這可不是什么好事,因為支持WebSockets的工具沒有像支持HTTP的工具那樣普遍,並且,這些工具用起來也更為復雜。

除了Burp Suite之外,還有一些其他工具也能用來處理WebSockets。雖然我們已經嘗試過所有的工具,但沒有一個完全符合我們的胃口。

  • Zed Attack Proxy(ZAP)
  • Pappy Proxy
  • Man-in-the-Middle Proxy(mitmproxy)
  • WebSocket/Socket.io(WSSiP)

對於希望通過WebSockets來繞過進攻端的安全檢測的讀者來說,可以參閱下面這篇文章。

https://www.blackhillsinfosec.com/command-and-control-with-websockets-wsc2/

在本文中,我們關注的重點是socket.io,這是一個流行的JavaScript WebSockets庫。然而,需要說明的是,文中介紹的攻擊思路不僅適用於其他庫,同時,也適用於WebSockets協議。

那么,socket.io到底有多受歡迎呢?它在Github上收獲了41,000多顆星。

同時,在NPM網站的WebSockets包排行榜上,它們還占據了第二名和第三名的位置。

事實上,就連優秀的OWASP Juice-Shop項目也采用了socket.io庫,因此,我們決定使用socket.io來完成相應的演示。

https://github.com/bkimminich/juice-shop/search?utf8=%E2%9C%93&q=socket.io&type= 

在本文中,我們假設讀者可以熟練使用Burp Suite測試Web應用程序,同時,文中涉及的所有測試工作,都可以利用該軟件的社區版本來完成。廢話少說,直入主題吧!

如果通過瀏覽器訪問Juice-Shop的話,就可以在后台快速考察WebSocket的流量了。為此,可以打開Burp,然后轉到Proxy-> WebSockets歷史記錄,從這里就可以看到相關的流量了。

我們知道,HTTP是一種無狀態協議,所以,它需要不停的發送請求/響應對;與此相反,WebSockets則是一種有狀態協議。這就意味着,我們可以從服務器獲得任意數量的傳出“請求”和任意數量的傳入“響應”。由於底層連接協議使用的是始終保持打開狀態的TCP協議,因此,客戶端和服務器可以隨時發送消息,而無需等待另一端。看到這里,您就能夠明白WebSockets歷史記錄視圖與HTTP歷史記錄之間的差異了。

在該視圖中,我們看到的,主要是發送和接收的單字節消息。但是,當應用程序執行一些有趣的操作時,我們將看到帶有更大的有效載荷的消息。

Burp提供的許多功能,也可以用來測試WebSockets。比如,Burp可以實時攔截和修改WebSocket消息,遺憾的是,Burp仍然缺乏針對WebSockets的Repeater、Scanner或Intruder功能。在Burp中,WebSocket攔截是默認啟用的,所以,我們只需打開主攔截即可。

我們將會收到截獲的WebSocket消息,這里跟處理HTTP消息的方式別無二致。同樣,我們也可以在攔截窗口中編輯這些消息。

之后,就可以在WebSockets歷史記錄選項卡中查看編輯后的消息了。

將WebSockets降級為HTTP


方法1:活用Socket.io的HTTP備用機制


我很快注意到了一件奇怪的事情:有時,我會在HTTP歷史記錄中看到類似於在WebSockets歷史記錄中所見過的消息。實際上,這個有趣的WebSockets消息與回答記分板質詢有關。下面展示的是來自服務器的相同響應,只不過這次是在HTTP歷史記錄中。所以,我斷定socket.io能夠通過WebSockets或HTTP發送消息。

之所以允許使用HTTP,根據我的推測,是為了在WebSockets不受支持或因某種原因而被阻止的情況下,使應用程序仍然可以正常運行。傳輸參數之所以會引起我的注意,是因為我在觀察相關請求過程中,發現其值有時候是“websockets”,有時候是“polling”。

在socket.io的文檔中,有一個章節對“polling”和“websockets”這兩個默認傳輸選項的運行機制進行了介紹。同時,它還介紹了如何通過將WebSockets指定為唯一的傳輸機制來禁用輪詢。不過,我認為反過來也是可行的,這樣,我們就可以將輪詢指定為唯一的傳輸機制了。

https://socket.io/docs/client-api/#with-WebSocket-transport-only

在瀏覽socket.io.js源代碼過程中,我無意中發現了以下代碼,看起來對我們非常有用。

this.transports=n.transports||["polling","WebSocket"]

上面這行代碼會將一個名為"transports"的內部變量設置為傳入的某個值,但是,如果傳入的值為false/empty的話,就將其設為默認的 [“polling”,”websocket”] 。到目前為止,這當然符合我們對輪詢和WebSockets的默認傳輸的理解。那么,接下來讓我們看看,當我們在Burp的Proxy->Options選項中通過設置匹配和替換規則來改變這些默認值后,會發生什么情況。

添加規則后,刷新頁面(必須啟用Burp的內置規則“Require non-cached response”或執行強制刷新),這樣,數據就不再通過WebSockets發送了。

這很好,但是,如果您使用的應用程序已經提供了優先於我們的新默認值的傳輸選項呢?在這種情況下,可以修改匹配和替換規則。下面給出的規則,將適用於socket.io庫的不同版本,並會忽略應用程序開發人員指定的任何傳輸選項。

為了便於復制粘貼,下面給出對應的字符串:

this\.transports=.*?\.transports\|\|\["polling","websocket"]

this.transports=["polling"]

請務必將其設置為正則表達式匹配。

方法2:阻止WebSockets升級


需要說明的是,方法1只能用於socket.io,不過,如果經過適當修改的話,也可以用於其他客戶端庫。但是,下面介紹的方法將更加通用,因為它是以WebSockets協議本身為目標。

經過一番研究后,我發現WebSockets會首先通過HTTP進行通信,以便與服務器進行相關的協商,然后將連接“升級”為WebSocket方式。為此,需要完成的重要工作包括:

1)客戶端發送帶有WebSocket某些特殊頭部的升級請求。

2)服務器將返回狀態碼101 Switching Protocols,以及WebSocket的某些特殊頭部。

3)通信轉換為WebSockets方式,之后,就看不到用於該特殊會話的HTTP請求了。

WebSockets RFC文檔的第4.1節提供了如何中斷這個工作流程的相關線索。

以下內容節選自https://tools.ietf.org/html/rfc6455#section-4.1頁面:

1.如果從服務器收到的狀態代碼不是101,那么客戶端仍然根據HTTP [RFC2616]過程來處理響應。特別是,如果客戶端收到401狀態代碼,則可能進行身份驗證;服務器可使用3xx狀態代碼重定向客戶端(但客戶端不需要遵循它們),等等。否則,請按以下步驟操作。

2.如果響應中缺少頭部字段Upgrade,或頭部字段Upgrade包含的值與大小寫敏感的ASCII字符串"WebSocket"不完全匹配的話,客戶端必須廢除這個WebSocket 連接。

3.如果響應中缺少頭部字段Connection,或頭部字段Connection包含的值與大小寫敏感的ASCII字符串"Upgrade"不完全匹配的話,客戶端必須廢除這個WebSocket連接。

4.如果響應中缺少Sec-WebSocket-Accept頭部字段,或Sec-WebSocket-Accept頭部字段的值並非是由Sec-WebSocket-Key(作為字符串,未經base64解碼)與字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"串聯起來的字符串(忽略任何前導和尾隨空格)的base64編碼后的SHA-1值的話 ,客戶端必須廢除這個WebSocket連接。

5.如果響應中包括Sec-WebSocket-Extensions頭部字段,並且該頭部字段要求使用的擴展並沒有出現在客戶端的握手消息中(服務器指示的擴展並非是客戶端所請求的),客戶端必須廢除這個WebSocket連接。(解析該頭部字段以確定請求哪些擴展的問題,將在第9.1節中討論。)

通過考察上面“必須廢除”連接的條件,我想出了如下文所示的一套替換規則,這些規則可以在上述五種情形下都會令客戶端廢除WebSocket連接。

一旦這些規則就位,所有WebSocket升級請求都會失敗。由於socket.io默認情況下無法使用HTTP,因此,這些規則完全可以達到理想的效果。當然,某些特殊實現或其他庫的行為可能有所不同,並導致您正在測試的應用程序出錯。但是我們的工作就是讓軟件做一些不應該做的事情!

原始響應看起來就是這種情況,並且會導致客戶端和服務器轉而使用WebSockets進行通信。

實際上,客戶端收到的響應雖然來自服務器,但是卻已經進行了相應的修改,按照RFC的規定,它會廢棄該WebSockets連接。

我在測試中遇到的一件事是,在使用這些匹配和替換規則后,客戶端在重試WebSockets連接時非常持久,並在我的HTTP歷史記錄中產生了大量不必要的流量。如果您正在處理socket.io庫,最簡單的方法就是使用上面介紹的第一種方法。如果您面對的是不同的庫或情況,則可能需要添加更多規則來讓客戶端認為服務器不支持WebSockets,甚至廢掉客戶端庫中的WebSockets功能。

將Burp Repeater用作Socket.io客戶端


由於我們已經迫使通信流量通過HTTP進行傳輸,而非通過WebSockets進行傳輸,所以,現在可以添加自定義的匹配和替換規則,將其應用於過去通過WebSockets傳輸的流量上面!

接下來,我們可以完成更進一步的修改,從而為使用Repeater、Intruder和Scanner等工具鋪平道路。當然,這些修改都是針對socket.io庫的。

不過,當我們重發socket.io使用的HTTP請求時,還面臨兩個問題。

  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頭部,只不過該字段的值近針對socket.io的有效載荷而已。例如,如果您要發送的有效載荷是“hello”,那么,相應的主體將是“5:hello”,Content-Length頭部的值是7。其中,5表示字符串“hello”中的字母數量,而7則表示字符串“hello”中的字母數量以及socket.io添加到主體內的字符串“5:”中的字母數量之和。與往常一樣,Burp將替我們更新Content-Length頭部,因此,這件事情我們無需擔心。但是,我還沒有找到能夠自動計算和包含有效載荷長度的好方法。更讓人頭疼的是,我發現socket.io竟然會在同一個HTTP請求中發送多條消息。由於每個消息都是一個封裝后的WebSockets有效載荷,並且每個消息都有自己的長度,因此,最終看起來就像這樣:“5:hello,4:john,3:doe”(實際的語法可能有所不同,這里只是便於演示)。計算長度時一旦出錯,服務器就會將其作為無效消息拒絕,這樣,我們就又不得不重新面對第一個問題了。

下面是一個消息體的示例。該示例取自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”更新原始請求。要想使用宏指令,請選擇Project options->Sessions->Macros->Add選項,這樣就可以創建新的宏指令了。

為了獲取建立新會話的URL,只需省略“sid”參數即可,例如:

/socket.io/?EIO=3&transport=polling&t=MJJJ4Ku 

我發現,“t”的值並不重要,所以我沒有理會它。

服務器響應包含了一個供我們使用的全新“sid”值。

接下來,單擊“Configure item”按鈕,並將參數名稱命名為“sid”。然后,選擇“Extract from regex group”選項,並使用如下所示的正則表達式。

"sid"\:"(.*?)"

這時,配置窗口應如下所示:

會話處理規則


現在,我們已經建好了一個宏指令,接下來要做的是,設法觸發它。這時候,Burp會話處理規則就派上用場了。為此,請選擇Project options->Sessions->Session Handling Rules->Add菜單項。

為“Check session is valid”創建新的規則動作

對新的規則動作進行配置,具體如下:

最后,在弄好新的規則動作后,還要修改規則的應用范圍,也就是說,處決定要將該規則應用到哪些地方。我建議至少將其應用於Repeater上面,這樣,我們就可以手動方式重發請求了。

我的規則應用范圍的配置如下所示。當然,您也可以讓應用范圍更加具體,但下面的選項應該能夠適用於大多數情況。

這里是在沒有會話處理規則的情況下發出的請求。

這里是在會話處理規則生效后發出的同一請求。請注意,會話處理規則“透明地”更新了cookie以及請求中“sid”參數的值。

結束語


最后說明一點,在處理上面討論的第2個問題的時候,由於無法使用Burp的Scanner和Intruder插件,於是,我修改了一個現有的Burp插件,這似乎可以完成這項工作,不過,由於某些原因,服務器對它並不友好。如果讀者有興趣進一步研究這個問題的話,請隨時與我聯系。

 

來源:https://xz.aliyun.com/t/2572


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM