Cracking SSH private key passphrases with hashcat and John the Ripper
Extract the hash from an encrypted id_rsa or id_ed25519 with ssh2john, pick the right hashcat mode for legacy PEM keys, and know when only John the Ripper can touch a modern OpenSSH key.
An encrypted SSH private key is just another password-protected secret, and like any of them it reduces to the same question: extract a hash, pick the right cracker and mode, run a wordlist. The wrinkle with SSH keys is that there are two very different formats hiding behind the same -----BEGIN ... PRIVATE KEY----- banner, and only one of them hashcat can touch today.
Two key formats, two different problems
Older keys — anything generated with ssh-keygen -m PEM, or produced by tools that still default to the traditional format — are OpenSSL-style PEM files. The passphrase derives a symmetric key via a simple MD5-based KDF, then decrypts the embedded RSA/DSA/EC key with DES, 3DES or AES-CBC depending on what the header advertises.
Since OpenSSH 7.8 (2018), ssh-keygen writes the newer openssh-key-v1 container by default for every key type, including the now-common id_ed25519. The only KDF that format supports is bcrypt-pbkdf, which is deliberately slower and memory-harder than the old MD5 scheme — good for you if it's your key, bad for an attacker, and the reason a lot of "I pointed hashcat at my key and it errored out" reports turn out to be a format mismatch rather than a cracking problem.
Extracting the hash with ssh2john
Both formats go through the same tool. ssh2john.py ships with the John the Ripper jumbo distribution (on Kali it's typically at /usr/share/john/ssh2john.py or /opt/john/ssh2john.py) and inspects the key header itself to decide how to parse it:
python3 /usr/share/john/ssh2john.py id_rsa > hash.txt
The output is a single line prefixed $sshng$, followed by a cipher id, the KDF parameters, and the encrypted key blob. That prefix is what both hashcat and John recognise — you never feed the raw key file to either cracker directly.
Cracking a legacy PEM key with hashcat
For a legacy key, the cipher id right after $sshng$ tells hashcat which mode to use:
| Hash prefix | hashcat mode |
|---|---|
$sshng$0$ | -m 22911 |
$sshng$6$ | -m 22921 |
$sshng$1$ / $sshng$3$ | -m 22931 |
$sshng$4$ | -m 22941 |
Run it the same way you would any other hashcat job:
hashcat -m 22911 hash.txt /usr/share/wordlists/rockyou.txt
If the passphrase follows a known pattern — a word plus a year, say — a mask attack or a rule-mutated wordlist will get there far faster than a raw dictionary pass; see the wordlist and rules setup that actually works.
If hashcat reports a token length exception, don't assume the hash is corrupt first — check whether the key is actually a modern openssh-key-v1 key. That's the next section.
When hashcat can't help: modern OpenSSH keys
Hashcat's documented SSH modes (22911–22951) were built for the legacy PEM/MD5-KDF format. A key protected with bcrypt-pbkdf — which, again, is the default for every key ssh-keygen has produced since 2018 — isn't covered by a stable mode yet. Don't waste GPU time force-feeding it into one of the modes above; the token layout doesn't match and it will reject the hash.
This is where John the Ripper earns its keep. The same $sshng$ output from ssh2john.py cracks directly in John:
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
If John doesn't auto-detect the right format, list what it has and pick the SSH-related one it offers (the exact name — ssh or ssh-ng — has moved around between jumbo releases):
john --list=formats | grep -i ssh
john --format=ssh-ng --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Because bcrypt-pbkdf is deliberately expensive, expect throughput far below what you'd see cracking a legacy PEM key or an unsalted fast hash — a short dictionary word still falls quickly, but a long random passphrase is a different story.
Takeaways
Identify which container you're dealing with before picking a tool: -----BEGIN RSA PRIVATE KEY----- and friends are the old PEM format hashcat can crack directly; -----BEGIN OPENSSH PRIVATE KEY----- is the bcrypt-pbkdf container that currently needs John the Ripper. Either way, ssh2john.py is the only way to get a crackable hash out of the key file, and the hash identifier's SSH private key page has the full command set once you've confirmed which mode applies.