← Back to Feed CACHED · 2026-06-29 08:29:36 · CACHE_KEY CVE-2026-58050
CVE-2026-58050 · CWE-190 · Disclosed 2026-06-28

libssh2 publickey-subsystem attribute-count integer overflow leading to heap buffer overflow in client

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

A 32-bit SSH client trusting a hostile server's math homework and getting handed a 4-byte buffer to write 200MB into

libssh2 ≤ 1.11.1 calls libssh2_publickey_list_fetch() to enumerate keys from a server's publickey subsystem. The parser reads an attacker-controlled 32-bit num_attrs from the wire and computes num_attrs * sizeof(libssh2_publickey_attribute) to size an allocation — with no overflow check. On 32-bit builds, num_attrs = 0x0ccccccd wraps the multiplication to 4 bytes, after which the attribute-parsing loop happily writes past the tiny heap chunk. The public PoC (bikini/exploitarium) demonstrates calc.exe execution on Win32 and a separate free/double-free chain on Win64 via a malformed version response. The bug lives in src/publickey.c; everything that links libssh2 — curl, git, libcurl-based agents, OpenWrt droplets, embedded clients — inherits it.

Vendor HIGH (7.0) is *defensible but optimistic*. The CVSS vector encodes the reality: AV:N/AC:H/PR:N/UI:N, meaning network reachable but high complexity because you have to either coerce a victim to connect to your malicious SSH server or MITM an existing connection. The integrity/confidentiality impact is :L only because the primary outcome is corrupted client memory; availability is :H because you can absolutely crash the client. The real-world ceiling depends on whether the libssh2 build is 32-bit — increasingly rare on servers, still common on embedded routers/IoT and some Windows installer toolchains. For a 64-bit Linux fleet running git-over-ssh against trusted forges, this is a backlog item, not a fire.

"Client-side heap overflow that needs a malicious SSH server AND a 32-bit client — vendor HIGH is generous for 2026 reality."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Stand up a malicious SSH server

Attacker runs a doctored sshd (or PoC harness from bikini/exploitarium) that advertises the publickey subsystem and answers publickey-list with a forged response. Off-the-shelf libssh/Twisted-conch are trivial to modify; the PoC ships a Python replay runner.
Conditions required:
  • Public reachability or position to redirect victim
  • Ability to speak SSH transport up through subsystem negotiation
Where this breaks in practice:
  • No skill barrier — PoC is public
Detection/coverage: Network IDS won't see anything anomalous in the encrypted channel; only an SSH-aware proxy with deep inspection would notice malformed publickey-list responses.
STEP 02

Coerce a libssh2-based client to connect

Victim must initiate (or be redirected to) an SSH session that invokes the publickey subsystem — note: this is the SECSH-publickey *subsystem* (RFC 4819), NOT routine pubkey authentication. Real callers are sparse: some key-management tooling, a handful of automation agents, and clients explicitly calling libssh2_publickey_init(). Most curl/git/scp paths never touch this subsystem.
Conditions required:
  • Victim uses a libssh2 client that calls publickey-subsystem APIs
  • Victim connects to attacker-controlled host (phishing repo URL, DNS poison, MITM)
Where this breaks in practice:
  • Vast majority of libssh2 consumers (curl, git, ansible via paramiko-not-libssh2) never invoke the publickey subsystem
  • Known-hosts pinning blocks naive MITM
  • Most enterprise SSH traffic terminates at trusted bastions
Detection/coverage: EDR/proxy can flag connections to untrusted SSH destinations; host-pinning failures show up in client logs.
STEP 03

Send the overflow trigger

Server replies to the list request with num_attrs = 0x0ccccccd. On a 32-bit libssh2 build, num_attrs * sizeof(libssh2_publickey_attribute) wraps to a 4-byte heap allocation. The follow-on parser loop then streams attacker-controlled key/value pairs past the chunk boundary.
Conditions required:
  • Target compiled as ILP32 / 32-bit
  • Heap allocator returns chunk without trap-guard
Where this breaks in practice:
  • 64-bit builds do not wrap on this multiplication — the dominant deployment on modern Linux/Windows is immune to *this* primitive
  • Hardened allocators (scudo, GWP-ASan, Windows segment heap) raise reliability cost
Detection/coverage: ASAN/MSAN catches in CI; runtime crashes surface as heap corruption signatures in WER/coredumps.
STEP 04

Pivot heap corruption to code execution

PoC overwrites an adjacent callback pointer and redirects control flow — calc.exe proven on Win32. Achieving the same on hardened 64-bit Linux requires the *separate* free-chain bug also documented in the PoC (Win64 path), which is a different primitive against libssh2_publickey_list_free().
Conditions required:
  • ASLR/CFG bypass info-leak or known-target
  • Writable function-pointer adjacent in heap layout
