Skip to content

Add the sshole detector for the OpenSSH password bypass on musl - #6

Merged
devGregA merged 8 commits into
mainfrom
devgrega/sshole
Nov 7, 2026
Merged

devGregA merged 8 commits into
mainfrom
devgrega/sshole

Conversation

@devGregA

@devGregA devGregA commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds the sshole detector for *sshole, the OpenSSH password authentication bypass on musl libc, fixed in OpenSSH 10.6p2 (commit 4353fdc43) and in Alpine's openssh packages of 2026-11-05.

On musl, sshd from 7.3p1 to 10.6p1 compares an account's password field of * with the * that crypt returns when it fails. The donor salt sshd borrows decides which passwords make crypt fail: sha256 or sha512 above 256 bytes, bcrypt or a bad rounds= value for every password.

What it checks

  • fs and image: each sshd's C library, release and apk package (Alpine 3.16 to 3.24 and edge backports), PAM linkage and UsePAM, sshd_config with Include and Match (PasswordAuthentication, AuthenticationMethods, PermitRootLogin, Allow/Deny lists, ChrootDirectory), and the accounts read the way musl reads them. exposed (critical, or high when a Match decides) names the accounts; sshd covers unfixed builds that today's accounts or settings do not expose, fixed builds and PAM-checked ones.
  • host: banner and offered login methods only. No password is ever sent. engine.ErrNotApplicable lets a host scan skip a detector whose service is not on that address.
  • repo and fix: apk add of an OpenSSH server on Alpine gets the fixed release floor for its branch (read from the base image through the guarded transport when the tag does not name it). Password fields set to * (usermod, chpasswd -e, sed on /etc/shadow, Ansible and cloud-init) become *NP*. Every edit is rescanned in memory first.

Other changes

  • netguard moves from detectors/php2shell/internal to internal, unchanged (noted in SNAPSHOT).
  • image.OpenRegistry takes a transport and refuses local archives.
  • CI fuzzes the new parsers.

Testing

  • go vet ./..., go test -race ./..., fuzzing of Inspect, accounts, config and the source-tree scanner.
  • parity/sshole.py: 8/8 fixture images (unfixed, fixed, password logins off, UsePAM on, root with PermitRootLogin, Match on address, Alpine 3.24, Debian).
  • Real images: linuxserver/openssh-server and atmoz/sftp report fixed; gitea, forgejo 11, jenkins/ssh-agent:alpine and panubo/sshd report unfixed but not exposed as shipped.
  • fix on a sample tree, then a rebuild of the edited Dockerfile, gives an image the detector reports fixed.

The Sensei port is in DefectDojo-Inc/defectdojo-scan-and-fix (branch devgrega/sshole-sensei).

Added after review: coverage gaps closed

  • Startup: the image's command (sshd -o, -f, through tini, sh -c and wrappers) and its startup scripts (the command's script, s6-overlay, cont-init.d, entrypoint.d, /etc/s6). A script that turns password logins on, sets a * field or sets passwords with chpasswd makes an unfixed server exposed (high). UsePAM yes at start counts only for PAM builds; disagreeing or test-mode (sshd -t) command lines are not applied. The engine passes the image's Entrypoint and Cmd (TargetContext.Command).
  • OpenWrt: opkg database, with the 23.05, 24.10 and 25.12 backports of 2026-11-05.
  • Source trees: ARG/ENV package lists (fixed where the value is written; unset variables are notes), Ansible apk tasks (fix: state: latest), Buildroot, OpenWrt and Yocto musl builds (manual steps with the fixed release or the patch), and alpine:3.N end-of-life bases moved to 3.16 as a labelled upgrade.
  • Parity: 10/10 fixture images, including an entrypoint that turns password logins on and sshd -o in the image command.

