DevOps 4 min read 742 words

sshhole: logging into Alpine over SSH with any password longer than 256 bytes

ES
sshhole: logging into Alpine over SSH with any password longer than 256 bytes

Ozgun Kultekin, from Wakefield, has published sshhole, a flaw in OpenSSH on Linux systems that use musl as their C library instead of glibc. On those systems, under certain conditions, sshd accepts any password longer than 256 bytes for accounts that, in theory, don’t accept a password at all. Root on Alpine among them.

What I liked most about the write-up is that there’s no single culprit. It’s the sum of four reasonable decisions made over 46 years, each in a different project, which together open the door.

Who is affected

All of these have to be true at once:

  • sshd built against musl and without PAM.
  • Password login enabled, which is the default.
  • An account whose password field is *: key-only users, LDAP users or root itself on Alpine.
  • Another account on the same machine with a real password.

That points to Alpine, OpenWrt, Buildroot, Yocto and many Docker images based on Alpine. It doesn’t affect glibc distributions (Debian, Ubuntu, Fedora), setups using PAM, Dropbear or OpenBSD.

The most serious case the author points out is Alpine’s official AWS and Azure images: they ship an alpine user with passwordless doas, so getting into that account is the same as being root.

How we got here

The story starts in 1980, when BSD used * in /etc/passwd for accounts like daemon. The idea is simple: sshd (and any program that checks passwords) hashes what the user types with the stored salt and compares it with the stored field. No valid hash is *, so nothing can match.

YearWhat happens
1980BSD uses * to mark accounts that never accept a password
2012musl makes crypt() return "*" on failure instead of NULL as POSIX says, so programs that don’t check for NULL don’t crash
2012musl adds SHA-256 and SHA-512 crypt with a 256-byte password limit to prevent denial-of-service attacks; above the limit it fails and returns "*"
2016To fix user enumeration (CVE-2016-6210), OpenSSH starts hashing with another real account’s salt when the user has no usable one, so every response takes the same time
2022Alpine 3.16 changes root’s password from ! to * so root can log in with a key without PAM

Putting the pieces together, as the author describes it: someone tries to log in as root with a 300-byte password. Since root’s field is *, sshd borrows the SHA-512 salt of another account and calls crypt(). musl sees the password is over 256 bytes, fails and returns "*". sshd compares "*" with the stored field, which is also "*", and the password is correct.

flowchart LR
    A["300-byte password"] --> B["sshd uses another account's salt"]
    B --> C["musl crypt fails · returns *"]
    C --> D["Compares * with root's field · *"]
    D --> E["Access granted"]

The glibc detail is almost an anecdote: in 2013 someone asked Debian for glibc to copy musl’s behaviour. Debian ended up moving to libxcrypt, which returns "*0" on failure. One character of difference, and that’s why glibc distributions aren’t affected.

What to do

The fix is to upgrade OpenSSH to 10.6p2 or later. On Alpine:

apk update && apk upgrade openssh
rc-service sshd restart

If you can’t upgrade yet, disable password login, which on an exposed server should be disabled anyway:

echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/00-no-passwords.conf
rc-service sshd restart

To check a machine, the author has published a tool at github.com/oz9un/sshole. If its output shows a line like root@your-box, an account without a password has accepted a password login, and that machine is exposed.

In containers there’s usually no sshd, and then there’s nothing to do. But if you have Alpine-based images that do include it (for debugging, for maintenance access, for some legacy service), it’s worth reviewing and rebuilding them.

My takeaway

A week ago I wrote about Alpine’s incident with GitLab and Ruby, where upgrading wasn’t enough because of how two pieces interacted. This case has something similar: musl was right not to return NULL, the 256-byte limit makes sense, OpenSSH’s borrowed salt fixed a real problem and Alpine changed root to * for a good reason. None of those decisions is a mistake on its own, and the flaw only appears when all four coincide.

It’s a good reminder that the value a function returns when it fails is also part of its contract. And that PasswordAuthentication no is still one of the most cost-effective lines you can put in a server’s configuration.

Source: sshhole, Ozgun Kultekin, Wakefield.