Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20261106185455.GZ23438@brightrain.aerifal.cx>
Date: Fri, 6 Nov 2026 13:54:55 -0500
From: Rich Felker <dalias@...c.org>
To: musl@...ts.openwall.com
Subject: OpenSSH password auth bypass analysis [was: Re: Advisory for
 OpenSSH+musl users]

On Thu, Nov 05, 2026 at 10:07:13AM -0500, Rich Felker wrote:
> Versions of OpenSSH prior to 10.6p2 using musl are affected by an
> authentication bypass when UsePAM=no and PasswordAuthentication=yes.
> Anyone who cannot immediately upgrade should edit /etc/ssh/sshd_config
> to set:
> 
> PasswordAuthentication no
> 
> and restart sshd after ensuring you have working pubkey access.
> 
> Dropbear and other non-OpenSSH software using password-based logins is
> not affected. It is my position that this is not a bug in musl just an
> erroneous use by the caller. But the mechanism by which this happened
> is something we have been wanting to change for a while now, and it
> very likely will be changed in the upcoming release cycle.
> 
> Details to follow soon.

OK, some details on the mechanism here, starting with backroung and
moving step-by-step:


1. The way you validate a password against a "hash" (which is actually
a combination of a 'setting' prefix and hashed password) is comparing
the result of crypt(password,hash) against hash. Only if they compare
equal should the user be granted access.


2. Mechanically, any character or sequence of characters that do not
form a valid hash (and that do not contain : or \n) can be placed in
the password field of the shadow file (or legacy passwd file) to
ensure that it's impossible to login as that user with a password,
since for such a hash, crype(pass,hash)!=hash is always true. The
conventional "no password access" value is "*".


3. In order to avoid leaking information about which user accounts
exist and have password-based login available, OpenSSH has a clever
hack to, when the user's hashed password is a single character like
"*" that can never be matched, use a different user's password hash as
the setting/salt. Originally they used root's:

https://github.com/openssh/openssh-portable/commit/9286875a73b2de7736b5e50692739d314cd8d9dc

and later switched to searching the user database until finding a user
with a password:

https://github.com/openssh/openssh-portable/commit/dbf788b4d9d9490a5fff08a7b09888272bb10fcc

Then, after obtaining the other_user_hash, OpenSSH checks the
condition:

	crypt(pass,other_user_hash) == hash

and, despite having already determined that password logins are
disabled for this user, allows the login the proceed if the condition
is met.


4. There is a historical disagreement on how crypt() should handle
exceptional conditions. Pre-POSIX, crypt was not permitted to return a
null pointer, and a lot of software did (and some apparently still
does) assume this. POSIX does not define any error conditions except
ENOSYS (which was probably about export controls), so defining a
behavior when the operation cannot be completed with the provided
parameters is entirely up to the implementation.

Some implementations -- mainly OpenBSD, FreeBSD, and recent Fedora --
return a null pointer and report the condition in errno.

A number of implementations including NetBSD, musl, and most
glibc-based Linux distros (now using libxcrypt) return what's come to
be known as a "failure token" -- a string that guarantees

	crypt(pass,failure_token) != failure_token

for any possible password pass.

The failure token musl uses is "*", except when the input salt/setting
is "*"; in that case, it returns "x", so that:

	crypt(pass,"*") != "*"

is satisfied to prevent any password from validating.

Other implementations commonly use "*0" and "*1" for these roles.


5. Having a failure token not match any password is contingent on the
caller of crypt passing the correct hash as the settign argument to
crypt. While it's guaranteed that:

	crypt(pass,failure_token) != failure_token

it is impossible to guarantee that:

	crypt(pass,some_other_hash) != failure_token

But that is exactly what OpenSSH was testing, by passing a hash
obtained from searching for another user account with a password.


6. While many password hash algorithms always succeed as long as the
setting string is not invalid, SHA256 and SHA512 -- not the base hash
algorithms themselves, but the way they're used in multiple rounds as
password hashes -- have quadratic time in password length, and thus
require treating excessively long passwords as an error condition to
avoid DoS in any process that validates passwords. So, for an
excessively long password pass, and some_other_hash taken from a user
who has a SHA256 or SHA512 hashed password:

	crypt(pass,some_other_hash) == failure_token


7. For any choice of failure_token, if it ends up in the password
field of the shadow file, it will allow password bypass for that user,
because:

	crypt(pass,some_other_hash) == failure_token == user_hash

Since by convention (what's implemented by shadow tooling, etc.) "*"
is used for disabling password access to accounts and is also the
failure token musl uses, this condition was met.


Mitigation, fix, and hardening:

As noted in the advisory, disabling password authentication entirely
mitigates the issue. There are a number of other configurations that
are also unaffected:

In order to be affected, the target account must have "*" as its
password hash (attempting to prevent password access), there must be
at least one other account that does have a password set, and the
first account found with a password (in order of enumerating
/etc/passwd) must be using SHA256 or SHA512 type password hashes.

OpenSSH fixed the bug by hard-coding failure after doing the
timing-masking crypt call with the wrong hash, so that if the target
user's password hash is shorter than 4 character (not a valid hash;
must be a failure token) the login attempt always fails:

https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482

This should fix the issue for any system where failure_token is under
4 bytes (all real world systems).

As for hardening musl against this kind of issue, pursuing a switch to
returning an error (null+errno) has been an open issue for a long time
now, and one I'd like to assess in the next release cycle. In the
immediate, I am going to propose a patch switching to the more common
"*0"/"*1" convention for failure tokens. This does not prevent misuse,
but it's safer under the assumption that the conventional "*" is what
appears in shadow. This should be able to go into the pending release
and would be easy for distros to apply as a backport if desired.

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.