Added in the second gap round

  • scan repo pulls the images a tree references (guarded transport, at most 20) and judges them with the command a Compose service (entrypoint, command) or Kubernetes container (command, args) sets.
  • fix: an Alpine-suffixed end-of-life base (python:3.9-alpine3.14) moves to the same tag on the nearest fixed Alpine release once that image is confirmed to be that release.
  • fix: Buildroot and Yocto builds get upstream commit 4353fdc43 as a patch scoped to OpenSSH 8.9p1 to 10.6p1 (Buildroot versioned patch directories; a version-specific Yocto bbappend). The patch applies cleanly to every release in that range; patched 8.9p1, 9.9p2 and 10.6p1 build on musl. Fixes can add files.
  • Not done: recognising the fix inside a stripped binary. It leaves no string or symbol, and a code pattern that could clear a vulnerable build is worse than the current false positive on hand-patched builds.

🤖 Generated with Claude Code

devGregA and others added 8 commits November 5, 2026 16:08
Finds OpenSSH servers exposed to *sshole (fixed in OpenSSH 10.6p2 and in
Alpine's openssh packages of 2026-11-05) and plans verified fixes in
source trees.

- fs and image: judges each sshd by its C library, its release and the
  apk database (Alpine 3.16 to 3.24 and edge backports), then follows
  musl's shadow lookup, sshd's salt choice and musl's crypt failures, and
  sshd_config with Include and Match, to report which accounts a crafted
  password reaches. PAM-checked builds are inactive.
- host: reads the banner and the offered login methods; never sends a
  password. engine.ErrNotApplicable lets a host scan skip a detector whose
  service is not on that address.
- repo and fix: apk installs of an OpenSSH server on Alpine (base release
  read from the image when the tag does not name it) get the fixed floor,
  and password fields set to "*" become "*NP*". Each edit is rescanned in
  memory before it is written.
- parity/sshole.py: eight fixture images, all matching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fuse symlinked reads

Also adds repo.Find and Options.Files for callers that rescan one finding
or a pull request's changed files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves netguard from detectors/php2shell/internal to internal, unchanged,
and adds image.OpenRegistry, which takes a transport and refuses local
archives. Base images a source tree names are now checked against the
address policy on every connection and redirect, not before them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urce trees

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ee gaps

- Images: sshd -o and -f from the container command and startup scripts
  (entrypoints, s6, cont-init.d, entrypoint.d) change the config judged; a
  startup script that turns password logins on, sets a "*" field or sets
  passwords with chpasswd makes an unfixed server exposed (high). The
  engine passes the image's Entrypoint and Cmd (TargetContext.Command).
- OpenWrt's opkg database and its 23.05, 24.10 and 25.12 fixes.
- Source trees: apk package lists in ARG and ENV (edited where the value is
  written), unresolved variables as notes, Ansible apk tasks (fix: state:
  latest), Buildroot, OpenWrt and Yocto configurations that build OpenSSH
  on musl, and an end-of-life alpine:3.N base moved to 3.16 as a labelled
  upgrade.
- parity/sshole.py: ten images, all matching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… flags; ignore disagreeing or test-mode sshd command lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, retag and patch fixes

- scan repo pulls the images a tree references through the guarded
  transport (engine Options.Guarded, at most 20) and judges them with the
  Compose entrypoint/command or Kubernetes command/args set for them
  (engine Options.Override; image.Opened keeps Entrypoint and Cmd).
- fix: an end-of-life Alpine-suffixed base (python:3.9-alpine3.14) moves to
  the same tag on the nearest fixed Alpine release once that image is
  confirmed to be that release; Buildroot and Yocto builds get upstream
  commit 4353fdc43 as a patch scoped to OpenSSH 8.9p1 to 10.6p1 (applies
  cleanly to every release in that range; patched 8.9p1, 9.9p2 and 10.6p1
  build on musl). Fixes can now add files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devGregA
devGregA merged commit 04b0b4c into main Nov 7, 2026
2 checks passed
@devGregA
devGregA deleted the devgrega/sshole branch November 7, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant