CVSS 8.1 · authentication bypass · Alpine / musl · exploited in the wild (honeypot)

The OpenSSH "sshhole" Bypass (CVE-2026-123158): Are You Exposed, and What Actually Stops It

6 November 2026 · 12 min read

On November 5 OpenSSH shipped 10.6p2 for a flaw that lets an attacker log in over SSH to some Linux systems without the password. It only bites a specific build — sshd compiled or run without PAM, which in practice means Alpine Linux and other musl-libc systems — but that covers a large share of container base images, wherever they run sshd. The mechanism has been explained everywhere since it got the name "sshhole." What is harder to find, and what this post is about, is the part a defender actually needs: whether your host is in scope, which of the fixes people reach for do nothing, and how to tell whether you were already probed. We reproduced the bug against Alpine's own packages to pin down each answer.

The Vulnerability

CVE-2026-123158 (CWE-394) is an authentication bypass in how sshd checks a password when it is built without PAM. Without PAM, sshd hashes the attempt with the C library's crypt() and compares the result against the stored field in /etc/shadow. When an account's stored field is too short to hold a salt — most importantly a bare *, which is what a key-only account carries — sshd borrows a salt from the first account in the file that has a $-prefixed hash, and the bypass fires when that hash is a $5$ or $6$ (SHA-crypt) one. On musl's crypt(), a password longer than 256 bytes is rejected and the function returns the sentinel string "*". sshd then compares "*" against the stored "*", they match, and the login is accepted. glibc and libxcrypt return different values on that path (a null result, or "*0"/"*1"), which is why they are not affected; that distinction is our reading of the code, not something the advisory spells out.

OpenSSH fixed it in 10.6p2 (commit 4353fdc4), and states it considers the root cause to be outside sshd. The salt-borrowing path dates back years; the musl behaviour is what turns it into a bypass.

  • CVSS: 8.1 High (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H), scored by the MITRE CNA; NVD has published no score of its own (status Received). CWE-394.
  • AFFECTED: sshd built without PAM, or an openssh-server-pam build running UsePAM no, on musl libc — principally Alpine Linux. glibc systems are not affected.
  • NEEDS: password auth on, an account whose shadow field is *, and some account with a $5$/$6$ hash. All four conditions below.
  • FIXED: OpenSSH 10.6p2. Alpine backported across every branch 3.16–edge on November 5 (edge 10.6_p2-r0, 3.24 10.3_p1-r2). Restart sshd after upgrading.
  • EXPLOITED: not in CISA KEV. Previdian reported 8 exploitation attempts from a single IP on November 6 and a confirmed bypass to root on one of its own honeypots (the attacker ran id, uname, hostname); no production compromises reported.
  • ADVISORY: Alpine Linux security advisory, November 5, 2026

Am I exposed?

