A Technical Deep-Dive into Server-Sent Events for C2 Communication
Innovation often arises from unexpected places. A project intended to explore the Model Context Protocol (MCP) for Agentic AI led to an in-depth analysis of a less-common protocol: Server-Sent Events (SSE). This exploration revealed a novel method for creating a stealthy, persistent reverse shell using only native PowerShell.
This article provides a technical walkthrough of this proof-of-concept, detailing:
-
An overview of Server-Sent Events (SSE) and its mechanics.
-
The strategic advantages of using SSE for reverse shell communication.
-
A step-by-step guide to building a functional listener (server) and client in PowerShell.
This content is for builders, defenders, and security professionals interested in exploring unconventional C2 channels and enhancing defensive strategies.
What Are Server-Sent Events (SSE)?
SSE is a lightweight HTTP-based protocol where the server (listener) pushes real-time data to the client. Unlike WebSockets, SSE is one-way only (server (listener) ➝ client) and requires no special handshake.
A typical SSE stream looks like:
The client makes a single GET request, and the server (listener) keeps that connection open, pushing events over time.
-
It's just HTTP (firewall-friendly)
-
It's long-lived (persistent connection)
-
It's simple to implement
The initial thought was clear: could this one-way, persistent stream be used to send commands to a reverse shell? This question shifted the project's focus entirely.
Understanding SSE for Real-Time Control
Server-Sent Events (SSE) enable a server to stream real-time updates to a client through a single, persistent HTTP connection. Much like subscribing to a live news feed, the client establishes an initial connection, allowing the server to push continuous updates as they occur. Each message is transmitted as lightweight plain text, formatted with a `data:` prefix followed by the payload and a newline. Because SSE operates over standard HTTP, it eliminates the need for complex WebSocket handshakes or inefficient polling, offering a seamless and efficient way to maintain a live data stream.
A user should see a single GET request hanging open, with the server periodically writing lines like:
data: server event (what's after data: could be anything)
data: server event
data: server event
data: server event
Why Use SSE for a Reverse Shell?
SSE provides a stealthy solution for a reverse shell with several advantages:
-
Commands are initiated by the server (listener), not the client.
-
Communication uses HTTP, making it accessible through any HTTP client, such as PowerShell's Invoke-WebRequest (IWR) or even a browser.
-
It leaves a smaller network footprint compared to polling or WebSockets.
-
The client only connects once, while the server sends new commands as needed.
Communication remains open and low-noise:
- The page appears as a standard placeholder for real-time updates, discreetly housing a command-and-control link.
Common reverse shell techniques typically rely on:
-
Raw Sockets (often blocked by EDRs)
-
HTTP polling (client continuously requests commands)
-
WebSockets (noisier due to dual-direction communication)
This shell listener minimizes its footprint by operating without additional tools or binaries, unlike traditional payloads that depend on utilities like Netcat or similar tools:
-
Fully native PowerShell implementation
-
No external tools or binary droppers required
Why this approach works:
Most TCP-based PowerShell shell techniques are well-documented, heavily detected by antivirus systems, and routinely blocked by EDRs. By leveraging built-in PowerShell classes, the entire operation remains:
-
Memory-resident
-
EDR-evasive
-
Free from special dependencies
The Goal: A Real-Time PowerShell Reverse Shell
Here is a look at the initial prototype: a streamlined HttpListener designed to broadcast timestamps at one-second intervals.
Building the Listener (Server (Listener) Side)
Start with the trustworthy HttpListener. It's a bit old-school but reliable, and it initiates the server (listener).
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://localhost:3007/")\
$listener.Start()
Pause until a client connects. GetContext() halts execution until a connection comes in, then we grab the request and response objects to start handling it.
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response
These headers are required to initiate a proper SSE stream.
if ($request.Url.AbsolutePath -eq "/stream") {
$response.StatusCode = 200
$response.ContentType = "text/event-stream"
$response.Headers.Add("Cache-Control", "no-cache")
$response.Headers.Add("Connection", "keep-alive")
Streaming Events. Create a StreamWriter to write messages into the response stream.
$writer = New-Object System.IO.StreamWriter($response.OutputStream)
$writer.AutoFlush = $true
Sending the current time through the stream.
for ($i = 0; $i -lt 60; $i++) {
$time = (Get-Date).ToString("HH:mm:ss")
$writer.WriteLine("data: $time`n")
Start-Sleep -Seconds 1
}
After running the server, navigate to the /stream endpoint to watch it in action while inspecting it in the browser's DevTools:
When analyzing the traffic in Wireshark, the expected behavior is confirmed:
-
A single persistent GET request to `/stream`.
-
All communication flows from the server to the client.
-
No additional client requests for updates.
This demonstrates that the connection remains open, with the server solely transmitting data — a crucial advantage for maintaining stealth.
Building the SSE Client
With the listener working and pushing timestamp data, it was time to build the other half: a minimalist PowerShell client. This client connects to the `/stream` endpoint and listens for incoming commands or events.
The goal was to avoid relying on external libraries or `curl.exe` and use native PowerShell only. Here is the barebones setup:
The flow of the client script works as follows:
-
A single HTTP GET request is made to /stream.
-
The Accept: text/event-stream header ensures the server recognizes it as an SSE (Server-Sent Events) request.
-
Once the connection is established, the stream remains open and is read line by line.
-
Messages starting with data: are parsed and printed.
-
The client then executes the string as a command.
Benefits of this approach:
-
A persistent connection remains open as long as the server allows.
-
Every message pushed by the server is promptly received by the client.
For demonstration purposes, a small change was made on the server to illustrate command injection:
$time = "whoami"
Achieved an SSE-based RCE.
Putting It All Together: The SSE Reverse Shell in Action
With a solid foundation, an SSE-based setup for pushing and executing commands, the transition to a fully functional reverse shell came together seamlessly.
The SSE server operates as the command sender, while the SSE client receives SSE events and executes them, creating a streamlined and efficient system.
The shell functions as follows:
-
Commands are sent via /stream
-
Outputs are returned via /result
The result is a clean, silent and persistent reverse shell, built entirely on native PowerShell.
What's in the Repository?
Explore the repository here: GitHub - TNCX-byte/PS_SSE_Shell
This repository includes:
-
A complete SSE Reverse Shell Server
-
A fully functional SSE Reverse Shell Client
Final Thoughts
Building this was never just about creating a shell — it was about exploring overlooked protocols, leveraging unconventional channels and doing more with less.
The combination of PowerShell and SSE provides:
-
Long-lived communication without the need for constant polling.
-
Execution control without requiring inbound or multiple outbound connections.
-
A memory-resident shell that remains hidden in plain sight.
There are countless ways to build a C2. This approach simply utilizes the same protocol powering live stock tickers and GitHub notifications.

