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 defaultPermitRootLogin prohibit-password— but the official tiny-cloud images (3.18.11–3.24.3) ship a useralpinewith*and passwordlessdoas. The bypass gets you a shell;doasgets 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
- Update and restart.
apk upgrade -U -a— andrc-service sshd restart, because upgrading the package does not replace the running daemon; the oldsshdkeeps accepting connections. - 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 — checksshd -T | grep -i passwordauthenticationafterwards, and noteMatchblocks), keep your session open while you test a fresh one. This closes the hole regardless of patch state. - Ask the two shadow questions on every musl host:
Both non-empty plus password auth on means the host was exploitable.awk -F: '$2=="*"{print $1}' /etc/shadow # reachable accounts awk -F: '$2 ~ /^\$[56]\$/{print $1}' /etc/shadow # the salt source - Hunt the log line.
/var/log/messages(Alpine's default syslog target) forAccepted password fornaming an account that has no password —alpine,root, key-only service users. Any hit is a compromise indicator until proven otherwise: reviewdoasusage, new users, fresh files,authorized_keys. If a login can't be explained, treat the machine as owned. - 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.comhas 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
- sshhole.com — Özgün Kültekin's write-up (timeline, commits, interactive demo).
- Alpine Linux security advisory — conditions, fixed packages per branch, mitigation and forensics recipes.
- CVE-2026-123158 record (MITRE CNA, 8.1; CISA SSVC: PoC / total impact).
- OpenSSH fix commit
4353fdcand the 10.6p2 release notes. - Help Net Security — Previdian honeypot compromise, nine-hour exploitation timeline, IoC.
- SOCRadar analysis — indirect-root framing, cloud image ranges.
- Repro labs: sshhole-repro, sshhole-lab (both unverified third-party code — review before running).