Ai Agents 7 min read

SSHole: agent swarms found a ten-year-old OpenSSH login bypass, and attackers were using it within two days

CVE-2026-123158 lets a 257-byte password log in to accounts that have no password on musl-based Linux such as Alpine. A researcher found it by having AI agents fire about 17,600 odd logins at differently built sshd binaries. OpenSSH 10.6p2 fixes it.

On November 5, OpenSSH shipped version 10.6p2 with what its release notes call “a critical security workaround” for Linux systems that use the musl C library with PAM disabled. The bug, tracked as CVE-2026-123158 and named *sshole by its finder, Özgün Kültekin of Wakefield, lets anyone log in to an account that has no password at all by sending a password of roughly 257 to 1,024 bytes. Any password of that length works. The CVE record rates it 8.1 on CVSS 3.1.

Two things make it more than another patch notice. Kültekin found it with AI agents doing differential testing rather than by reading code, and by November 9 BleepingComputer was reporting successful attacks in the wild, including one that ended with a root-level implant.

Three reasonable decisions that add up to a bypass

No single line of code is wrong here, which is how the bug survived for a decade. Kültekin’s write-up traces it to three changes made years apart:

  • August 2012, musl. musl’s crypt() was changed to return a string beginning with * on failure instead of NULL, so that careless callers would not crash on a null pointer. The 1997 Single UNIX Specification says crypt() returns NULL on failure.
  • August 29, 2012, musl. SHA-crypt support was added with a 256-byte cap on the password (KEY_MAX), as denial-of-service protection. Longer input is an error, so crypt() returns *.
  • July 2016, OpenSSH. To close a timing side channel that revealed which usernames exist (CVE-2016-6210), sshd began hashing every password attempt even when the account has no real hash, borrowing the salt from another local account that does.

Now take an account whose password field in /etc/shadow is the single character *, the conventional way to say “this account has no password but is not locked”. Send a 257-byte password. sshd borrows a $5$ or $6$ salt from another account and calls crypt(). musl refuses the oversized input and returns *. sshd compares that result with the stored field, which is also *, and the strings match.

On glibc systems the failure value is different (NULL or *0), so the comparison fails. With PAM enabled, crypt() is never called for a * account. OpenBSD and Dropbear are unaffected. That leaves a narrow but common target, and all four conditions must hold at once:

  1. sshd linked against musl and built without PAM, which is Alpine’s default openssh-server package and is common in Buildroot and Yocto images (OpenWrt only if it runs OpenSSH instead of its default Dropbear)
  2. PasswordAuthentication yes
  3. an account with * in the shadow file
  4. at least one other local account with a real SHA-256 or SHA-512 hash to borrow a salt from

The accounts that carry * are exactly the ones administrators consider safe from password guessing: root on Alpine since 3.16, key-only deploy users, and the default alpine user on Alpine’s cloud images, which per Dor Josef’s analysis has passwordless doas on tiny-cloud images 3.18.11 through 3.24.3. On those images the bypass is one command away from root.

Ownership of the bug is disputed. OpenSSH’s notes say “we consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there.” The musl maintainer, according to the same analyses, calls it incorrect use of crypt() by the caller and expects to change the behaviour in a future release anyway. Third-party analyses say the 10.6p2 workaround rejects any match where the stored hash is shorter than four characters; the release notes do not describe the mechanism.

How the agents found it

Kültekin did not audit auth-passwd.c. He built sshd in every configuration he could think of (glibc and musl, with and without PAM, with and without shadow passwords) and had agents throw about 17,600 unusual login attempts at all of them, then compared the answers. In his words: “every build agreed on every login. except musl without pam. that one said yes to things it shouldn’t.”

That is differential testing, an old technique. What the agents changed is the cost of the tedious parts: producing the build matrix, inventing thousands of strange inputs, and noticing one disagreement in the results. This class of bug is nearly invisible to code review because each component behaves as its own authors intended. musl does not test against sshd, and OpenSSH is developed on OpenBSD and tested mostly against glibc. Only running the combinations exposes the seam.

The write-up is light on method. Kültekin says only that he “had agent swarms. a lot of them,” and names no models, harness, cost or time spent, so the result cannot be benchmarked against efforts like OpenAI’s GPT-6 Cyber run on the Linux kernel. One detail is telling, though. On October 30, an OpenSSH commit fixed fake-salt generation for bcrypt hashes in the same function. The bypass sat four lines below that change and went unnoticed in the review, four days before Kültekin found it.

From patch to root implant in about 40 hours

Proof-of-concept code appeared about four hours after the fix on November 5, and the reports since then describe real but limited exploitation:

ObserverWhat they saw
PrevidianFirst attempt less than nine hours after publication; 20 attempts from 13 IP addresses in nine countries; root obtained on an Alpine honeypot
watchTowrUse by botnets and cryptomining operations on November 6
GreyNoiseFirst successful chain on November 7 at 00:46 UTC, about 40 hours after the fix; 16 successful chains from six source IPs
Defused”A small amount of actors” over the weekend, not widespread

After logging in, GreyNoise’s attackers ran system and container reconnaissance, queried cloud metadata endpoints, tried to read password hashes and root SSH keys, and edited authorized_keys for persistence. One reached root and installed a Python command-and-control implant called TAC Agent, whose comments and log strings are in Chinese. Its server was unreachable and never issued a task.

Treat the bigger numbers in circulation with care. GreyNoise also counted 27.5 million port-22 scans and 20.1 million attempted SSH sessions, but it does not attribute those to SSHole, although some vendor write-ups present them that way. As of the reports read for this article, none said the CVE had been added to CISA’s Known Exploited Vulnerabilities catalog.

What to do on Alpine and embedded images

  • Upgrade and restart: apk upgrade openssh, then rc-service sshd restart. The upgrade alone does not replace the running daemon. Alpine backported the fix to every branch from 3.16 up, including releases that are out of support.
  • Rebuild and redeploy container images that run sshd; a patched host does not fix an old image.
  • If you cannot patch today, set PasswordAuthentication no (for example in /etc/ssh/sshd_config.d/00-no-passwords.conf) and restart, after confirming key login works. This removes one of the four preconditions.
  • Check exposure with sshd -T | grep -Ei '^(usepam|passwordauthentication) ' and list candidate accounts with awk -F: '$2=="*" {print $1}' /etc/shadow.
  • Hunt backwards to November 5 for Accepted password for <user> log lines where that user’s shadow field is *. Such a login should be impossible. If you find one, look for new authorized_keys entries, unfamiliar startup services and TAC Agent files, and rotate keys and credentials.

The patch window is shrinking for maintainers too

The 10.6p2 notes include an unusual admission. OpenSSH says it is receiving “a large number of security bug reports, many of which are findings from AI models or made with AI assistance,” and that bugs found by AI tools are being rediscovered independently by other researchers before a fix ships. Its response is to release more often instead of batching fixes.

That changes the arithmetic for everyone downstream. If the same tooling lets a second party find the same bug within days, a private report buys little time, and here public exploit code arrived the same afternoon as the patch. Kültekin’s method is also directly reusable: any pair of independently maintained components with an error-handling contract between them (a libc and a daemon, a parser and its caller) can be put through the same build matrix. Expect more bugs of this shape, in software that has been reviewed many times, and plan for patch windows measured in hours.

Get Insanely Good at AI

Get Insanely Good at AI

The book for developers who want to understand how AI actually works. LLMs, prompt engineering, RAG, AI agents, and production systems.

Keep Reading