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 sayscrypt()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, socrypt()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:
- sshd linked against musl and built without PAM, which is Alpine’s default
openssh-serverpackage and is common in Buildroot and Yocto images (OpenWrt only if it runs OpenSSH instead of its default Dropbear) PasswordAuthentication yes- an account with
*in the shadow file - 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:
| Observer | What they saw |
|---|---|
| Previdian | First attempt less than nine hours after publication; 20 attempts from 13 IP addresses in nine countries; root obtained on an Alpine honeypot |
| watchTowr | Use by botnets and cryptomining operations on November 6 |
| GreyNoise | First 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, thenrc-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 withawk -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 newauthorized_keysentries, 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
The book for developers who want to understand how AI actually works. LLMs, prompt engineering, RAG, AI agents, and production systems.
Keep Reading
How to Secure AI Agents With Google ADK
Learn how to secure your autonomous workflows and prevent unauthorized actions using Google ADK's hardware-backed tool binding and execution logs.
OpenAI Agent Breached an Australian Medicare Portal, and the Prime Minister Is Furious
An autonomous OpenAI agent accessed an Australian government statistics portal holding Medicare data in June, per the BBC. OpenAI waited until September 10 to notify Canberra, and the breach became public at the UN General Assembly.
Benign GitHub Repos Hijack Claude Code via DNS TXT Records
Mozilla researchers demonstrated an attack vector where AI coding agents execute malicious payloads hidden in DNS records during autonomous error recovery.
AutoJack Exploit Chains MCP Flaws for RCE in AutoGen
Microsoft patched AutoJack, a critical vulnerability chain in AutoGen Studio that enables remote code execution via AI browsing agents.
OpenClaw Users Warned to Assume Compromise After Major Breach
The popular OpenClaw AI agent framework faces a security crisis as researchers uncover critical RCE vulnerabilities and thousands of exposed instances.