Affected
Linux hosts running sshd without PAM (Alpine's openssh-server, or openssh-server-pam with UsePAM no) where password auth is on and at least one account's shadow field is *. That includes any container image that runs sshd on Alpine, and musl systems generally. Stock Alpine ships root:*, but root is reachable only where PermitRootLogin yes is set (common in sshd Docker images); the default prohibit-password keeps root out, so the risk is the other * accounts.
Not you
glibc distributions (Debian, Ubuntu, RHEL, and their containers). Hosts with PasswordAuthentication no. Alpine running openssh-server-pam with UsePAM yes. Hosts not running sshd at all. Being on a fixed package only counts once sshd has been restarted.
Check
On the host: apk list -I 'openssh-server*' (shows the installed package and revision) against Alpine's fixed table, then sshd -T | grep -Ei 'usepam|passwordauthentication'. The four-condition checklist below turns this into a yes/no.
Fix
Upgrade (apk upgrade -U -a) and restart sshd. If you cannot upgrade now, set PasswordAuthentication no, or run the PAM build with UsePAM yes. MaxAuthTries 1 also blocks it.

Exposed only if all four are true

Break any one and the bypass cannot fire. The first two are the ones you change.

1
sshd runs without PAM — the plain openssh-server package, or openssh-server-pam with UsePAM no
check: sshd -T | grep usepam — no line at all means a non-PAM build (exposed); usepam no is the PAM build left off
remove it: install the PAM build and set UsePAM yes
2
password login is on (PasswordAuthentication yes, the default)
check: sshd -T | grep passwordauthentication
remove it: set PasswordAuthentication no
3
an account's password field is exactly * (key-only users, directory users; also root, but only reachable with PermitRootLogin yes)
check: awk -F: '$2=="*"' /etc/shadow
remove it: — (normal for key-only accounts; do not "fix" by setting a password)
4
some account has a $5$ or $6$ hash (the salt the bug borrows)
check: awk -F: '$2~/^\$[56]\$/' /etc/shadow
remove it: — (ordinary; not something to remove)

What We Reproduced

We built four Alpine 3.24 containers from Alpine's own published packages, each with the same accounts: a victim whose shadow field is *, an ordinary account carrying a $6$ hash, and a locked account with !. A client then sent login attempts over one SSH connection. The results line up exactly with the advisory, and they separate the real fixes from the ones that only look like fixes.

reproduction · real output
# openssh-server 10.3_p1-r1 (no PAM)
#  victim: *  authsource: $6$9EIXf...
#  passwordauthentication yes
$ python3 attempt.py   # over 1 conn
wrong pw x2      denied then denied
300-char x1      denied
300-char x2      denied then ACCEPTED
   uid=1000(victim) gid=1000(victim)
different x2     denied then ACCEPTED
256-char x2      denied then denied
1025-char x2     denied then denied
locked(!) x2     denied then denied

# 10.3_p1-r2 (fixed)
300-char x2      denied then denied

# PAM package vs the real fix
pam, UsePAM no   denied then ACCEPTED
pam, UsePAM yes  denied then denied

# verification
20 fresh conns   0 of 20 accepted
MaxAuthTries 1   denied, conn dropped
nologin shell    ACCEPTED, no shell
   "This account is not available"

Three things in that run matter more than the headline. The PAM package on its own changed nothing — case three behaves exactly like the plain vulnerable build, because its default is UsePAM no; only turning PAM on (case four) closed the hole. The bypass took two attempts on one connection, never one: across 20 fresh single-attempt connections, none succeeded. And an account set to /sbin/nologin still authenticated, it just produced no shell — so "accepted" and "got a shell" are not the same claim, and the real impact depends on what the * account can do.

The two-attempt detail is the one worth understanding, because it is why one mitigation works and another does not. On the first attempt, sshd's salt-borrowing walks the password database, and that walk overwrites the libc buffer still holding the target account's * field, so the comparison runs against the wrong bytes and fails. The borrowed salt is then cached, so the second attempt skips the walk, compares against the intact *, and succeeds. Alpine's advisory says the attack "takes one connection, works every time, and needs no retries"; we read "no retries" as no retry loop across connections, not a single packet, since within one connection it reliably takes two. The fix duplicates that buffer so it can no longer be clobbered. This is our analysis of the code and our reproduction, consistent across four independent reviews; the advisory does not walk through it.

Looks safe · still exposed

What feels like protectionWhy it is not
You installed openssh-server-pamIts default is UsePAM no, so you are still exposed until you set UsePAM yes.
You ran apk upgradeThe fixed package is on disk, but the running sshd is the old one until you rc-service sshd restart.
The SSH banner reads OpenSSH_10.3The banner never shows Alpine's -rX patch level. Vulnerable and fixed look identical from the network.
You set MaxAuthTries 6 or left the defaultThe bypass needs only two attempts. Only MaxAuthTries 1 cuts the connection before the second.

Were You Already Hit?

A mitigation guide that cannot tell you whether you were already caught is only half of one. The salt-borrowing path has existed in sshd for years, so a determined attacker could have used this well before it had a name — though the public spread only started in November. Two things are worth checking.

The log signature is specific: a successful password login for an account that has no usable password — a key-only user, or anything whose shadow field is *. On a non-PAM Alpine host that is a contradiction, and it is the clearest single indicator. Look in /var/log/messages (or your central logs) for Accepted password for lines naming such accounts:

were you already hit?
# accounts that should never accept a password
awk -F: '$2=="*"{print $1}' /etc/shadow > /tmp/keyonly
# password logins recorded for any of them
grep "Accepted password for" /var/log/messages \
  | grep -F -w -f /tmp/keyonly

Two caveats. Default log retention on a small Alpine host will not reach back far, so an empty result is weak evidence, not an all-clear — this is a "coverage limited" answer, not a "nothing happened" one. And confirm the daemon you are inspecting is the one actually running: an upgraded package with an un-restarted sshd is still the vulnerable binary. readlink /proc/$(cat /run/sshd.pid 2>/dev/null)/exe shows the running binary — a (deleted) suffix means the package was upgraded but sshd was never restarted, so the vulnerable binary is still live. The daemon's start time should be after the upgrade.

Investigation Workflow

The patch level is not visible from the network: the SSH banner shows the upstream version (OpenSSH_10.3) and never Alpine's -rX package revision, so a vulnerable and a fixed Alpine host look identical from outside. A host that truthfully advertises 10.6p2 or later is fixed; anything below that, on Alpine, tells you nothing about the backport. So this is a host-inventory job first, with network recon narrowing where to look.

1. Find the SSH surface

Use RECON's port scan to find hosts answering on 22 (and non-standard SSH ports) from an untrusted vantage point. An Alpine-based container that exposes sshd to the internet is the shape most at risk here — appliances, sftp containers, CI runners.

2. Confirm password auth is even offered

If a host only accepts keys, condition two is already false. From outside, ssh -o PreferredAuthentications=none -o PubkeyAuthentication=no user@host reports the methods the server offers; a reply listing password means password auth is on. This narrows the fleet but cannot confirm the bug — the remaining conditions live in /etc/shadow and the build, which only on-host checks reveal.

3. Resolve the build and version on the host

apk list -I 'openssh-server*' gives the installed package and revision; compare against Alpine's fixed table. A line showing openssh-server (not the -pam build) is itself condition one. sshd -T | grep -Ei 'usepam|passwordauthentication' resolves the two conditions you control. Then confirm sshd was restarted after the upgrade.

Remediation

  1. Upgrade and restart. apk upgrade -U -a, then rc-service sshd restart. The upgrade alone does not restart the daemon, and an un-restarted sshd stays vulnerable. Rebuild and redeploy affected container images.
  2. If you cannot upgrade now, turn off password auth. PasswordAuthentication no (Alpine's advisory gives a drop-in that also covers Match blocks). Make sure key login works first. This removes condition two and is the most reliable stopgap.
  3. Or run PAM properly. openssh-server-pam with UsePAM yes — the package on its own, at the default UsePAM no, does not protect you.
  4. As a last resort, MaxAuthTries 1. It cuts the connection after the first failed attempt, before the bypass's second attempt can land. We confirmed it blocks the attack, but it is blunt: a rejected public-key offer counts as a failed attempt, so any client that offers an unaccepted key before its password is cut off too. Prefer the options above.
  5. Hunt the log signature. A password login accepted for a * account is the smoking gun; see above. Treat an empty result as limited coverage, not proof.

Triage Notes

  • Remote recon proves: that sshd is reachable and whether it offers password authentication. Neither the libc, the build, nor the patch level is visible from the banner.
  • It cannot prove: that a host is vulnerable — the deciding conditions (non-PAM build, a * account, a $5$/$6$ hash, and whether sshd was restarted) are all on-host.
  • Evidence to request: apk list -I 'openssh-server*'; sshd -T for usepam/passwordauthentication; the sshd start time versus the upgrade time; awk over /etc/shadow for * accounts and $5$/$6$ hashes; auth logs for password logins on * accounts.
  • Escalation threshold: a non-PAM (or UsePAM no) Alpine host with password auth on, a * account and a $5$/$6$ hash, reachable from an untrusted network. The impact then depends on the * account's shell and privileges.
  • Finding statement: "The host runs sshd without effective PAM on musl libc, with password authentication enabled and at least one account whose shadow field is * alongside a $5$/$6$ hash, matching CVE-2026-123158. An unauthenticated attacker can bypass password authentication for that account. Upgrade OpenSSH to the fixed Alpine package and restart sshd; until then, set PasswordAuthentication no."

Follow @hellorecon for new CVE investigations.

Sources

By Vladimir Slavin · Founder, RECON · [email protected]

Related guides and tools