Check your website Blog
5 November 2026

sshole: ten years, one asterisk, open SSH

OpenSSH 10.6p2 shipped on 5 November with a fix for CVE-2026-123158, nicknamed sshole: on sshd built against musl libc without PAM — the default build of Alpine's openssh-server — any password between 257 and 1024 characters logs you into any account whose shadow field is *. No password knowledge required, no race, no retries: Alpine's advisory says it plainly — "one connection, works every time." Preceding post on this blog measured a kernel race at a couple of events per fifty-million attempts; this one is a one-shot. And it was not theoretical for long: Previdian reported the first exploitation attempt less than nine hours after the CVE went public, with root obtained on a live Alpine honeypot and reconnaissance (id, uname, hostname) run through non-interactive SSH sessions.

What the bypass does

The attacker needs three things they can usually learn or guess: an account whose /etc/shadow field is exactly * (key-only deploy users, LDAP/musl-nscd users, root on modern Alpine), the machine to have at least one other account with a real $5$/$6$ hash, and PasswordAuthentication yes — the OpenSSH default. Then:

ssh root@alpine-box        # password prompt: paste ~300 characters of anything

That's the whole exploit. As the researcher puts it: "yes, any password. yes. i know. yes."

How four sensible decisions compose into this

No single component here is broken in an exciting way. The bypass is an emergent property of four independently reasonable choices made by three projects over four decades:

Year Decision Where
1980s convention * as the password field of an account that must never take a password — not a hash, so nothing can match it Unix, inherited by Linux
2012 (Aug) musl's crypt() stops returning NULL on failure — programs didn't check — and returns "*" instead, "an unmatchable hash": "reportedly a better behavior" musl b3c4cc12
2012 (Aug, three weeks later) SHA-crypt implementations cap the key at 256 bytes for DoS protection — and when the cap trips, crypt() "fails", returning… "*"; the comment in crypt_sha512.c says "on error * is returned" — literally, the whole time musl 88bf5a8a
2016 Fix for CVE-2016-6210 (response-time oracle revealing which usernames exist): if the target account has no usable hash, sshd borrows another account's salt and hashes the attempt anyway to burn equal time OpenSSH sys_auth_passwd()
2022 Alpine 3.16 changes root's field from ! (which, without PAM, also killed key logins — the CVE-2019-5021 fix) to *: "still forbids password login but allows logins via ssh keys". Narrator: it did not. Alpine aports fbe926af

Compose: sshd (musl, no PAM) takes the * account, finds no salt in it, borrows the $6$ salt from some other account, and calls crypt("AAAA…300 chars", "$6$salt…"). musl rejects keys longer than 256 bytes and returns "*". sshd compares "*" against the stored "*". Match. The 2016 fix against a timing side-channel turned "instant no" into "hash it anyway" — and made the bypass deterministic.

Why ten years of nobody: PAM-based distros never call crypt() for a * account; OpenSSH's own test base is OpenBSD and glibc (glibc returns NULL, already refused); a fresh Alpine box has no $6$ hash until an admin sets a password, so the salt source often doesn't exist; cloud images set PasswordAuthentication no via cloud-init. The pieces were famously close to surfacing earlier: in 2013 a Debian dev asked glibc to adopt musl's "*" convention (glibc went with libxcrypt, which returns "*0" — one character away from the same bug), and a 2024 musl thread about busybox's passwd breaking at 257 characters discusses the same "*" at the same limit without anyone saying "sshd".

The discovery method deserves a note, because it generalises: Özgün Kültekin (Wakefield) built sshd across the whole matrix — glibc/musl × PAM/no-PAM × shadow/no-shadow — and threw ~17,600 crafted login attempts at every build, with agent swarms doing a lot of the work. Every build agreed on every input except musl-without-PAM. There is also a delicious four-day margin: a security review of OpenSSH fixed pick_salt() — the fake-salt generator — on 30 October; the bypass sat four lines below it.

Exposure

