Repository navigation
Add vuln-sshhole check for OpenSSH on musl without PAM - #53
Merged
Merged
Conversation
OpenSSH sshd built against musl libc without PAM accepts any password of 257 to 1024 bytes for a valid user whose shadow field is "*" (https://sshhole.com/, no CVE yet). pick_salt() borrows the $5$/$6$ salt of another account, musl's SHA-256/512 crypt returns "*" for keys over 256 bytes, and that matches the "*" field. The first password attempt on a connection always fails, because pick_salt() runs getspnam() for every account and overwrites the static buffer that sys_auth_passwd() still points at. So the check sends a 256-byte control password first, then 257 and 1024 bytes on the same connection, and reports a finding only when the control is rejected and a trigger is accepted. A second connection records whether a single attempt works; it does not on 9.7p1 or 10.3p1, so no reusable session is claimed. analyze now writes vuln-<id>.jsonl for every reported vuln. testdata/sshhole builds controls on Alpine and Debian. Alpine's own openssh packages carry the upstream fix as pw.patch since 2026-11-05, so the vulnerable control is built from the upstream tarball. Verified: 10.3p1/musl 1.2.6 and 9.7p1/musl 1.2.5 flag root and alpine only; the no-hash, PAM, patched-package and Debian controls are clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseTargets appended every address of an expanded CIDR a second time, so each host was scanned twice (and with a user@ prefix, once with and once without the user). Against password checks that doubles the failed logins per host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenSSH answers the attempt that reaches MaxAuthTries with a "Too many authentication failures" disconnect instead of USERAUTH_FAILURE. The check counted that attempt as never tried, so servers with MaxAuthTries 3 (Cisco FMC, NX-OS, ISE in the lab) were reported as inconclusive. The reason now also says how many attempts were rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renames vuln-sshhole to vuln-musl-crypt-star, following the mechanism-named vuln checks, and combines the two implementations of the same probe. Kept from the sshhole check: every password goes out on one connection (256-byte control, then 257 and 1024 bytes), since the first attempt on a connection always fails on musl; a MaxAuthTries disconnect counts as a rejection; PerSourcePenalties refusals are logged instead of reported; the finding is reported on an accepted trigger password with a proof command that shows the libc and OS. Added from vuln-musl-crypt-star: the check skips OpenSSH 10.6p2 and later, the release that rejects short password hashes, so no failed logins are spent on fixed servers; an exec that ends without an exit status still counts as having run; and an in-process SSH server covers the check end to end, now modelling the first-attempt failure and MaxAuthTries 2. The cached session credential now wraps the trigger password in ssh.RetryableAuthMethod, so a fresh connection resends it once and interact and the session checks work on real musl hosts. The check confirms that on a second connection before claiming a reusable session and records the attempt that logged in. analyze writes each vuln record as <id>.jsonl, since vuln IDs already carry the vuln- prefix.
docs/targets holds one directory per check with a README that explains how to build the target, run the check against it, and confirm the finding and its absence on the controls. The Alpine and Debian images, setup script and run.sh move there from testdata/sshhole, with the container names following the check name, and the README covers the five variants, the version gate, the interactive session and the unit tests.
The check, its files, the docs/targets directory and the container names all use the advisory name, so one name refers to the bug everywhere.
hdm
approved these changes
Nov 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
vuln-sshholefor the password bypass at https://sshhole.com/ (no CVE yet). OpenSSH sshd built against musl without PAM accepts any 257 to 1024 byte password for a valid user whose shadow field is*, when another account has a$5$or$6$hash. OpenSSH 10.6p2 (2026-11-05, "avoid password authn bypass") rejects short stored hashes, and Alpine backported that aspw.patchto 3.20, 3.22 and 3.24.The first password attempt on a connection always fails. musl's
getspnam()returns a pointer into a static buffer, andpick_salt()calls it for every account and zeroes the hash it copies, sosys_auth_passwd()compares against an empty string.pick_salt()caches the salt, so later attempts on the same connection compare against*. A probe that sends one long password per connection never finds the bug.The check runs per user on OpenSSH servers older than 10.6p2 that offer
password; fixed releases are skipped without a connection, and distribution backports with an older banner are probed but rejected cleanly. On one connection it sends a 256-byte random control password, then 257 bytes, then 1024 bytes if 257 fails, and reports only when the control is rejected and a longer password is accepted. AToo many authentication failuresdisconnect atMaxAuthTriescounts as a rejected attempt. The proof records the output ofid; uname -a; ls /lib/ld-musl-*; head -3 /etc/os-release.A second connection then authenticates with the accepted password alone, wrapped in
ssh.RetryableAuthMethodso it is resent once. When that opens a session the password is cached asSessionAuth, so-I firstand-I alland the session checks work on real musl hosts, and the proof records which attempt logged in.analyzenow writes<id>.jsonlfor each reported vuln.docs/targets/sshholebuilds Docker controls: unpatched 10.3p1 from the tarball, the same with no$6$account, a PAM build, Alpine's patched package, and Debian with glibc.docs/targets/README.mddescribes the layout for future targets.It also fixes
parseTargets, which appended every address of an expanded CIDR a second time, so each host was scanned twice and the password checks doubled their failed logins.TestParseTargetsCIDRcovers it.Testing: the original check flagged root and alpine only on 10.3p1/musl 1.2.6 and 9.7p1/musl 1.2.5 with the four negative controls clean. The merged check adds an in-process SSH server that models the first-attempt failure and covers the finding,
MaxAuthTries 2, the version gate, the password-any skip, and reuse of the cached credential.go test ./...passes.