SSHHole (CVE-2026-123158), also called asterisk-hole, lets an unauthenticated attacker log in as root, or any account locked with *, on OpenSSH builds that use musl libc. That setup is common in the Yocto, Buildroot and Alpine images many connected medical devices run on. No password, no prior access. Here's the chain of events that makes it work, why it takes exactly two tries, and why the most carefully hardened embedded images are the most exposed.
If you lock down a Linux device, one of the first things you do is take passwords away from accounts that shouldn't have them. Root, service users, anything meant for keys only. The standard way to do that is to drop a single * into the shadow file where the password hash would go. No valid hash is one character long, so the account can never be logged into with a password. It's been the convention since 3BSD in 1980.
On a specific but very common class of device, that * is now the login.
CVE-2026-123158 is an authentication bypass in OpenSSH as built against musl libc. On an affected device, an attacker with nothing but network access can authenticate as any account locked with *, including root. It needs two password attempts on a single connection and a password that's a little too long. That's the entire exploit. Our researcher Dan Beard reproduced it independently, including on a stock Yocto image, which is to say: not a lab curiosity, a thing that ships.
The bug is a good one. Not because it's hard to trigger (it isn't), but because nobody did anything obviously wrong to create it. It's the product of two reasonable, defensive design decisions, made years apart in two different codebases, that happen to collide on a single character.
At a glance. Affected: musl + OpenSSH older than 10.6p2 + no PAM + password auth on + a *-locked account, all at once. Impact: unauthenticated login as any locked account, root included. Fix: OpenSSH 10.6p2 (commit 4353fdc43), or PasswordAuthentication no / MaxAuthTries 1 as a stopgap.
Which medical devices are affected
We spend our time on medical devices, and this vulnerability reads like it was built to find them.
Think about what a hardened embedded Linux image for a fielded device tends to look like. Built with Yocto, Buildroot, or Alpine, so very often musl rather than glibc. Stripped down, so frequently no PAM. Service and maintenance accounts locked with * because that's what the hardening checklist says. And password authentication left on, because a tech in the field needs a way in when the keys don't work. That's not a contrived worst case. That's a normal, responsibly built device. It's four of the five conditions by default, and the fifth is “have you shipped OpenSSH 10.6p2 yet.”
Dan reproduced this on a stock Yocto image. No exotic configuration. The kind of base a lot of devices are standing on right now, in hospitals, sitting on networks, with a root account that's supposed to be unreachable.
If you build devices: run your SBOMs against the five conditions today. musl, OpenSSH version, PAM state. Where they line up, treat it as a passwordless root login and move accordingly, including working out whether fielded units put you in scope under FDA postmarket expectations or EU MDR. An OpenSSH that can no longer be updated on a fielded image also belongs in your SBOM's end-of-support data.
If you run devices: our five-minute field test is below. Your biomed team can run it on a unit you own, and it comes with the questions worth sending your vendors. You don't need source or special tooling, just SSH reach and a spare unit.
The setup: two systems, one symbol
musl, 2012. musl made two changes to its crypt() that matter here.
First, crypt() started returning the one-character string * on failure, instead of NULL. The reasoning was defensive: plenty of callers don't null-check, and a NULL return crashes them. * can't collide with a real hash (far too short to be valid), so it's a safe “this failed” marker that won't blow up naive code.
Second, musl added a length guard. Hashing attacker-controlled input with sha512crypt is real CPU work, and unbounded input is a cheap denial of service. So musl refuses to hash anything past its length cap and, again, returns *.
Both calls are sensible on their own. The problem is the character. * was already taken. It's the shadow file's “account locked, no password” marker, and has been for four decades. musl reached for a value that “can't match a real hash” and landed on the one value the rest of the system already uses to mean “never let this account authenticate.”
OpenSSH, 2016. Separately, OpenSSH was closing a timing side channel (CVE-2016-6210). Before the fix, you could tell real accounts from fake ones by how long sshd took to reject them: a real account ran a full hash, a missing one short-circuited. The fix makes sshd do the same amount of work either way.
Part of that is how it handles an account whose shadow field is too short to be a hash, like a *-locked account. sshd won't use * as a salt (it's nonsense as a salt), but it also won't just bail, because bailing is the timing leak. Instead it calls pick_salt(), which walks the password database and borrows a real salt from a valid account, so the hash runs and the timing looks normal.
To keep that cheap, the borrowed salt gets cached. The first connection pays for the walk; after that it's a static buffer sitting ready. Hold that thought. The cache is the whole game.
One more detail, and it's the load-bearing one: when sshd grabs the account's shadow entry, it holds a pointer straight into musl's internal buffer. It doesn't copy the string out. So sshd's idea of “this account's password field” is really “whatever is in musl's static buffer right now.” As long as nothing else touches that buffer, fine. Something else touches that buffer.
Why it takes two tries
Everything happens inside one connection, in sshd's per-connection child. The state is isolated per connection, so this isn't a race between logins. It's a sequence inside a single one.
Attempt one: warming the cache
The attacker sends any old password for a *-locked user.
sshd looks up the account. musl fills its static shadow buffer, and sshd keeps a pointer to it. That pointer now reads *.
One character is too short to pass sshd's “is this long enough to be a salt” check, so there's no salt yet, and sshd calls pick_salt() to go borrow one. The cache is cold, so pick_salt() does the full walk: iterate accounts, find one with a real hash, copy its salt.
Here's where it bites itself. After borrowing the salt, OpenSSH scrubs the buffer it was working in with explicit_bzero(), standard hygiene for not leaving password material lying in memory. But because sshd never copied the shadow entry out, the buffer it's zeroing is the same buffer sshd's own pointer is still aimed at. The * gets wiped to an empty string under sshd's feet.
So when the comparison finally runs, it's comparing the freshly computed hash against "", not against *. Mismatch. Access denied.
The first attempt fails, and it fails for the exact reason the bug exists: sshd is reading through a dangling alias into libc's scratch memory. The sloppiness that makes the vulnerability possible is, on attempt one, the thing that saves you. It also leaves the salt cache warm.
Attempt two: the bypass
Same connection. This time the password is 300 bytes.
sshd looks the account up again. musl refills its buffer. The pointer reads * once more. Same short-field check fails, so sshd calls pick_salt() again. But the cache is warm now. pick_salt() sees a salt already sitting in its static buffer and returns immediately. It skips the account walk, which means it skips the buffer scrub. Nothing zeroes the * this time.
Now crypt() runs on the 300-byte password. That trips musl's length guard. musl won't hash it, and returns its failure sentinel: *.
And so the final comparison is:
strcmp("*", "*") == 0 // accepted
The * on the left is musl's “password too long, I refused to hash it.” The * on the right is the shadow file's “this account is locked.” Two completely different meanings, same byte, and strcmp doesn't care about meaning. sshd reads a match and hands over a shell. If the account was root, that's uid 0.
Two attempts, because you need the first one to warm the cache and suppress the scrub that would otherwise wipe your match. The bug has to get out of its own way before it works.
Is my stack affected?
Short version: you need all five of musl, OpenSSH older than 10.6p2, no PAM, password auth on, and a *-locked account. Drop any one and the chain breaks. A few common stacks break it for you:
glibc. glibc's crypt() never emits *, and its sha512crypt doesn't cap input length. Your 300-byte password just gets hashed like any other. No sentinel, no match. This is purely a musl story.
Dropbear. Dropbear never built the 2016 borrowed-salt path. It hands the literal * to musl's crypt() as a salt, musl can't parse it, returns NULL, and Dropbear treats that as the plain failure it is.
PAM. If PAM is in the auth path, sshd isn't running the crypt()/shadow comparison this abuses. Not affected through this bug.
The uncomfortable condition is the last one. Locking service accounts with * isn't a mistake, it's the hardening guide. Which means the devices most likely to satisfy condition five are the ones someone took the time to lock down. On a vulnerable build, the better your hygiene, the cleaner the path in.
The fix
OpenSSH 10.6p2 (commit 4353fdc43, 2026-11-05) closes it three ways, any one of which would be enough:
- shadow_pw() now returns an xstrdup() copy instead of a pointer into libc's buffer. sshd's view of the password field can't be wiped out from under it anymore.
- Shadow fields shorter than four characters are rejected before any crypto runs (MIN_PASSWORD_HASH_LEN = 4). A bare * never reaches the comparison.
- pick_salt() copies what it borrows and uses freezero, so the borrowed-salt path no longer shares state with libc at all.
If you can't take 10.6p2 yet, the commit backports cleanly to OpenSSH 7.3p1 and later. It's a small patch.
Backports often keep the old version number, so check for the commit, not just the version string. Our post on Linux patching covers why that makes vulnerability tracking harder.
Config-only, if you can't rebuild today:
- PasswordAuthentication no closes it completely on key-only devices, and you should probably be running this anyway.
- MaxAuthTries 1 is the only config-only option that keeps password auth working. It holds because the attack needs the second attempt. Note the “1”: MaxAuthTries 2 is still exploitable. Treat this as a tourniquet, not a fix, and schedule the rebuild.
How to test a device: the five-minute field test
For healthcare provider and biomedical engineering teams. You can test this from any workstation with SSH reach to the device. No source access needed. Test only devices your organization owns, operates, or has written authorization to assess, and only in a controlled setting.
- Open an SSH session to a known locked service account (e.g. root), with public-key auth disabled for the attempt. For example:
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password root@<device-ip> - At the first password prompt, enter a short junk string. It gets rejected, as expected.
- At the second prompt in the same connection, enter an over-length password. 300 characters is a safe choice (musl stops hashing above 256).
A shell at step 3 is active exploitation, not a successful login. Capture the account name and id output, disconnect, and keep the evidence.
Reading the result
- Rejected both times: not vulnerable in this configuration (or already patched; the two look identical from outside).
- Accepted on the second prompt: vulnerable. The account doesn't matter. Every *-locked account on that image, root included, is effectively passwordless to anyone with network reach.
Cautions
- Use a spare or test unit, never one serving patients. Repeated failures can trip lockout or alerting on some devices.
- Stop at two attempts. The test is definitive by design.
- One positive means assume the whole model line is affected until the manufacturer says otherwise.
- Report positives to the manufacturer's product-security contact. Many device vendors haven't published advisories yet.
Questions for your vendors
- Which OpenSSH version ships, and does it include the fix (commit 4353fdc43 / 10.6p2 or later)?
- Is sshd linked against musl, and is PAM in the auth path?
- Are maintenance and root accounts *-locked in the shipped image?
- What's your patch and advisory timeline, and your recommended interim mitigation? (MaxAuthTries 1; anything 2 or higher is insufficient.)
Disclosure and credit
Independent research and reproduction by Daniel Beard, Medcrypt, including verification on a stock Yocto image. This is coordinated disclosure: the upstream fix shipped in OpenSSH 10.6p2 on 2026-11-05, the Alpine advisory is public, and a working PoC is already circulating, which is why we're publishing detection, mitigation, and guidance now rather than sitting on it.
Questions, or a positive finding on your own devices? Medcrypt Product Security, info@medcrypt.com.
FAQ
Is my Yocto image affected?
Only if all five conditions hold at once: musl libc rather than glibc, an OpenSSH release older than 10.6p2, no PAM in the SSH auth path, password authentication on, and at least one account locked with *. Dan reproduced it on a stock Yocto image. Check your SBOM for musl, the OpenSSH version and PAM state. If you rely on glibc or Dropbear, confirm a distro default or an sftp dependency didn't pull OpenSSH back in.
Which OpenSSH versions are affected?
The CVE record covers sshd in OpenSSH before 10.6p2 on Linux, when musl libc is used without PAM. The fix is OpenSSH 10.6p2 (commit 4353fdc43), and the commit backports cleanly to 7.3p1 and later.
Can I fix it without a full rebuild?
You can close it with config while you schedule the rebuild. PasswordAuthentication no closes it completely on key-only devices. MaxAuthTries 1 is the only config-only option that keeps password auth working, and MaxAuthTries 2 is still exploitable. Treat a device on MaxAuthTries 1 as mitigated, not fixed. The real fix, backporting commit 4353fdc43, is a small patch but still means rebuilding the image.
Do I have to tell FDA about this?
It depends on your risk assessment of fielded devices. Section 524B expects critical vulnerabilities that could cause uncontrolled risks to be patched as soon as possible, out of cycle. We advise treating your 524B postmarket plan as the controlling document and mapping it onto FDA's 2016 controlled and uncontrolled risk framework for Part 806 reporting decisions. Coordinate customer notification through your disclosure channel.
References
CVE-2026-123158, NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-123158
OpenSSH 10.6p2 release notes: https://www.openssh.org/releasenotes.html#10.6p2
OpenSSH upstream fix, commit 4353fdc43, 2026-11-05: https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482
Alpine Linux security advisory, alpine-announce, 2026-11-05: https://www.alpinelinux.org/posts/2026-11-05-openssh-password-authentication-bypass/
Wakefield researcher Ozgun Kultekin disclosure: https://sshhole.com/
Medcrypt, independent verification and Yocto reproduction, internal write-up; contact Medcrypt Product Security for details
musl libc crypt() source, src/crypt/crypt.c; 1024-byte cap: https://git.musl-libc.org/cgit/musl/tree/src/crypt/crypt.c
OpenSSH sshd_config(5), MaxAuthTries and PasswordAuthentication behavior: https://man.openbsd.org/sshd_config
FDA Cybersecurity in Medical Devices: Quality Management System Considerations and Content of Premarket Submissions, final guidance, February 2026: https://www.fda.gov/media/119933/download. Medcrypt's breakdown: FDA premarket cybersecurity guidance


