Sliver's Magic Number: Crashing C2 with a Single Packet
Jan 6, 2026·8 min read
Executive Summary (TL;DR)
An unauthenticated attacker can send a huge HTTP request with a specially crafted nonce (any multiple of 65537) to the Sliver C2 server. This bypasses size checks, forcing the server to allocate massive amounts of memory, which crashes the process and terminates all active C2 sessions.
A critical vulnerability exists in the Sliver Command-and-Control (C2) framework that allows an unauthenticated, remote attacker to trigger a memory exhaustion condition, leading to a denial-of-service. The flaw stems from an unchecked code path for anonymous requests where the server reads the entire HTTP request body into memory without any size limits. This bypass is triggered by providing a specific 'magic number' as a nonce, effectively giving the attacker control over the server's memory allocation.
The C2 That Cried Uncle
Sliver is a popular, open-source command-and-control framework. For red teamers, it's the central nervous system of an operation, the conductor of a malicious orchestra. It's designed to be robust, stealthy, and resilient. After all, if your C2 goes down, your entire operation is deaf, dumb, and blind.
A C2 server, by its very nature, is an internet-facing fortress. It's built to withstand scrutiny from blue teams and rival threat actors. You'd expect the front gates to be heavily armored, with every visitor frisked and vetted. The developers at Bishop Fox clearly put a lot of effort into securing the authenticated pathways, the ones used by legitimate implants checking in.
But security is like a chain; it's only as strong as its weakest link. In this case, the weakness wasn't a complex cryptographic flaw or a clever logic bug in the state machine. It was something far more mundane, a classic blunder: forgetting to check the size of a package delivered by a complete stranger, just because they knew a secret handshake.
The VIP Lane to Oblivion
In network services, the line between authenticated and unauthenticated is sacred. Authenticated sessions have proven their identity; they get access to the castle's inner chambers. Unauthenticated sessions are the peasants at the gate, and they should be treated with extreme suspicion. Sliver's developers understood this, which is why their authenticated request handler, readReqBody(), is properly paranoid, using an io.LimitedReader to cap how much data it's willing to accept.
However, there's another entrance: the anonymousHandler(). This handler is designed for initial implant check-ins before a full session is established. It uses a nonce to decide how to decode the incoming traffic. And here lies the fatal oversight. If an attacker provides a nonce that is a perfect multiple of the prime number 65537, the server shrugs and says, "Ah, you're with the NoEncoder party. No checks needed, come right in!"
This NoEncoder path is a VIP lane that bypasses all the usual security checks. It leads directly to the startSessionHandler(), which, in its blissful ignorance, assumes the incoming request is reasonably sized. This is the classic mistake of locking the heavily fortified front door while leaving the barn-sized loading dock wide open for anyone who knows the magic password, which in this case, isn't even a secret.
The Smoking Gun: A Tale of Two Readers
Let's dive into the code and see exactly where the wheels fall off. The whole mess starts in server/encoders/encoders.go, where the server decides which decoder to use based on the nonce.
// server/encoders/encoders.go
func EncoderFromNonce(nonce uint32) (Encoder, error) {
// ... some cases ...
// Passthrough for staging, etc.
if nonce%65537 == 0 {
return &nop.NoEncoder{}, nil
}
// ... other cases ...
}That nonce % 65537 == 0 is our golden ticket. If we send a nonce like 65537, 131074, etc., we get the NoEncoder. This routes our unauthenticated request to startSessionHandler() in server/c2/http.go.
Here's the vulnerable code:
// server/c2/http.go (The Vulnerable Path)
func (s *HTTPC2) startSessionHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
// ... some setup ...
// Oh dear. This reads EVERYTHING.
body, err := io.ReadAll(req.Body)
if err != nil {
// ... error handling ...
return
}
// ... tries to process the giant body ...
}
}io.ReadAll is a simple, honest function. It does exactly what it's told: read from the source until it hits an error or EOF. It has no concept of limits. If the client says it's sending 10 gigabytes of data, io.ReadAll will happily try to allocate a 10-gigabyte buffer in memory. Contrast this with the correct way to handle this, found in the authenticated path:
// server/c2/http.go (The Secure Path)
func readReqBody(req *http.Request) ([]byte, error) {
// Create a reader that is limited to a sane size
lr := io.LimitedReader{R: req.Body, N: int64(MaxRequestSize)}
// Now, ReadAll is safe!
body, err := io.ReadAll(&lr)
if err != nil {
// ... error handling ...
}
return body, nil
}The difference is night and day. io.LimitedReader acts as a chaperone, slapping the hand of io.ReadAll if it gets too greedy. The fix is simply to apply this same logic to the unauthenticated handler. It's a textbook case of inconsistent security controls.
Weaponizing a Magic Number
Exploiting this flaw is hilariously simple. You don't need a complex ROP chain or a memory-massaging heap spray. All you need is a tool like curl and the ability to count. The goal is to make the server allocate more memory than it has, causing the operating system's OOM (Out-Of-Memory) killer to terminate the process.
First, we construct the attack flow. The attacker's machine sends a specially crafted POST request. The server's routing logic checks the nonce, sees our magic number, and directs the request down the path of least resistance—and least security.
Here's a one-liner to execute the attack. We'll tell the server we're sending a large file but just pipe in zeros from /dev/zero. The server won't care what the data is; it will just try to read it all into memory.
# Tell the server we are sending a 10GB file
# and use a nonce that is a multiple of 65537.
# We pipe /dev/zero to continuously stream data.
curl -X POST 'http://<sliver_c2_ip>:<port>/?q=65537' \
--header 'Content-Type: application/octet-stream' \
--header 'Content-Length: 10000000000' \
--data-binary @/dev/zeroRun this, and then watch the memory usage of the Sliver process on the server. You'll see it climb rapidly, plateau as the system starts swapping frantically, and then suddenly, the process will vanish. Your curl command will die with a broken pipe. Mission accomplished. The C2 is down.
Dropping Shells and Dropping Calls
A denial-of-service vulnerability might sound less exciting than remote code execution, but in the context of C2 infrastructure, it's devastating. This isn't just about making a website temporarily unavailable. This is about pulling the plug on an active offensive operation.
When the Sliver process crashes, every single active implant session is severed. All those shells, all those beacons—gone. The red team operators are instantly blinded, their access revoked. They are left staring at dead terminals, unable to issue commands or exfiltrate data. The C2 server is effectively bricked until an administrator manually restarts it.
This creates a huge window of opportunity for the blue team. A savvy defender who sees a C2 server flap (go down and come back up) might get suspicious and start digging. Worse, a determined attacker can simply re-run the exploit the moment the server comes back online, creating a sustained outage. This can prevent the red team from ever re-establishing control, effectively burning their entire infrastructure and forcing them to rebuild.
Putting a Leash on the Data Stream
The fix, as you might guess, is straightforward: treat all input as hostile. The developers need to apply the same io.LimitedReader logic from the authenticated path to the vulnerable unauthenticated path. This ensures that no matter what Content-Length header an attacker sends, the server will refuse to read more than a predefined, sane amount of data.
If you're running a vulnerable version and can't update immediately, you're not totally out of luck. A solid workaround is to place a reverse proxy like Nginx or Caddy in front of the Sliver listener. You can configure the proxy to enforce a maximum request body size. For example, in Nginx, you would use the client_max_body_size directive.
# nginx.conf
server {
listen 80;
server_name your.c2.domain;
# Don't let anyone send more than 10MB
client_max_body_size 10M;
location / {
proxy_pass http://127.0.0.1:8080; # Your actual Sliver listener
# ... other proxy settings ...
}
}This acts as an external bodyguard, throwing out oversized packages before they even reach Sliver's front door. Ultimately, though, the real lesson is a simple one that developers have to learn over and over: never, ever trust the client, especially when it comes to resource allocation. Validate everything.
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Sliver Bishop Fox | Refer to vendor advisory | - |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 |
| CWE Name | Uncontrolled Resource Consumption |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| CVSS v3.1 Score | 7.5 (High) |
| Impact | Denial of Service |
| Exploit Status | Proof-of-Concept Available |
MITRE ATT&CK Mapping
The software does not properly control the allocation and maintenance of a limited resource, such as memory, which can lead to a denial of service when the resource is exhausted.
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.