⏱️ Reading time: 10 min
An SSH server on Alpine Linux can let someone into an account marked as locked if the password you send exceeds 256 bytes. There’s no secret to guess: it’s enough for that account’s field in /etc/shadow to be a plain asterisk and for the system to use musl libc without PAM.
📑 En este artículo
The finding, published on sshhole.com, points to a musl libc vulnerability that has been hidden for fourteen years. Since 2012, musl’s crypt() function has returned the same character used to mark locked accounts when it fails, and until now nobody had connected that to sshd.
TL;DR
- On Alpine with musl and sshd without PAM, a password longer than 256 bytes opens any locked account in /etc/shadow.
- The flaw dates back to 2012: musl’s crypt() returns ‘*’ on failure, the same symbol used to mark locked accounts since 1980.
- Musl has limited sha512crypt to 256 bytes since August 29, 2012, and once that limit is exceeded, crypt() returns that same ‘*’.
- The finding emerged from testing 17,600 logins against six sshd builds; only musl without PAM accepted entries it should have rejected.
- The flaw went unnoticed for 10 years: OpenSSH is tested on OpenBSD and glibc, and most distros use PAM, which avoids the bug.
What musl is
Musl is the minimalist standard C library (libc) used by Alpine Linux and many Docker images, designed to produce small, static binaries. It implements the POSIX functions a C program needs, including crypt(), which sshd calls to compare the entered password against the hash stored on the system.
What happened
The test was simple in design and massive in scale: compile sshd six different ways (with glibc or musl, with or without PAM, with or without /etc/shadow) and run roughly 17,600 login attempts against it with unusual hashes, broken accounts, and overly long passwords. According to sshhole.com, the author used swarms of automated agents to generate and run those attempts in parallel.
Five of the six combinations agreed on every result. The pattern was always the same: an account with ‘*’ in the password field of /etc/shadow, the standard marker for a locked account since 1980, let in anyone who sent a password longer than 256 bytes, but only in the musl build without PAM.
The finding was published in November 2026, just four days after an OpenSSH security review, dated October 30, 2026, fixed a neighboring function (pick_salt()) without touching the real bug, which lived four lines below in the same file.
How the musl libc vulnerability arose
The locked-account marker was born in 1980, when Bill Joy added it in 3BSD for the daemon account, with a curious detail: the real name he wrote down for that user was “the devil himself”. An asterisk isn’t a valid hash, so no password can ever match it, which is why for 46 years it worked as a reliable lock.
In the nineties, the hash moved to /etc/shadow with the format $6$salt$hash (sha512crypt). sshd takes the entered password, hashes it with the same stored salt, and compares the result against that field; if they match, the login succeeds. The 1997 Single UNIX Specification established that if crypt() can’t do its job, it must return NULL. Many programs never checked for that case and ended up comparing against NULL, which caused them to crash.
Musl solved that in August 2012 with commit b3c4cc12: instead of NULL, crypt() returns a character that no real password can ever produce. That character is ‘*’, the same symbol that has marked locked accounts since 1980.
Twenty days later, commit 88bf5a8a added sha512crypt to musl and set a 256-byte limit on it (the KEY_MAX constant), because sha512crypt gets slower the longer the input is, and a huge password could tie up the server for a long while. Past that limit, crypt() fails and, because of the change made twenty days earlier, returns ‘*’.
In 2016 came CVE-2016-6210: asking sshd about a user that doesn’t exist responded faster than asking about a real one, because there was no hash to compute, and by timing that difference an attacker could enumerate valid accounts. The OpenSSH patch made sshd, when given an invalid user, grab the salt from some other account and hash the password anyway, just to burn the same amount of time; the commit message said “for invalid users”, but it ended up applying to real accounts with ‘*’ in the field too.
In 2019, Alpine ran into CVE-2019-5021: its official Docker images shipped without a root password. The first fix locked the account with ‘!’, but without PAM that symbol also broke login via SSH key, which is how almost every cloud image is accessed. In 2022, Alpine 3.16 went back to using ‘*’ to lock root (commit fbe926af), which restored key-based access. Ten years after b3c4cc12, Alpine’s root account once again depended on that same asterisk.
Some came close to noticing. In 2013, a Debian developer asked glibc to copy musl’s ‘*’ on the libc-alpha list; six years later, Debian migrated to libxcrypt, which uses ‘*0’ (a different character), and closed the request there. In December 2024, a thread on the musl mailing list discussed a busybox passwd bug with 257-character passwords, the same limit and the same ‘*’, but nobody mentioned sshd.
Technical details: the mechanism behind the bug
The mechanism is a representation collision, not a cryptographic flaw. sshd, when not using PAM, calls crypt() directly with the password and the shadow field, then compares the result against that same field, treating both as plain text strings. If the stored field is already ‘*’ and crypt() also returns ‘*’ because the input is too long, the string comparison returns true, even though neither side is a real hash.
char *computed = crypt(password, shadow_field);
if (strcmp(computed, shadow_field) == 0) {
// access granted
}
If password is longer than 256 bytes, crypt() returns “*”. If shadow_field is already “*” because the account is locked, strcmp(“*”, “*”) returns 0 and the condition holds without any real hash behind either side.
sequenceDiagram
participant C as SSH Client
participant S as sshd without PAM
participant M as musl crypt
C->>S: sends a 300-byte password
S->>M: requests the crypt of the password
M-->>S: responds with the asterisk character
Note over S: the shadow asterisk matches the crypt asterisk
S-->>C: access granted
The PAM path never goes through this. PAM intercepts authentication before it reaches crypt(), and when the account’s field is already ‘*’ it doesn’t even try to compute a hash; that’s why the collision only shows up in builds without PAM, which are common in minimal Alpine containers.
The table below summarizes which libc and PAM combination turned out to be vulnerable in the tests described on sshhole.com.
| sshd configuration | Uses PAM? | Result with a password longer than 256 bytes |
|---|---|---|
| glibc with PAM | Yes | Rejects the login; PAM never calls crypt() for a ‘*’ field |
| glibc without PAM | No | Rejects the login; glibc’s crypt() doesn’t return ‘*’ on failure |
| musl with PAM | Yes | Rejects the login, for the same reason as glibc with PAM |
| musl without PAM | No | Accepts the login into accounts locked with ‘*’ |
Impact and analysis
The musl libc vulnerability has a limited scope, since it only affects accounts marked as locked and sshd builds without PAM, not normal logins with a real password. Even so, Alpine is the base for a huge number of Docker images, and many of them run without PAM by design, to stay lightweight.
In practice, the risk depends on which accounts have that asterisk and whether sshd lets them attempt a login at all. Many service accounts use a /sbin/nologin shell that blocks access regardless, and root usually has PermitRootLogin set to no. The most sensitive case is an Alpine container with PAM disabled where someone re-enabled password login for an account meant to stay locked.
⚠️ Heads up: the bug doesn’t require guessing any real password: it’s enough to send any string longer than 256 bytes against an account with ‘*’ in /etc/shadow.
What’s next
Meanwhile, the musl libc vulnerability remains documented but without a confirmed official patch, since neither musl nor OpenSSH has yet published a commit that closes it. OpenSSH’s review on October 30, 2026 fixed pick_salt() without touching the bug, which according to sshhole.com sits just four lines below in the same file.
There’s also no CVE identifier assigned to this specific finding so far. Alpine system administrators can mitigate it by checking whether PAM is active, confirming that service accounts use a no-login shell, and checking the PermitRootLogin setting in their sshd configuration.
Try it yourself: check the password field of your service accounts in /etc/shadow and confirm whether your sshd runs with PAM before trusting that an asterisk protects you.
Frequently Asked Questions
Which distributions use musl libc and could be affected?
Alpine Linux is the most widespread distribution using musl as its default libc, which is why it dominates Docker container base images; Void Linux and some embedded systems also use it. The bug only triggers in sshd builds without PAM.
Is Alpine Linux with PAM also exposed?
No, according to sshhole.com’s tests: PAM never calls crypt() when the account’s field is already ‘*’, so that authentication path never produces the collision.
Is there already a CVE for this musl bug?
None has been publicly assigned as of this writing. There are related CVEs tied to the same asterisk in Alpine, such as OpenSSH’s CVE-2016-6210 and Docker’s passwordless root CVE-2019-5021.
Which accounts typically have ‘*’ in /etc/shadow?
Traditionally, system accounts that shouldn’t have interactive login, such as daemon, bin, or mail; in Alpine 3.16 onward, root as well, when it’s locked.
How does this relate to OpenSSH’s CVE-2016-6210?
That 2016 CVE already forced sshd to call crypt() even for users without a real hash, to prevent response time from revealing whether an account existed. That same call is the one that, on musl without PAM, ends up comparing two asterisks.
Has musl already released a patch for this musl libc vulnerability?
There’s no public confirmation of a commit that fixes it. OpenSSH’s security review on October 30, 2026 fixed pick_salt() without touching the bug, which sits four lines below in the same file.
References
- sshhole.com: original article with the finding and an interactive demo on Debian and Alpine.
- git.musl-libc.org: 2012 commit that makes crypt() return ‘*’ instead of NULL.
- git.musl-libc.org: commit that adds sha512crypt and the 256-byte limit.
- lists.mindrot.org: 2016 OpenSSH patch for CVE-2016-6210.
- gitlab.alpinelinux.org: Alpine 3.16 commit that reverts to using ‘*’ for root.
- pubs.opengroup.org: 1997 Single UNIX Specification on crypt().
- lists.mindrot.org: OpenSSH security review from October 30, 2026.
📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day. @programacion
Featured image: Foto de Kaffeebart en Unsplash
Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.
Leave a comment
0 Comments