A server that should have refused every password let one through. Not clever. Just long.
The bug is called SSHole, tracked as CVE-2026-123158, and it affects OpenSSH builds that use musl libc with PAM turned off. That combination is normal on Alpine Linux, and on plenty of routers, containers and embedded images.
We reproduced it on a local Alpine 3.22 box running an unpatched OpenSSH 10.6p1 build. A 257-byte password was rejected on the first attempt and accepted on the second, for an account whose password field was a single *. The fix is OpenSSH 10.6p2. Updating the package is not enough on its own: sshd has to be restarted.
Key facts
| Field | Value |
|---|---|
| CVE | CVE-2026-123158 |
| Nickname | SSHole |
| Severity | 8.1 High, CWE-394 |
| Affected | OpenSSH before 10.6p2, musl libc, PAM disabled |
| Fixed | OpenSSH 10.6p2, 5 November 2026 |
| Password range | 257 to 1024 bytes |
| Exploited | Yes, CISA KEV since 9 November 2026 |
| Found by | Ozgun Kultekin, Wakefield |
- On musl systems without PAM, an account locked with
*can accept a password of 257 bytes or more. - The first attempt in a connection fails. A second attempt in the same connection succeeds. That is not a typo.
- Debian, Ubuntu, Fedora, RHEL, Arch, OpenBSD, FreeBSD and macOS are not affected by this path.
- Alpine has fixed packages for 3.16 through edge. Restart sshd after upgrading.
- If you cannot patch today, set
PasswordAuthentication noand reload sshd.
1. What is SSHole?
SSHole is an authentication bypass in OpenSSH on musl libc when sshd is built without PAM. It lets an account whose shadow entry is * accept any password between 257 and 1024 bytes. The * field is meant to make password login impossible, which is why key-only and service accounts use it.
Two old behaviours have to meet for the bug to exist. musl's crypt() returns the string * when it cannot hash a password, and OpenSSH compares the result with the stored field. If the stored field is also *, the comparison succeeds. OpenSSH 10.6p2 adds a minimum length check for the stored field, copies the shadow entry before it can be overwritten, and rejects passwords over 1024 bytes. The first of those three closes the hole.
🔒 Check Your Network Security
Find out if your IP is blacklisted, leaking via WebRTC, or acting as a proxy.
2. How did a wrong password get in?
The short answer: musl's crypt() refuses to hash anything over 256 bytes and returns * instead of a hash. sshd compares that * with the * in /etc/shadow and treats the match as a successful login. The longer answer involves a borrowed salt, a shared buffer and the reason the second attempt is the one that works.
Here is the path inside sshd:
pw_password = "*" account has no password
salt = NULL "*" is too short to use as a salt
salt = pick_salt() sshd borrows another account's "$6$..." salt
hash = crypt("A" * 257, salt) musl returns "*" for keys over 256 bytes
strcmp(hash, pw_password) == 0 "*" equals "*"
return 1
pick_salt() exists because of CVE-2016-6210, a timing leak that let an attacker work out which usernames existed. The fix was to hash a fake password with a real salt so that invalid users took the same time as valid ones. In 2016 that was a sensible trade. In 2026 it is the part that makes the collision possible. Ozgun Kultekin's writeup walks the same history from the 1980 * to the 2021 musl cap.
Our test box
We installed Alpine 3.22 in WSL, built OpenSSH 10.6p1 from source with --without-pam, and created two users. alice had * in /etc/shadow. bob had a normal $6$ hash. sshd listened on 127.0.0.1 only.
| Password bytes | Attempt 1, cold connection | Attempt 2, same connection |
|---|---|---|
| 255 | denied | denied |
| 256 | denied | denied |
| 257 | denied | accepted |
| 300 | denied | accepted |
| 1024 | denied | accepted |
| 1025 | denied | denied |
The control account behaved normally. bob rejected 257 bytes on both attempts and accepted his real password.
Why the first attempt fails is the interesting part. On a cold connection, pick_salt() walks /etc/shadow and calls getspnam() for each user. musl returns one shared static buffer, so the pointer sshd saved for alice is overwritten by the last entry in the scan, then zeroed. The comparison then runs against an empty string, not *. The attempt fails even though both sides should be *.
pick_salt() stores its salt in a static buffer. On the next attempt in the same process it returns early, skips the scan, and leaves alice's field alone. Now the comparison really is * against *, and sshd lets the login through.
That matches the researcher's own note that a * account "needs a wrong try first". We measured it rather than taking it on faith.
We did not run a glibc control on this machine. The claim that glibc systems are unaffected comes from the advisory and from the fact that musl's crypt() is the component returning *.
3. Who is affected, and was it exploited?
Your server is vulnerable only when five conditions are true at the same time: musl libc, no PAM, password auth on, an account locked with *, and another account with a real $5$ or $6$ hash. Miss one and this path does not work.
| Condition | What to look for |
|---|---|
| musl libc | Alpine, OpenWrt with OpenSSH, Buildroot, Yocto, postmarketOS |
| No PAM | sshd built without PAM, or UsePAM no |
| Password auth on | PasswordAuthentication yes, the default |
A * account | key-only users, LDAP users, root on modern Alpine |
| A real hash nearby | another account with $5$ or $6$ in shadow |
Who is not affected
Debian, Ubuntu, Fedora, RHEL, Arch, OpenBSD, FreeBSD and macOS use glibc, PAM, or a different password path. They are not affected by this bug. Dropbear is a different SSH server and is not affected. Servers with PasswordAuthentication no are not reachable through this path.
A ! in the password field is not the same as *. ! blocks password logins and also blocks key logins on some builds, which is why Alpine moved root to * in 3.16.
Docker is a common question. The base image itself does not run sshd, so it is not exposed. If you install and run openssh-server inside an Alpine container with password auth on, the container has the same problem as any other Alpine host.
Was it exploited?
Yes. CISA added CVE-2026-123158 to the Known Exploited Vulnerabilities catalog on 9 November 2026, with a remediation deadline of 12 November. GreyNoise recorded the first in-the-wild exploitation attempt on 7 November at 00:46 UTC, two days after the advisory.
The numbers are ugly. GreyNoise counted 72.5 million scans against port 22, five possible exploitation attempts, and 42 confirmed post-exploitation actions. Previdian reported root on an Alpine honeypot in under nine hours, across 30 attempts from 19 IPs. SANS saw 713 scans on 6 November, all using long all-A passwords.
4. How do I check my server?
Run four commands. The first tells you whether the libc is musl. The second tells you whether PAM is in use. The third lists accounts locked with *. The fourth lists accounts that have a real hash, which is the salt source the bug needs.
ldd --version 2>&1 | head -1
sshd -T | grep -i '^usepam'
awk -F: '$2=="*"{print $1}' /etc/shadow
awk -F: '$2 ~ /^\$[56]\$/{print $1}' /etc/shadow
If the first line says musl, usepam says no, and both account lists are non-empty, you are in the affected group. That does not mean you were attacked. It means the door was unlocked.
To look for abuse, search for accepted password logins by accounts that should never have one:
grep "Accepted password" /var/log/messages
journalctl -u sshd | grep "Accepted password"
On Alpine, sshd logs to /var/log/messages through syslog. A * account with an accepted password entry is the signal the researcher's detection script looks for. Treat any hit as a compromise, not a curiosity.
Save the output of these commands before you patch. If something goes wrong later, you will want to know what the box looked like before the change.
5. How do I fix it?
Upgrade OpenSSH to 10.6p2 or to the fixed build for your Alpine branch, then restart sshd. The restart is the step people skip. apk upgrade replaces the binary, but the running daemon keeps the old code in memory until it is restarted.
apk upgrade openssh
rc-service sshd restart
Fixed Alpine packages:
| Branch | Fixed openssh |
|---|---|
| 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 |
Backports exist down to 3.16. Check your branch before assuming apk upgrade is enough, and confirm with apk info -v openssh.
If you cannot patch right now, turn password authentication off. Key logins keep working, and the vulnerable comparison is never reached.
mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sshd -t
rc-service sshd restart
Test the config before reloading. A typo in a drop-in file can lock you out just as effectively as an attacker.
Restarting sshd can interrupt new connections for a moment. Do it in a maintenance window, not in the middle of someone's deploy.
If you want the longer version of how fast an unpatched box becomes someone else's problem, our PHP2Shell breakdown covers that pattern.
6. Frequently Asked Questions
How long does the password have to be?
257 bytes or more. 256 is hashed normally and does not match. 1025 and above are rejected by the 1024-byte cap that was already in the code. The useful range is 257 to 1024 bytes.
Does updating alone fix it?
No. You have to restart sshd. Upgrading the package leaves the old process running until it is restarted or the machine reboots.
Is Debian or Ubuntu affected?
No. They use glibc and PAM, and their sshd does not take this path. The same goes for Fedora, RHEL, Arch, OpenBSD, FreeBSD and macOS.
Is Alpine in Docker affected?
The base image is not, because it does not run sshd. A container running openssh-server with password auth on is affected exactly like a normal Alpine host.
Is a ! account safe?
! is not the trigger. The bypass needs *. That said, ! can block key logins on some builds, which is why Alpine uses * for root.
Why does the first attempt fail?
Because pick_salt() scans /etc/shadow and overwrites the shared buffer holding the target account's password field. The next attempt uses the cached salt and skips the scan, so the field survives and the comparison succeeds.
How do I know if I was attacked?
Search sshd logs for Accepted password for any account whose shadow field is *. If you find one, assume the host is compromised and rebuild it rather than cleaning it.
Why did this go unnoticed for so long?
The pieces are old and individually harmless. * dates to 1980, musl's * return dates to 2012, and the borrowed salt dates to 2016. Nothing tested the combination until this year, and most Linux distributions go through PAM, which never reaches this code for a * account.
7. Final Thoughts
This is a strange bug because every component did what it was designed to do. * means no password. musl returns a safe placeholder instead of NULL. OpenSSH hashes a fake password to hide a timing leak. Put them together and the placeholder matches the lock.
The fix is small and the upgrade is boring, which is the best outcome a security story can have. Do the upgrade, restart the daemon, and check the logs for a * account that somehow logged in with a password. If you find one, the password was not the problem. The server was.
Sources
Found this guide helpful?
More security explainers, tested fixes, and plain-English breakdowns.
Read more on byteGeekery →