|
|
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.