DEV Community

Cover image for sshole (CVE-2026-123158): when '*' in /etc/shadow became a working password on musl
Stanley A.
Stanley A.

Posted on Originally published at wardenbit.com

sshole (CVE-2026-123158): when '*' in /etc/shadow became a working password on musl

If you run Alpine, OpenWrt, Buildroot, Yocto, or any container image built on musl without PAM, go and check your OpenSSH version before you read the rest. Fixed in 10.6p2 (2026-11-05). It is in CISA KEV with a 2026-11-12 federal due date, and someone logged into a honeypot as root less than nine hours after the CVE went public.

For everyone else, the interesting part is why it works, because the mechanism is a genuinely instructive collision.

The collision

Two failure tokens, one byte:

  • 1980, 3BSD: /etc/passwd marks an account that must never accept a password by putting * in the hash field. It is still the convention in /etc/shadow today. Nothing can hash to *.
  • 2012, musl commit b3c4cc12: crypt() stops returning NULL on failure and returns "*" instead, because callers that skip the null check were segfaulting. Twenty days later, sha256/sha512crypt adds a 256-byte input cap (a DoS guard — sha512crypt cost scales with input) and returns the same "*" when it refuses.

Both decisions are defensible. Both picked the one string the rest of the system already uses to mean the opposite thing.

In between, 2016, CVE-2016-6210: OpenSSH closed a username-enumeration timing leak by making sshd hash even for accounts that have no usable hash. When the stored field is too short to contain a salt, sshd calls pick_salt(), borrows a real salt from another account, hashes anyway, and caches the borrowed salt so subsequent attempts are cheap.

Now the exploit, as reproduced by Medcrypt on a stock Yocto image:

Attempt 1 — any password for a *-locked account. sshd reads the shadow entry through a pointer into musl's static buffer (it never copies the string out). pick_salt() does the full walk, borrows a salt, and OpenSSH scrubs its working buffer with explicit_bzero() — which, because sshd is still aliasing that buffer, blanks the * under its own feet. The comparison runs against "". Denied. Salt cache is now warm.

Attempt 2 — same connection, 300-byte password. pick_salt() sees the cached salt and returns immediately, which means it skips the scrub. The pointer still reads *. musl refuses the over-length input and returns "*". strcmp("*", "*") == 0. Accepted, uid 0 if the account was root.

Two attempts because the first has to disable the mechanism that would otherwise wipe your match. Alpine's advisory summarizes the same thing as "one connection, works every time, needs no retries" — the difference is the cache-priming step, which Medcrypt documents and the advisory skips. Practically: one ordinary SSH session, no retry loop.

Affected: five conditions, all of them

  1. musl libc (ldd /usr/sbin/sshd, or just ls -l /lib/ld-musl*).
  2. OpenSSH < 10.6p2.
  3. No PAM in sshd's auth path — Alpine's openssh-server qualifies. openssh-server-pam is clear with UsePAM yes and affected with UsePAM no.
  4. PasswordAuthentication yes (the default).
  5. An account with * in the shadow field plus another local account with a $5$/$6$ hash. One real password on the box makes every locked service account reachable.

Not affected: glibc (libxcrypt's failure token is *0, one character away from disaster), anything going through PAM, Dropbear, OpenBSD, containers not running sshd, accounts locked with !, and cloud images configured by cloud-init (it writes PasswordAuthentication no).

Alpine's official AWS/Azure tiny-cloud images 3.18.11 through 3.24.3 ship an alpine user with a * field and passwordless doas. Safe on first boot; root for an attacker the moment any $6$ hash lands on the machine.

Patch, then actually restart

apk upgrade -U -a
rc-service sshd restart          # the package upgrade does NOT restart the daemon
sshd -T | grep -i passwordauthentication
Enter fullscreen mode Exit fullscreen mode

Fixed openssh packages by branch: edge 10.6_p2-r0, 3.24 10.3_p1-r2, 3.23 10.2_p1-r1, 3.22 10.0_p1-r11, 3.21 9.9_p2-r1, 3.20 9.7_p1-r6, 3.19 9.6_p1-r3, 3.18 9.3_p2-r4, 3.17 9.1_p1-r7, 3.16 9.0_p1-r6. Point releases 3.21.10, 3.22.8, 3.23.8 and 3.24.4 carry them.

On a musl distro that is not Alpine, the upstream commit 4353fdc43 touches auth-passwd.c, openbsd-compat/port-uw.c and openbsd-compat/xcrypt.c; Medcrypt reports it backports cleanly to 7.3p1 and later. Check for the commit, not the version string — distro backports routinely keep the old version number, so a 9.9p2 in the package version says nothing about whether the patch is in.

Config-only mitigations, in order of preference:

  • PasswordAuthentication no — closes it entirely on key-only hosts. Confirm your key login works in a second terminal before restarting sshd, or you will lock yourself out.
  • MaxAuthTries 1 — Medcrypt's stopgap, and the only option that keeps password auth working, because the attack needs the second attempt. MaxAuthTries 2 is still exploitable. Alpine's advisory does not mention it at all; tourniquet, not a fix.

Detection

# Alpine
grep 'Accepted password for' /var/log/messages | grep -E 'root|alpine'
# what a successful bypass looks like: a password login for an account with no password
awk -F: '($2=="*"){print $1}' /etc/shadow      # your reachable account list
Enter fullscreen mode Exit fullscreen mode

Then audit authorized_keys additions, recent users, and doas/sudo use on anything internet-facing. You are looking for a password login to an account that has no password — which on this bug is normal-looking once it succeeds.

Why this got found fast

Kültekin built twelve sshd configurations and fired roughly 17,600 unusual login attempts at them; only the musl-without-PAM build accepted what it should have refused. He credits part of the throughput to AI assistance. OpenSSH's own 10.6p2 notes now state the project will make more frequent releases rather than batching fixes, because AI-reported bugs are being independently rediscovered by others — an argument for shipping small fixes often rather than holding them for a release train.

KEV entry, for the record: dateAdded 2026-11-09, dueDate 2026-11-12, cwes [CWE-394], knownRansomwareCampaignUse Unknown, forensicTriage Yes. The deadline binds FCEB agencies; forensicTriage: Yes is the bit worth reading — CISA expects defenders to establish whether the host was already compromised, which is the question nobody can answer after the logs roll.

CVSS 3.1 8.1, AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H. The AC:H is honest: the attack complexity lives in the account configuration, not in the exploit.


Full write-up with the owner-facing version of this, including the three questions to send your hosting provider: wardenbit.com

Top comments (0)