Skip to content

Your C2 Is a WebSocket Upgrade and the Proxy Logged It as Chat

WebSocket C2 Channel Lab: Upgrade, Subprotocol, Detection

Your C2 Is a WebSocket Upgrade and the Proxy Logged It as Chat

HTTP beacons are so 2018. the implant sends Connection: Upgrade, Sec-WebSocket-Protocol: chat.v1, Origin set to something wholesome, and then rides a long-lived TLS session exchanging JSON frames that look like typing indicators. your proxy wrote status 101 and went back to sleep. your SIEM does not have a use case for “socket that never ends.”

Lab only. server and implant below bind to 127.0.0.1 on a disposable lab. do not point WebSocket C2 at production chat infrastructure you do not own. authorized channel testing and detection engineering only.

the trick in one paragraph

WebSockets (RFC 6455) start as HTTP and become a bidirectional frame pipe after a 101 upgrade. to a TLS-inspecting proxy that does not parse frames, that is “a browser opened a socket.” to a C2 author, that is interactive tasking without the awkward rhythm of periodic GETs. subprotocols, Origin headers, and browser-like JA3 make it rude to fingerprint from metadata alone.

Kali terminal starting a tiny Python WebSocket C2 server on localhost with subprotocol chat.v1.
Step 1: lab listener. subprotocol and Origin are the costume.

step 1: stand up a toy C2

websockets.serve(handler, '127.0.0.1', 8443, subprotocols=['chat.v1'])

real operators put this behind nginx or Cloudflare with a boring path like /socket or /ws/v1/notifications. the lab stays local so you can see both ends without involving a CDN ToS team.

Kali terminal implant connecting with Origin https://chat.lab.local, receiving a whoami task, and answering.
Step 2: implant upgrade + tasking. same shape as a chat client.

step 2: implant side

connect with a browser-ish Origin and the agreed subprotocol. send a hello. receive JSON ops. respond. encrypt inside the frames if you are not already trusting TLS alone (you should not). jitter by staying connected and whispering occasionally instead of reconnecting every sixty seconds like a guilty HTTP beacon.

Kali terminal showing proxy JSON logs with HTTP 101 upgrade to websocket and an established session on port 8443.
Step 3: proxy view. status 101, upgrade websocket, SNI looks fine.

step 3: what logs actually capture

most enterprise proxies record the upgrade and then… nothing useful. bytes in/out maybe. not opcodes. not that every binary frame is 512 bytes on a five-minute metronome. if you terminate TLS, you can do better. if you do not, you are trusting SNI and vibes.

Kali terminal hunting long-lived WebSocket flows with high byte counts to a lab chat SNI.
Step 4: hunt long-lived WS, weird subprotocols, servers that should not chat.

why defenders care

  • 101 is not benign by default: it is a protocol switch you probably do not parse.
  • beacon math changes: duration and frame cadence matter more than request count.
  • domain fronting cosplay: WS over a “known” chat or notifications host blends into allowlists.
  • interactive C2: operators get shells that feel local without opening raw TCP 443 to an IP from 2009.

controls that are not theater

  1. allowlist WebSocket destinations per app. default deny for WS egress from servers.
  2. terminate TLS on a proxy that logs subprotocol, Origin, frame counts, and duration.
  3. alert: WS sessions > N minutes from hosts that are not browsers; binary frame size entropy; unknown subprotocols.
  4. JA4 / process correlation: python/curl doing WS from a finance VM is a ticket.
  5. for red team: scope the domains, document the upgrade path, tear down when the test ends.
honesty check: blocking all WebSockets breaks real products. the mature move is inventory which apps need them, then treat everything else like the suspicious tunnel it is.

sloth take

you taught the SOC to hate strange POSTs to /gate.php. attackers upgraded the connection and kept talking. status 101 is not a participation trophy. parse the frames or accept that “chat” is how shells commute now.