Where this breaks in practice:
  • CFG/CET on Windows, Intel CET shadow stacks, fortify_source on Linux — none are silver bullets but each costs reliability
  • Target process privilege is whatever the libssh2 caller runs as — usually a user-level CLI, not SYSTEM
Detection/coverage: EDR memory-protection (Cylance/Defender/CS) flags indirect-call anomalies; libssh2 callers rarely have legitimate JIT regions.
03 · Compensating Control

1
Inventory libssh2 consumers by architecture — Within the noisgate MEDIUM window (no mitigation SLA, 365-day remediation), inventory hosts running libssh2-linked binaries. Use ldd $(which curl git ssh) | grep libssh2 on Linux, dumpbin /dependents on Windows. Tag any 32-bit results as priority — that's your primary-RCE exposure. Embedded/router firmware vendors should be queried directly.
2
Block outbound SSH egress to untrusted destinations — Egress firewall rule: deny TCP/22 outbound except to known bastions, forges (github/gitlab/bitbucket IP ranges), and approved jump hosts. Kills the malicious-server prerequisite for every non-developer host. Implement within 30 days as standard hygiene — useful well beyond this CVE.
3
Pin SSH known_hosts and enforce StrictHostKeyChecking=yes — For any libssh2-using automation, ship a managed known_hosts and set LIBSSH2_HOSTKEY_HASH_* verification in calling code. Prevents trivial MITM redirect onto an attacker server.
4
Update libssh2 when distro packages land — Track CVE-2026-58050 in Debian/Ubuntu/RHEL/Alpine security trackers; deploy the backport during normal monthly patch cycle. Per noisgate remediation SLA for MEDIUM, complete within 365 days — accelerate to 30 days for any host you tagged as 32-bit or running publickey-subsystem automation.
5
Audit code for publickey-subsystem callers — Grep your codebase and dependencies for libssh2_publickey_init, libssh2_publickey_list_fetch, libssh2_publickey_add. If you find none, you can defer this CVE to backlog hygiene with documented rationale.
What doesn't work
  • Disabling SSH server pubkey auth — irrelevant, this is a *client-side* bug triggered by a malicious server.
  • Rotating SSH keys — the bug is in protocol parsing, not key material.
  • WAF / IDS signatures on encrypted SSH traffic — you cannot inspect inside the transport layer without terminating it.
  • Disabling the publickey-authentication method in sshd_config — wrong subsystem; this is RFC 4819 publickey-*subsystem* used for key management, not pubkey auth.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed. Disclosed 2026-06-28; no campaign telemetry from CrowdStrike, Mandiant, or KEV.
Public PoCbikini/exploitarium — working Win32 RCE (calc.exe) plus Win64 free-chain primitive; Python replay runner included.
EPSS0.00311 (~0.3 percentile band) — minimal exploit-likelihood signal.
KEV statusNot listed as of 2026-06-29.
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H — network but AC:H because you must lure a victim or MITM. VulnCheck assigns CVSS v4 8.3.
Affected versionsAll libssh2 releases through 1.11.1, primarily exploitable on 32-bit (ILP32) builds. 64-bit builds vulnerable to the secondary free-chain only.
Fixed versionPatched in upstream master (zero-out list[keys] after growth + reject overflow num_attrs); expect distro backports for 1.11.1-x to land within days — track Debian/Ubuntu/RHEL trackers.
Affected codesrc/publickey.c → libssh2_publickey_list_fetch() and libssh2_publickey_list_free().
Disclosure2026-06-28, credit ashdfrkl per VulnCheck advisory.
Downstream blastcurl, git (libcurl-based), Ansible (when libssh2 backend), NetMiko/Nornir, embedded firmwares (OpenWrt, BusyBox-based) — but ONLY callers that actually invoke the publickey subsystem are exposed to the primary RCE path.

Sources.

  1. VulnCheck advisory — libssh2 publickey integer overflow
  2. NVD — CVE-2026-58050
  3. PoC repo — bikini/exploitarium libssh2 publickey-list calc PoC
  4. RedPacket Security — CVE alert
  5. CIRCL Vulnerability-Lookup — CVE-2026-58050
  6. GHSA-mf77-5hj2-98w9 (libssh2 advisory mirror)
  7. RFC 4819 — SSH Public Key Subsystem
  8. libssh2 upstream — src/publickey.c
05 · The Call

Final Verdict
↓ DOWNGRADED to MEDIUM (5.4/10)

