websocket tunneling
on this page
websocket tunneling exploits the websocket protocol’s persistent, bidirectional communication to create high-performance covert channels. after initial http handshake, websocket provides near-native tcp performance with minimal overhead.
technical description
websocket (rfc 6455) upgrades http connections to full-duplex communication channels:
- initial http upgrade request
- persistent connection after handshake
- binary or text frame support
- minimal framing overhead (~2-14 bytes)
- works over standard ports (80/443)
- tls encryption on wss://
protocol upgrade:
GET /socket HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
implementation: wstunnel
overview
wstunnel (https://github.com/erebe/wstunnel) provides high-performance tunneling:
- websocket and http/2 transport
- tls/mtls support
- udp over websocket
- reverse tunneling
- achieves gigabit speeds
installation
# download binary
wget https://github.com/erebe/wstunnel/releases/latest/download/wstunnel-linux-x64
chmod +x wstunnel-linux-x64
sudo mv wstunnel-linux-x64 /usr/local/bin/wstunnel
# or using cargo
cargo install wstunnel
server setup
# basic websocket server
wstunnel server ws://0.0.0.0:8080
# with tls (wss)
wstunnel server wss://0.0.0.0:443 --tls-cert cert.pem --tls-key key.pem
# with client authentication
wstunnel server wss://0.0.0.0:443 \
--tls-cert cert.pem \
--tls-key key.pem \
--restrict-to 127.0.0.1:22
# http2 transport
wstunnel server --http2 https://0.0.0.0:443
client usage
# forward local port to remote
wstunnel client -L tcp://8000:localhost:22 ws://server:8080
# multiple forwards
wstunnel client \
-L tcp://8000:localhost:22 \
-L tcp://8080:localhost:80 \
ws://server:8080
# udp forwarding
wstunnel client -L udp://5353:8.8.8.8:53 ws://server:8080
# reverse tunnel (expose local service)
wstunnel client -R tcp://0.0.0.0:8080:localhost:3000 ws://server:8080
# through http proxy
wstunnel client --http-proxy proxy:3128 \
-L tcp://8000:localhost:22 \
wss://server:443
implementation: websockify
overview
websockify (https://github.com/novnc/websockify) bridges websocket to tcp:
- originally for novnc
- python and c implementations
- supports multiple backends
- token-based authentication
setup
# install
pip install websockify
# basic proxy
websockify 6080 localhost:5900
# with ssl
websockify --cert cert.pem --key key.pem 6443 localhost:22
# with authentication
websockify --auth-plugin BasicAuth --auth-source users.txt 6080 localhost:5900
# token-based targets
echo "token1: localhost:22" > tokens.txt
websockify --token-plugin TokenFile --token-source tokens.txt 6080
javascript client
// browser websocket client
const ws = new WebSocket('wss://server.com:6443');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
console.log('tunnel connected');
// send ssh traffic through websocket
};
ws.onmessage = (event) => {
// receive tunneled data
const data = new Uint8Array(event.data);
processData(data);
};
traffic analysis
websocket frame structure
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 |
+-------------------------------+-------------------------------+
packet analysis
from scapy.all import *
import struct
def analyze_websocket(packet):
if packet.haslayer(TCP) and packet[TCP].payload:
payload = bytes(packet[TCP].payload)
# check for websocket upgrade
if b'Upgrade: websocket' in payload:
print("websocket handshake detected")
return
# analyze websocket frame
if len(payload) >= 2:
byte1, byte2 = struct.unpack('BB', payload[:2])
fin = (byte1 >> 7) & 1
opcode = byte1 & 0x0f
masked = (byte2 >> 7) & 1
payload_len = byte2 & 0x7f
if opcode in [0x1, 0x2]: # text or binary frame
print(f"websocket data: opcode={opcode}, len={payload_len}")
sniff(filter="tcp port 8080", prn=analyze_websocket)
performance characteristics
bandwidth
- overhead: ~2% compared to raw tcp
- throughput: 95-98% of tcp capacity
- gigabit speeds achievable
- minimal latency addition
scalability
- thousands of concurrent connections
- low memory footprint per connection
- efficient event-driven architecture
- multiplexing over single tcp connection
comparison
transport | overhead | latency | detection |
---|---|---|---|
raw tcp | 0% | baseline | easy |
websocket | 2-5% | +1-5ms | medium |
http polling | 50-200% | +100-500ms | easy |
http/2 | 5-10% | +5-10ms | hard |
detection methods
behavioral indicators
- long-duration websocket connections
- high data volume over websocket
- websocket to unusual ports
- consistent bidirectional traffic
- connections to non-web services
detection rules
# suricata rule
alert http any any -> any any (
msg:"websocket tunnel - suspicious upgrade";
content:"Upgrade|3a 20|websocket";
flow:to_server,established;
threshold: type limit, track by_src, count 5, seconds 60;
sid:1000005;
)
# zeek script
event websocket_established(c: connection, aid: count, resp: response) {
if (c$duration > 3600) # connection over 1 hour
print fmt("long websocket: %s -> %s", c$id$orig_h, c$id$resp_h);
}
traffic analysis
def detect_websocket_tunnel(flows):
suspicious = []
for flow in flows:
if flow['protocol'] == 'websocket':
# check data patterns
if flow['bytes'] > 10_000_000: # >10MB
suspicious.append(flow)
# check duration
if flow['duration'] > 3600: # >1 hour
suspicious.append(flow)
# check packet frequency
if flow['packets_per_second'] > 100:
suspicious.append(flow)
return suspicious
countermeasures
proxy filtering
# nginx websocket restrictions
location /websocket {
# limit connection duration
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# restrict origins
if ($http_origin !~ '^https://trusted\.com$') {
return 403;
}
# rate limiting
limit_req zone=ws_limit burst=10;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
content inspection
# websocket frame inspection
def inspect_websocket_frame(frame_data):
# check for tunneling patterns
if len(frame_data) > 1000: # large frames
entropy = calculate_entropy(frame_data)
if entropy > 7.5: # likely encrypted
return "suspicious"
# check for protocol signatures
signatures = [
b'SSH-2.0', # ssh protocol
b'\x05\x01\x00', # socks5
b'GET /', # http in websocket
]
for sig in signatures:
if sig in frame_data:
return "tunnel_detected"
return "clean"
advantages and limitations
advantages
- standard protocol, widely supported
- works through proxies
- tls encryption built-in
- excellent performance
- bidirectional communication
- browser compatibility
limitations
- requires websocket support
- initial http handshake visible
- connection-oriented (tcp only)
- some firewalls block websocket
- easier to detect than http polling
real-world usage
legitimate uses
- real-time web applications
- chat applications
- live streaming
- gaming
- financial trading platforms
malicious usage
- 2019: apt groups using websocket c2
- 2020: cryptominers via websocket
- 2021: ransomware c2 channels
commercial tools
- cobalt strike: websocket listeners
- metasploit: websocket payloads
- pupy rat: websocket transport
implementation variations
socket.io tunneling
// server
const io = require('socket.io')(3000);
const net = require('net');
io.on('connection', (socket) => {
const client = net.connect(22, 'localhost');
socket.on('data', (data) => {
client.write(data);
});
client.on('data', (data) => {
socket.emit('data', data);
});
});
// client
const socket = io('http://server:3000');
socket.on('data', (data) => {
// handle tunneled data
});
golang implementation
// simple websocket tunnel
package main
import (
"github.com/gorilla/websocket"
"net"
)
func tunnel(ws *websocket.Conn, target string) {
tcp, _ := net.Dial("tcp", target)
// ws -> tcp
go func() {
for {
_, data, _ := ws.ReadMessage()
tcp.Write(data)
}
}()
// tcp -> ws
buffer := make([]byte, 4096)
for {
n, _ := tcp.Read(buffer)
ws.WriteMessage(websocket.BinaryMessage, buffer[:n])
}
}
testing setup
local testing environment
# start websocket tunnel server
wstunnel server ws://0.0.0.0:8080
# setup client tunnel
wstunnel client -L tcp://2222:localhost:22 ws://localhost:8080
# test ssh through tunnel
ssh -p 2222 localhost
# monitor traffic
tcpdump -i lo -w websocket_tunnel.pcap 'port 8080'
performance testing
# iperf through websocket tunnel
# server side
iperf3 -s
# client through tunnel
wstunnel client -L tcp://5201:server:5201 ws://server:8080
iperf3 -c localhost -t 60
# compare with direct connection
iperf3 -c server -t 60
references
- rfc 6455: the websocket protocol
- “websocket security considerations” - owasp
- “tunneling tcp over websockets” - black hat presentation
- wstunnel documentation: https://github.com/erebe/wstunnel
- websockify project: https://github.com/novnc/websockify