L
o
a
d
i
n
g
.
.
.

security

SSHole: when a failed hash and a locked password are the same string

By Michele Zonca

#security

#openssh

#linux

11 November 2026

7 minutes to read

November 11, 2026

On 5 November OpenSSH released 10.6p2 with a single security fix in sshd: a password authentication bypass on some Linux systems built without PAM. It is tracked as CVE-2026-123158 and has been called SSHole. I have not reproduced it. What follows is the release note, the root cause analysis published by PentestIT and the reports it collects, so the details are their reading of the code, not mine.

The one-byte collision

In /etc/shadow, an account whose password field is * has no usable password. It cannot log in with one, and it is how many system and cloud-image users are created.

musl’s crypt() returns "*" when it fails. sshd, in the non-PAM path (sys_auth_passwd() in auth-passwd.c), hashes the submitted password and compares the result with the stored field using strcmp(). If hashing fails on a locked account, the comparison is strcmp("*", "*"), and it succeeds.

The interesting part is how to make hashing fail. According to the analysis:

  1. sshd accepts passwords up to 1024 bytes.
  2. A stored field of * is too short to contain a salt, so sshd’s xcrypt() calls pick_salt(), which borrows the salt of the first $-style hash it finds in the password database. This was added in 2016 as the fix for CVE-2016-6210, a timing difference that allowed user enumeration.
  3. If the borrowed salt is $5$ or $6$, musl uses its SHA-crypt code, which refuses keys longer than 256 bytes and returns "*".

So a password between 257 and 1024 bytes, sent to an account whose field is *, on a host where some other account has a SHA-crypt hash, is accepted. The analysis also notes that the bypass needs a second attempt on the same connection, because of a cached salt and a pointer into libc’s static shadow buffer, and that the default MaxAuthTries of 6 leaves plenty of room.

Why it is narrow

Every condition has to hold, and most systems miss at least one:

  • musl, not glibc. glibc returns NULL when crypt() fails, and libxcrypt returns "*0" or "*1". Neither equals *. Debian, Ubuntu and RHEL are reported as not affected, and Red Hat lists all its products as not affected.
  • No PAM. With UsePAM yes, sshd never reaches sys_auth_passwd().
  • PasswordAuthentication yes, which is the upstream default.
  • A locked account stored as exactly *. A field of ! or !$6$... never matches.
  • Another account with a $5$ or $6$ hash. If the borrowed hash is bcrypt, musl truncates at 72 bytes instead of failing.
  • Not root by default. PermitRootLogin defaults to prohibit-password, which forces password authentication to fail for uid 0.

The stock Alpine openssh-server package is the obvious case: musl, no PAM. The analysis points out that Alpine’s official cloud images ship an alpine user with * and passwordless doas. They are safe as shipped, and become exploitable as soon as someone sets a password on any other account. That last detail is the one I find uncomfortable, because it turns a routine administrative action into the trigger.

Accounts with /sbin/nologin still count. A shell is not needed if AllowTcpForwarding is enabled: the account can be used as a tunnel into the network behind the host.

Exploitation

The timeline in the analysis: CVE published on 5 November, first exploitation attempt seen by GreyNoise the same day at 18:34 UTC, first successful login on GreyNoise’s own sensors on 7 November. CISA added it to the KEV catalog on 9 November. Sources disagree about how far beyond honeypots this has gone, and I have not found a named victim. The gap between publication and the first attempt was a few hours. Once the conditions are written down, the exploit is a long string and a retry, so there was little to slow it down.

Whose bug is it

The OpenSSH release note says the root cause is outside sshd, but that sshd can be hardened against it. I think both statements are true, and the case is a good example of how bugs live at interfaces.

musl’s choice to return "*" on failure is a documented convention, used because a string that can never be a valid hash cannot match a real one. That reasoning holds as long as the caller compares against real hashes. /etc/shadow uses the same string for the opposite purpose: it is not a hash, it is a marker meaning “never matches”. Each convention is reasonable on its own. Put next to each other through a strcmp(), the two “never matches” values match each other.

The pick_salt() step makes it worse in a way nobody would have predicted in 2016. A fix for a timing leak made the hash of one account depend on the hash type of another account. That is why adding a password to one user can open a different one.

The fix in commit 4353fdc does not change musl. It makes sshd reject any stored field shorter than 4 characters before comparing, so *, !, !! and *0 can never match, and makes shadow_pw() return a copy instead of a pointer into libc’s buffer. In other words, it stops trusting that the error value and the sentinel are different.

What I take from it

  • On Alpine and other musl systems without PAM: upgrade to 10.6p2 or the distribution backport, then restart sshd. Upgrading the package does not replace the running daemon. readlink /proc/<pid>/exe showing (deleted) means the old binary is still running.
  • If a restart is not possible right now, PasswordAuthentication no closes the path. On a server that only uses keys this should have been the setting anyway.
  • To check for past compromise, look for Accepted password for <user> in the auth log for any account whose shadow field is *. That line should never appear, so any hit is worth treating as a compromise.
  • More generally: when comparing a computed value with a stored one, an error value that has the same type as a valid result is a trap. Checking the return for failure explicitly, instead of relying on the error string never matching, would have avoided this.

Sources: OpenSSH 10.6p2 release notes; PentestIT, “sshole (CVE-2026-123158): OpenSSH musl Password Bypass Root Cause, Detection and Mitigation”.