Condition Affected Not affected
libc musl: Alpine, OpenWrt, Buildroot, Yocto — and "a lot of the docker images you've pulled" that run sshd glibc (Debian/Ubuntu/Fedora/RHEL) — crypt() returns NULL
sshd build no PAM (Alpine's default openssh-server, or UsePAM no) PAM (openssh-server-pam with UsePAM yes), Dropbear, OpenBSD
config PasswordAuthentication yes (the default) key-only configs, cloud-init defaults
accounts any * shadow field + at least one $5$/$6$ hash elsewhere locked ! accounts; boxes with no password hash at all

Two exposure escalations matter operationally:

  • Indirect root is the real story. Alpine's root:* is shielded by the default PermitRootLogin prohibit-password — but the official tiny-cloud images (3.18.11–3.24.3) ship a user alpine with * and passwordless doas. The bypass gets you a shell; doas gets you root. One account with a password is enough to make every account without one reachable.
  • The exploit is trivially automatable in practice even though CISA's SSVC enrichment says "automatable: no" — the preconditions (musl, no-PAM, password auth on, * account, salt account) are configuration-side and not all visible externally, but against a known Alpine host with password auth it is a single scripted connection.

Patch, verify, hunt

  1. Update and restart. apk upgrade -U -a — and rc-service sshd restart, because upgrading the package does not replace the running daemon; the old sshd keeps accepting connections.
  2. Can't update: disable password auth. echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf (drop-in dir is only read from 3.19 up — check sshd -T | grep -i passwordauthentication afterwards, and note Match blocks), keep your session open while you test a fresh one. This closes the hole regardless of patch state.
  3. Ask the two shadow questions on every musl host:
    awk -F: '$2=="*"{print $1}' /etc/shadow          # reachable accounts
    awk -F: '$2 ~ /^\$[56]\$/{print $1}' /etc/shadow # the salt source
    
    Both non-empty plus password auth on means the host was exploitable.
  4. Hunt the log line. /var/log/messages (Alpine's default syslog target) for Accepted password for naming an account that has no password — alpine, root, key-only service users. Any hit is a compromise indicator until proven otherwise: review doas usage, new users, fresh files, authorized_keys. If a login can't be explained, treat the machine as owned.
  5. Scanner noise incoming. Alpine patched every branch 3.16→edge on the same day, but the fixes carry the branch versions (3.21 → 9.9_p2-r1, 3.24 → 10.3_p1-r2, …). Version-banner checks against "10.6p2" will misreport patched Alpine hosts; behaviour checks (sshhole.com has an interactive demo; repro labs are linked from the CVE record) are the right primitive for fleet verification.

The fix, and why it landed on OpenSSH

The upstream fix (4353fdc) is small and boring in the best way: before comparing, sshd now refuses any stored field shorter than 4 characters (MIN_PASSWORD_HASH_LEN) — "*", "!!" and friends can never compare equal to a crypt() output, whatever libc returns. It also cleans up the ownership of the shadow strings (xstrdup/freezero) along the same path. The commit message carries the interesting line: "We do not consider the root cause to be within OpenSSH but we are well equipped to avoid it." Right on both counts — musl's error convention and OpenSSH's fake-salt path are each defensible; the guard belongs where the two meet, which is sshd. Damien Miller also confirmed OpenSSH learned of the bug on 4 November, a day before release, and that the project now considers long embargoes "a dangerous practice that primarily benefits attackers" — which is why patching and scanning started the same day.

A CVSS aside, since MITRE's 8.1 uses attack complexity high: AC is doing double duty again (as in CVE-2026-98432). Here the complexity is arguably low for a targeted attack — one connection, works every time — and what actually gates the risk is configuration reachability: musl + no PAM + password auth + a * account + a salted account. That's a fleet-inventory question, not an attack-craft question.

Takeaway

This bug had no exotic primitive — no race, no parser confusion, no memory error. Four locally-rational conventions, 34 years apart, and a comparison at the seam. If you run Alpine anywhere — and if you run containers, you run Alpine images somewhere, the ones that ship sshd included — check the two shadow files above today, and treat the past days as unverified until the log line turns out not to exist. And for the tooling people: this is the second time this month where the defensible answer to "are we affected" was "check behaviour, not versions" — version strings lied about backports in CVE-2026-98432's vendor kernels, and they'll misreport Alpine's patched packages tomorrow.

Sources

← All posts