Why this verdict

  • Server-attacker model: this is a malicious-server bug. Defender exposure is gated by either (a) users initiating SSH to untrusted hosts or (b) attacker holding MITM on a TLS-less hop — most enterprise SSH goes to known-hosts-pinned bastions, collapsing the reachable population.
  • Subsystem rarity: the publickey subsystem (RFC 4819) is *not* used during normal pubkey auth — curl/git/scp/sftp do not touch it. Only key-management tooling and bespoke automation invoke libssh2_publickey_init(). Most libssh2 callers in a 10k-host fleet are immune by API surface.
  • 32-bit dependency: the primary RCE primitive requires the multiplication to wrap, which only happens on ILP32 builds. Modern server fleets are 64-bit; remaining 32-bit exposure is in embedded/IoT/router firmware, which is a real but bounded population.
  • Role multiplier — typical libssh2 consumer (curl/git client on workstation): chain fails at step 2 (subsystem not invoked). Blast radius: none.
  • Role multiplier — automation/key-management server using publickey subsystem on 64-bit Linux: primary chain fails at step 3; secondary free-chain may succeed but requires harder primitives. Blast radius: single-host RCE as the agent user. Not a fleet pivot unless the agent itself holds privileged credentials — which is the worst plausible high-value-role outcome and floors this at HIGH for *those specific hosts*. Fleet-wide, that subpopulation is well under 1%.
  • Role multiplier — embedded 32-bit network device (OpenWrt router, IoT gateway) speaking outbound SSH: primary chain succeeds if device initiates publickey-subsystem queries. Blast radius: device compromise. Real fleets rarely have these devices dialing arbitrary SSH endpoints.
  • EPSS 0.00311 + not-KEV + no observed exploitation = no urgency multiplier from external threat data.

Why not higher?

HIGH would require either active exploitation, a fleet-scale role multiplier on a canonical high-value component, or a client-attacker model. This is a client-side bug in a library where the typical caller doesn't even touch the affected API, on the increasingly rare 32-bit build target — none of those upgrade conditions hold.

Why not lower?

LOW would mean immaterial impact; arbitrary code execution in a privileged automation agent is not immaterial. The Win32 PoC demonstrably pops calc.exe, and embedded fleets still carry 32-bit libssh2. Real RCE in real (if narrow) populations earns MEDIUM, not LOW.

06 · Verification

Crowdsourced verification payload.

Run on each Linux/macOS target host as any user (no privileges required). Invoke as ./check_cve_2026_58050.sh. The script locates the libssh2 shared library, extracts its version string, checks whether the host is 32-bit, and reports VULNERABLE / PATCHED / UNKNOWN. For Windows targets, port the logic against libssh2.dll version metadata.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_cve_2026_58050.sh — libssh2 publickey-subsystem integer overflow
# Exit codes: 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN
set -u

FIXED_MAJOR=1
FIXED_MINOR=11
FIXED_PATCH=2   # first release expected to carry the fix; adjust when upstream tags

find_lib() {
  for p in /usr/lib /usr/lib64 /usr/local/lib /lib /lib64 /opt; do
    find "$p" -maxdepth 4 -name 'libssh2.so*' 2>/dev/null
  done | sort -u
}

ver_from_lib() {
  local lib="$1"
  # libssh2 embeds version via LIBSSH2_VERSION macro -> strings grep
  strings "$lib" 2>/dev/null | grep -Eo '^libssh2/[0-9]+\.[0-9]+\.[0-9]+' | head -1 | cut -d/ -f2
}

is_32bit() {
  local lib="$1"
  file "$lib" 2>/dev/null | grep -q 'ELF 32-bit'
}

verdict=UNKNOWN
found=0
while IFS= read -r lib; do
  [ -z "$lib" ] && continue
  found=1
  ver=$(ver_from_lib "$lib")
  arch32=no; is_32bit "$lib" && arch32=yes
  if [ -z "$ver" ]; then
    echo "[?] $lib — version string not found (arch32=$arch32)"
    [ "$verdict" = UNKNOWN ] && verdict=UNKNOWN
    continue
  fi
  IFS=. read -r MA MI PA <<<"$ver"
  if [ "$MA" -gt "$FIXED_MAJOR" ] || \
     { [ "$MA" -eq "$FIXED_MAJOR" ] && [ "$MI" -gt "$FIXED_MINOR" ]; } || \
     { [ "$MA" -eq "$FIXED_MAJOR" ] && [ "$MI" -eq "$FIXED_MINOR" ] && [ "$PA" -ge "$FIXED_PATCH" ]; }; then
    echo "[+] $lib v$ver (arch32=$arch32) PATCHED"
    [ "$verdict" != VULNERABLE ] && verdict=PATCHED
  else
    echo "[!] $lib v$ver (arch32=$arch32) VULNERABLE — primary RCE primitive applies on 32-bit"
    verdict=VULNERABLE
  fi
done < <(find_lib)

if [ "$found" -eq 0 ]; then
  echo "[?] libssh2 not found in standard paths — check statically linked binaries (curl, git, etc.)"
  echo UNKNOWN
  exit 2
fi

echo "$verdict"
case "$verdict" in
  PATCHED)    exit 0 ;;
  VULNERABLE) exit 1 ;;
  *)          exit 2 ;;
esac