This article will look at various techniques for breaking SSH private keys.
An unencrypted private key can be used by anyone with access to the file.
An encrypted key, on the other hand, can only be used by those who know the password needed to decrypt the key.
Thus, a compromised encrypted private key is of no value to the attacker until it can be broken.
In the first part, we’ll look at the different types of key available and how to generate them. In the second part, we’ll look at how to try and break these keys using a "naive" script, John the Ripper and Hashcat.
This article is also available in french 🇫🇷.
Note 📝: a glossary is available at the end of the document.
Motivation
As a technical auditor (or learner in a virtual laboratory), sooner or later you’re likely to come face to face with an SSH private key after compromising a machine or user account. At that point, it’s worth knowing what you can do to take advantage of this opportunity (decrypt the key and use it). All the more so as, if successful, this would make it possible to pivot to one of the machines where it can be used (see how to break a hashed known_hosts
file).
ssh-keygen
General
To generate test keys, we’ll use the ssh-keygen
command which is part of openssh.
Note 📝: dropbear has a dropbearkey
command but does not support encrypted keys. No arms encrypted key no chocolate breaking.
To install openssh on ArchLinux: pacman -Syu openssh
. For other distributions, see the package name on repology.
Normally, generated private keys will be stored in ~/.ssh/
, i.e. in each user’s home directory.
This article uses ssh-keygen
supplied with OpenSSH 9.6 and based on OpenSSL 3.2.1.
➜ ssh -V
OpenSSH_9.6p1, OpenSSL 3.2.1 30 Jan 2024
The basics
It is possible to generate a key with the default configuration without specifying any options.
➜ ssh-keygen
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/noraj/.ssh/id_ed25519): /tmp/clé_démo
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /tmp/clé_démo
Your public key has been saved in /tmp/clé_démo.pub
The key fingerprint is:
SHA256:Fh+BPhBLTUY8/9n1b/DQQTvHoSkPZ4N20wpiOivM6DE noraj@norarch
The key's randomart image is:
+--[ED25519 256]--+
| o*+.. |
| ..o= . o |
| .o.o. . =.o|
| o=.B O =+|
| S.+.Oo+o=|
| + oo+ o|
| E+ o +.|
| .o+ . +|
| .. . . |
+----[SHA256]-----+
We will obtain a private key as follows:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBgxQkqMd
jyuADNv6HN31l5AAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBqcVNY4wFSlVOz+
EpJAa06Qtuv1uciGbBUCRHmHhbGoAAAAkNl+6oYEq7ZyEWuubCSBuATjTVf0if/QdNYWB6
e8NGSrpGEgoSCzaJOo2mnBp20P4G8hpT5RFs5skfEWBlItEyX85FO2bj8YCIlRtCruaegC
f40zSY7acP8Y2YE5v6RCPZ9TT3TckMERlKsMSWM6ksmvBkoYLZq/kR7Od2XuQTrsAXdxMb
cHXF72FPtfdiN1Pw==
-----END OPENSSH PRIVATE KEY-----
Whose corresponding public key is:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqcVNY4wFSlVOz+EpJAa06Qtuv1uciGbBUCRHmHhbGo noraj@norarch
By default, the key is generated in OpenSSH private format (other supported formats are PEM, PKCS8 and RFC4716), but this may change depending on the key type or option used.
-m key_format
Specify a key format for key generation, the -i (import), -e (export) conversion options, and the -p change passphrase operation. The latter may be used to convert between OpenSSH private key and PEM private key formats. The supported key formats are: “RFC4716” (RFC 4716/SSH2 public or private key), “PKCS8” (PKCS8 public or private key) or “PEM” (PEM public key). By default OpenSSH will write newly-generated private keys in its own format, but when converting public keys for export the default format is “RFC4716”. Setting a format of “PEM” when generating or up‐dating a supported private key type will cause the key to be stored in the legacy PEM private key format.
Note 📝: RFC4716
is a format specific to public keys, so it doesn’t apply to private keys. I don’t know why the man
page of ssh-keygen
talks about it for private keys too. By the way, exporting a private key requesting the RFC4716 format will result in a private key in OpenSSH format.
Here is the list of formats supported by ssh-keygen
according to key type:
Type ⬇️ Format ➡️ | Default | PEM | PKCS8 | OpenSSH |
---|---|---|---|---|
DSA | OpenSSH | ✅ | ✅ | ✅ |
RSA | OpenSSH | ✅ | ✅ | ✅ |
ecdsa | OpenSSH | ✅ | ✅ | ✅ |
ed25519 | OpenSSH | ❌ | ❌ | ✅ |
Warning: For example, ssh-keygen -t ed25519 -f clé_ed25519 -m PKCS8
will provide a key in OpenSSH format and not PKCS8 (silent fallback). If the requested format is not available, it will be replaced by the OpenSSH private format.
To read the private key in a human-readable format, use one of the commands below, depending on the key type and format.
# DSA
openssl dsa -in clé_dsa_pem -text -noout
openssl dsa -in clé_dsa_pkcs8 -text -noout
# RSA
openssl rsa -in clé_rsa_pem -text -noout
openssl rsa -in clé_rsa_pkcs8 -text -noout
# ecdsa
openssl ec -in clé_ecdsa_pem -text -noout
openssl ec -in clé_ecdsa_pkcs8 -text -noout
For ed25519 keys, which can only be generated in OpenSSH format by ssh-keygen
, you’ll need to convert them upstream, for example using the sshpk-conv
command provided in the npm sshpk package.
# ed25519
npm i -g sshpk
sshpk-conv clé_ed25519_openssh -t pem -p -o clé_ed25519_pem
sshpk-conv clé_ed25519_openssh -t pkcs8 -p -o clé_ed25519_pkcs8
However, even in PEM or PKCS8 format, it will be impossible to read them directly with OpenSSL.
The only Elliptic Curve algorithms that OpenSSL currently supports are Elliptic Curve Diffie Hellman (ECDH) for key agreement and Elliptic Curve Digital Signature Algorithm (ECDSA) for signing/verifying.
x25519, ed25519 and ed448 aren’t standard EC curves so you can’t use ecparams or ec subcommands to work with them. If you need to generate x25519 or ed25519 keys then see the genpkey subcommand.
_Source_
Since Ed25519 is the EdDSA signature system using SHA-512 (SHA-2) and Curve25519 (a special case of EdDSA), there wouldn’t be much to display anyway.
Key types
Here are the different key types supported by ssh-keygen
:
- DSA
- RSA
- ecdsa
- ecdsa-sk
- ed25519
- ed25519-sk
The two types ending in MARKDOWN_HASHdcf8c5b65ddef54be59c5ece40fffecdMARKDOWNHASH
(Security Key_) are variants specifically reserved for hardware two-factor authentication devices supporting FIDO/U2F (e.g. YubiKey).
To specify the type of key to be generated, use the option -t
.
Encryption types
The various encryption algorithms are:
- 3DES CBC
- AES 128/192/256 CBC/CTR/GCM
- chacha20-poly1305
The list may differ depending on the version of OpenSSL installed. The command below lists the supported versions:
➜ ssh -Q cipher
3des-cbc
aes128-cbc
aes192-cbc
aes256-cbc
aes128-ctr
aes192-ctr
aes256-ctr
aes128-gcm@openssh.com
aes256-gcm@openssh.com
chacha20-poly1305@openssh.com
To specify the encryption algorithm, use the option -Z
.
Generation examples
Here are some ZSH scripts for generating SSH keys.
Generate a key for each key type:
#!/usr/bin/env zsh
for key_type in dsa rsa ecdsa ed25519 # ecdsa-sk ed25519-sk
do
ssh-keygen -N '123soleil' -t ${key_type} -f /tmp/all-keys/clé_${key_type}_openssh
done
Generate a key for each encryption algorithm:
#!/usr/bin/env zsh
for cipher in $(ssh -Q cipher)
do
ssh-keygen -N '123soleil' -t ed25519 -Z $cipher -f /tmp/all-ciphers/clé_ed25519_${cipher}_openssh
done
Generate a key for each key type with each encryption algorithm:
#!/usr/bin/env zsh
for key_type in dsa rsa ecdsa ed25519 # ecdsa-sk ed25519-sk
do
for cipher in $(ssh -Q cipher)
do
ssh-keygen -N '123soleil' -t $key_type -Z $cipher -f /tmp/all-keys-ciphers/clé_${key_type}_${cipher}_openssh
done
done
Generate a key for each key type with each export format:
#!/usr/bin/env zsh
for key_type in dsa rsa ecdsa ed25519 # ecdsa-sk ed25519-sk
do
for format in OpenSSH PEM PKCS8 RFC4716
do
if [[ $format = 'OpenSSH' ]]
then
ssh-keygen -N '123soleil' -t $key_type -f /tmp/all-keys-formats/clé_${key_type}_${format}
else
ssh-keygen -N '123soleil' -t $key_type -f /tmp/all-keys-formats/clé_${key_type}_${format} -m ${format}
fi
done
done
Generate a file containing the pseudo-hash for each key type with each encryption algorithm in OpenSSH and PEM format (this will be useful for later):
#!/usr/bin/env zsh
for key_type in dsa rsa ecdsa ed25519 # ecdsa-sk ed25519-sk
do
for cipher in $(ssh -Q cipher)
do
ssh2john /tmp/all-keys-ciphers/clé_${key_type}_${cipher}_openssh > /tmp/all-keys-ciphers/clé_${key_type}_${cipher}_openssh.jtr
ssh2john /tmp/all-keys-ciphers/clé_${key_type}_${cipher}_PEM > /tmp/all-keys-ciphers/clé_${key_type}_${cipher}_PEM.jtr
done
done
Approaches
We will now try to break down these keys using three methods:
- "naive" script,
- John the Ripper,
- Hashcat.
For the sake of simplicity, for each method we consider only the dictionary attack.
Indicative execution times are given for an Intel i5-1145G7 @ 2.6 GHz processor (script, John the Ripper) with an Intel® Iris® Xe Graphics (Hashcat) iGPU running OpenCL 3.0 NEO
.
Naive (script)
This so-called naive Ruby script will read a password dictionary and execute an ssh-keygen
command with the candidate password until ssh-keygen
returns something other than an error. This will then indicate that the correct password has been found.
require 'open3'
if ARGV.size == 2
password_found = false
File.readlines(ARGV[1], chomp: true).each do |password|
Open3.popen3("ssh-keygen -y -f #{ARGV[0]} -P '#{password}'") { |i,o,e,t|
error = e.read.chomp
if error.empty?
puts "\nThe password is: #{password}"
password_found = true
elsif /incorrect passphrase supplied to decrypt private key/.match?(error)
print '.'
else
puts "Error: #{t.value}"
puts error
end
}
break if password_found
end
else
puts "Usage : ruby #{__FILE__} SSH_KEY WORDLIST"
puts "Example: ruby #{__FILE__} ~/.ssh/id_ed25519_crack /usr/share/wordlists/passwords/richelieu-french-top20000.txt"
end
Note 📝: using the -y
option only works for the OpenSSH format. For a key in PEM format, you’ll need a command that attempts to change the password (or remove the encryption) from the key: ssh-keygen -f /path/key -m pem -p -P pwd_attempt
. But don’t perform this in the context of an audit without the client’s permission or else copy the key locally before.
This script ran in 89 seconds (or 80 without displaying .
when the password is wrong).
Display output:
➜ time ruby ssh-bf.rb ~/.ssh/id_ed25519_crack /usr/share/wordlists/passwords/richelieu-french-top20000.txt
.....................................................................................................................................................................................................................
.....................................................................................................................................................................................................................
.........................................................................................................................
The password is: fripouille
ruby ssh-bf.rb ~/.ssh/id_ed25519_crack 87,56s user 1,28s system 99% cpu 1:29,02 total
In particular, this script is called "naive" because it is not multi-threaded (i.e. does not use threads or child processes), which is extremely inefficient.
On the other hand, the advantage of this method is that it works for all supported file formats, encryption algorithms and key types, as it uses ssh-keygen
directly.
John the Ripper
As there is often a very long time between two versions of JtR, there are often many bugs fixed or new features in the main branch of the git repository that are not in the latest version available. I therefore recommend installing the git version of JtR (e.g. john-git for ArchLinux).
All tests will be performed with the version below:
John the Ripper 1.9.0-jumbo-1+bleeding-173b5629e8 2024-01-18 00:08:42 +0100 MPI + OMP [linux-gnu 64-bit x86_64 AVX AC]
ssh2john can be used to create a pseudo-hash understandable by JtR from an SSH private key.
➜ ssh2john ~/.ssh/id_ed25519_crack > /tmp/hash_jtr.txt
/home/noraj/.ssh/id_ed25519_crack:$sshng$6$16$3843af13aef53d4c0906f2998b082b3d$274$6f70656e7373682d6b65792d7631000000000a6165733235362d6374720000000662637279707400000018000000103843af13aef53d4c0906f2998b082b3d0000001800000001000000330000000b7373682d6564323535313900000020785c404a9750e39a4cdb788e787f13fdf0d62ca91ea76e3034272722980c222d00000090a99a138ebcd23a3fac923d88fb2b42833e4fc29d409efe86d543f8224cd11263b511e6cc858919bb58692a07664fb56905915bfe8d4a31db398827a65070f33dc127c3ca7d2ad9d184922e7a5e657de10166ee6adfc0b4cc736567adaeb8b1a160d008b1e5bd0a0188be18152d8eecec7bbd9b35d8f551e059bc57fa7642b7535a4d1aad8bb616576b9fb6b2e62bb7e5$24$130
This time, breaking takes 18 seconds, which is much less than the naive approach, thanks to multi-thread execution.
➜ time john /tmp/hash_jtr.txt -w=/usr/share/wordlists/passwords/richelieu-french-top20000.txt --format=ssh
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 2 for all loaded hashes
Cost 2 (iteration count) is 24 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
fripouille (/home/noraj/.ssh/id_ed25519_crack)
1g 0:00:00:15 DONE (2024-01-24 17:23) 0.06601g/s 38.02p/s 38.02c/s 38.02C/s michael1..poiuyt
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
john /tmp/hash_jtr.txt --format=ssh 136,21s user 0,04s system 719% cpu 18,947 total
What’s also handy about JtR is that, whatever the key type or encryption algorithms used, the reference to specify is always the same: ssh
.
Key types supported by JtR and ssh2john
:
Type | JtR | ssh2john |
---|---|---|
DSA | ✅ | ✅ |
RSA | ✅ | ✅ |
ecdsa | ✅ | ✅ |
ed25519 | ✅ | ✅ |
Key formats supported by ssh2john
:
Format | Support |
---|---|
OpenSSH | ✅ |
PEM | ✅ |
PKCS8 | ❌ |
RFC4716 | N/A |
Encryption algorithms supported by ssh2john
(possibly some algorithms not supported by ssh2john
are supported by john
but there is no tool to generate the pseudo-hash):
Algorithm ⬇️ Format ➡️ | OpenSSH (all) | PEM (DSA, RSA, ecdsa) | PEM (ed25519) |
---|---|---|---|
3des-cbc | ❌ | ✅ | ❌ |
aes128-cbc | ❌ | ✅ | ❌ |
aes128-ctr | ❌ | ✅ | ❌ |
aes128-gcm@openssh.com | ❌ | ✅ | N/T |
aes192-cbc | ❌ | ✅ | ❌ |
aes192-ctr | ❌ | ✅ | ❌ |
aes256-cbc | ✅ | ✅ | ❌ |
aes256-ctr | ✅ | ✅ | ❌ |
aes256-gcm@openssh.com | ❌ | ✅ | N/T |
chacha20-poly1305@openssh.com | ❌ | ✅ | N/T |
Note 📝: For keys of type ed25519
, sshpk
was used to convert them to PEM format, as ssh-keygen
can only export them to OpenSSH format. The @openssh.com
algorithms could not be converted by sshpk
. sshpk
was useful for writing the article, but will not be useful for an auditor, as converting the key requires knowing the password.
Encryption algorithms supported by JtR:
Algorithm | Support |
---|---|
3des-cbc | ✅ |
aes128-cbc | ✅ |
aes128-ctr | ✅ |
aes128-gcm@openssh.com | ✅ |
aes192-cbc | ✅ |
aes192-ctr | ✅ |
aes256-cbc | ✅ |
aes256-ctr | ✅ |
aes256-gcm@openssh.com | ✅ |
chacha20-poly1305@openssh.com | ✅ |
In conclusion, ssh2john
only works with 2 algorithms when the private key is in OpenSSH format, which is a real shame, as this is the default format for ssh-keygen
. In PEM format, all algorithms are supported by ssh2john
for DSA, RSA, ecdsa keys, but none for ed25519 keys.
Hashcat
As with JtR, there is often a very long time between two versions of Hashcat. To get bug fixes or new features, you’ll need to install the git version using the main branch of the repository. I therefore recommend installing the git version of Hashcat (e.g. hashcat-git for ArchLinux).
All tests will be performed with the version below:
v6.2.6-846-g4d412c8e0+
The format of the SSH pseudo-hash managed by Hashcat seems to be the one generated by ssh2john. Hashcat does not provide any tools for this purpose. However, the format supported seems to be an old version and does not support the new format required for algorithms using Bcrypt PBKDF (bcrypt_pbkdf.3) introduced in 2013 in OpenSSH.
Unlike JtR, Hashcat uses different modules and therefore has different references depending on the keys. The distinction is made according to identification numbers arbitrarily chosen by ssh2john
.
$0$
: module_22911$6$
: module_22921$1$
et$3$
: module_22931$4$
: module_22941$5$
: module_22951$2$
: no module
We’ll see later what these numbers correspond to in a dedicated section.
However, Hashcat does not support the "new" output format of ssh2john
as it stands: /path/file:pseudo-hash
. You will therefore need to remove the path from the output file. The command below deletes the path of all .jtr
files (arbitrary extension) for bulk editing.
find . -type f -name '*.jtr' -exec sed -i -E 's/.+://' {} \;
For one hash alone, the following command should suffice:
ssh2john clé_rsa_aes256-cbc_PEM_demo | cut -d ':' -f 2 > clé_rsa_aes256-cbc_PEM_demo.hc
Key types supported by Hashcat:
Type | HC |
---|---|
DSA | ✅ |
RSA | ✅ |
ecdsa | ✅ |
ed25519 | ❌ |
Strictly speaking, there are no key formats supported, as Hashcat doesn’t offer a tool for converting keys to pseudo-hash, but relies on ssh2john
. The same limitations will therefore apply.
Encryption algorithms supported by Hashcat:
Algorithm ⬇️ Type ➡️ | DSA | RSA | ecdsa | ed25519 |
---|---|---|---|---|
3des-cbc | ✅ | ✅ | ✅ | N/A |
aes128-cbc | ✅ | ✅ | ✅ | N/A |
aes128-ctr | ✅ | ✅ | ✅ | N/A |
aes128-gcm@openssh.com | ✅ | ✅ | ✅ | N/A |
aes192-cbc | ✅ | ✅ | ✅ | N/A |
aes192-ctr | ✅ | ✅ | ✅ | N/A |
aes256-cbc | ✅ | ✅ | ✅ | ❌ |
aes256-ctr | ✅ | ✅ | ✅ | ❌ |
aes256-gcm@openssh.com | ✅ | ✅ | ✅ | N/A |
chacha20-poly1305@openssh.com | ✅ | ✅ | ✅ | N/A |
In conclusion, it is difficult to test ed25519
because ssh2john
has little support for this type of key. For ssh-keygen
+ ssh2john
output, only the identification numbers $1$
, $2$
, $3$
and $6$
are printed. $1$
and $3$
are managed by the same 22931 module and seem to work. $1$
is managed by module 22921, but never works. The $1$
(AES 256-bit CTR) is output only in OpenSSH format for all key types. $2$
is not managed by any Hashcat module.
Finally, the only module that is usable in practice is 22931
, the others correspond either to key combinations that are never output by ssh-keygen
(perhaps OpenSSL? or very old versions of OpenSSH?), or that are not managed by ssh2john
(you’d have to extract the cryptographic parameters from the private key and create a pseudo-hash by hand).
Note 📝: with HC it’s impossible to break the reference key ~/.ssh/id_ed25519_crack
generated with the default parameters of ssh-keygen
.
Other tools
Unlike for encrypted archives or KDBX password containers, there aren’t really any tools outside JtR and HC for breaking encrypted SSH keys.
Then there’s ssh-key-crack, a C tool, single-threaded execution, abandoned 16 years ago, handling only RSA and DSA keys, in PEM format only, directly on the key (with no intermediate format, as with JtR and HC with ssh2john
).
Or sshPrivateKeyCrack, a python tool, archived, abandoned 2 years ago, is just a "naive" script making system calls to ssh-keygen
, removes key encryption once the password is found, but supports multi-threaded execution.
ssh2john format
The pseudo-hash format generated by ssh2john
is arbitrary and doesn’t correspond to any standard.
It breaks down as follows, with the filename and pseudo-hash separated by :
:
(fichier:)$sshng$alg_type$len_salt$salt$len_data$data($rounds$ciphertext_begin_offset)
Pseudo-hash is made up of a signature sshng
prefix and 5 or 7 parts delimited by $
.
Example of a hash:
/home/noraj/.ssh/id_ed25519_crack:$sshng$6$16$3843af13aef53d4c0906f2998b082b3d$274$6f70656e7373682d6b65792d7631000000000a6165733235362d6374720000000662637279707400000018000000103843af13aef53d4c0906f2998b082b3d0000001800000001000000330000000b7373682d6564323535313900000020785c404a9750e39a4cdb788e787f13fdf0d62ca91ea76e3034272722980c222d00000090a99a138ebcd23a3fac923d88fb2b42833e4fc29d409efe86d543f8224cd11263b511e6cc858919bb58692a07664fb56905915bfe8d4a31db398827a65070f33dc127c3ca7d2ad9d184922e7a5e657de10166ee6adfc0b4cc736567adaeb8b1a160d008b1e5bd0a0188be18152d8eecec7bbd9b35d8f551e059bc57fa7642b7535a4d1aad8bb616576b9fb6b2e62bb7e5$24$130
The first part, after the signature, corresponds to an identification number (arbitrary) associating key type + encryption algorithm.
0
: RSA/DSA + 3DES1
: RSA/DSA + AES-1282
: RSA/DSA/EC + Bcrypt PBKDF + AES-256-CBC3
: EC + AES-1284
: RSA/DSA + AES-1925
: RSA/DSA + AES-2566
: RSA/DSA/EC + Bcrypt PBKDF + AES-256-CTR
Note that ssh2john
still generates a hash with $1$
for a DSA + 3DES key instead of $0$
because ssh2john
generates a $0$
only for a "key length of 24" (why, given that 16, 24 and 32 are generally AES key sizes, whereas 3DES uses 112 or 168 bits?)
The following parts correspond to
- salt length
- salt
- data length
- data
- optionally, the number of passes for
bcrypt_pbkdf
- optionally,
ciphertext_begin_offset
forbcrypt_pbkdf
Benchmark
Let’s take the following key:
- Type: RSA
- Encryption algorithm: AES 256 bits with CBC mode
- Format: PEM
- Size: 4096 bits
- Password:
test12345
(position 385742 inrockyout.txt
)
➜ ssh-keygen -N test12345 -t rsa -Z aes256-cbc -m PEM -f clé_rsa_aes256-cbc_PEM_demo -b 4096
Generating public/private rsa key pair.
Your identification has been saved in clé_rsa_aes256-cbc_PEM_demo
Your public key has been saved in clé_rsa_aes256-cbc_PEM_demo.pub
The key fingerprint is:
SHA256:zys3VyMJ6r9laWTGgo/QsrsCbaxV1tu3iRR4GlKW80E noraj@norarch
The key's randomart image is:
+---[RSA 4096]----+
| oE |
| = . |
| + + . |
| +.+.=. |
| o oo.SB.o=. |
| . = ++=o=+.o |
| = ....+o== . |
| . . .o +=+ |
| .o. +++ |
+----[SHA256]-----+
➜ ssh2john clé_rsa_aes256-cbc_PEM_demo
clé_rsa_aes256-cbc_PEM_demo:$sshng$1$16$31C54C9A4E9873B4DD6C58DC79B80A6C$2352$7189892c8e079116a1115d3496eb8e17e789e65ce7cbc1b6e0bc03e69034a6bf0f9bfb0d52be414942ae19691c43084ddcf2d3acda121bee07f64e980666c695a2dd713621129cb3490b85369c5233c0613f409c52cc6f9e82eae37dd7113fc3f17d63b6ac09f400bf55a1fd7739e483f02f3c1b6b2c8a13ea22250b0cfff832f7efd309501c5a7188f47d4654372e5ab0191a303765cbb592aaf97f07186edbfcb1b4f9eb0798a395448a64dec5d38eb9db616f2eed80aa94261c513e79b87a495d458a5da75efbc292e7c8c721a41b10f4e775e7d97e43716225bc6162a0665624acee70962d3e8b4eb3dc7d87db653345b1ef6da2beb5f35efc3724d9eac03a94065f6a96c44cd344529a3295e1efec7b218d179780e16f9c49bc8976ae1390e0eb9a885f0245cb9ad8aa4530a343afbdfafd7ae3e04ed5a122abfeedd1860e8ae82a6b3f728472b650bbfe3a7536296af24540c67f79bb13b8b497b1ffea30373d665202e3d80223c5e3f7d19b982a567c6b545392d559f5f42947e0aaec6b16cfb44a53f2cea559a87f0ea6b15bbf25fb3cc7139b66822bd21e91972d4db8d0e894b6472da4246f454d28fe3e05a898d6e1ef4141afaaa52e3b1691eba3be9ebc372262f7c2e2db0b44aa3dbefbc93d85bc69b70456447c06b029809e3c7c7be5511a83be0164d510ebc93bd0e75e971dea579eac03197a9e25f061c075b7934b3a3e8942c9a52d25d260365c9990c0dc7c79d64e868570b9033fdc322cffaa753b4526c7403a47667443942ebe99798a2eb7b228bf00526679856bd2bd0895dd1bb70064c0c8f06cb31d9c95e246cb781f9f701aafcefb6e454e2ffe1e68dbdb1202ca000e0d379be7f9f59ab87dd255f53a0e69e13215fb47bec6158e2cb5341f3ce6c4c51475634150467316b62f5fd9fbfef6ee0d891f595f38cd29724af3efa543ccf16cea9d0c1da5ce430e2112e811ae1a50c6de930722bb0425221fb6ea1f4404fccdda0824043111170c81c0c7311774a577a44fd3529dd848c6bf24920d74f23d4b9bad9a7a7baaab99efcfe35a46bf8234b546bb4fd122876de2a0b61d01b32e05f975f4bd1df0d56c6fc4e64c354f6329908e038899a97f00dd2f0d2aafe57195faa35f3c4943e59757e7262336fdd1972183927225e4097642c3be4152be32c149d20a07f3a07860c6adc0da3a9dd8454362adbeacf296abeb9e9a38ac923235f9c32b6f926e302ba9ed07744602875400e8d0dede22c3232f1a5d7a7c989f55dbba073eff87ac1ef00b2e1e686553c35bd9dc8aff32b1cdc25bf0b99bdeb561b1f39f3812ba33be6b94997edc89a44fe44e728290f9a8bbcdcac05426de57ec85c93e6825cc44aa92c8192485c3300f36fc08cee81babb9778a8653ecd01155018e6e3310629cf21f42b7aa9dbd6ba88bf80bd896c81ad0f426b162e70398dadc610d116decb0b71cc59468a2d14aa678f3e4d33e6fab715a195b2e61ef243a8ddbff50096948f8fc249aba75bde37a2abc9dff3d05a1e8a651f0efe9e24e22441499e7b0068109e3d16073b46dfa1eca94344feabc0959644ae56c330084cd8d05a089beced712da5c33bd3764129433ec1403a4e73d2b3a34173068bd6f5b84bf1e2e53eef9e89a2da217ada0bbd2d13fc4e1d9c24a16973cf0f5c79cc0eb3469716e7b584d8bcb3808aa05376c3a8334b53c5afca68d371688bf7607768c93abca882bd50feef2180c1fb5127ba4d1b9e7fcf0443b058115032b7bf896ad8b43136f124857d518a184e8c69e94b70982797db721fade1686574eb3f9f3d02de8bfd4ed56a43b4f16cc21adac5e517b5e7b15fecf921df9b294a374a4205638a41cc0aafbfe2324b4b862ffeb918bcf65228c736cdb5e9f1b5c440e39732e815ca7e589aa72f77c4b81d94d7e647f01e81022ad25e7d8fb0bcfcec2e01ce2a01c35ee8abfb56fd4c299184d0d0fba1ae08e1284d7f9de90c56616a0304705a9face5503338f0b44621be71643375b0d5b0fcce1f415238332e37dede2115faff839cfef64d2886ec030955d2db7300ac48896334d48ce9fc69285d3558ab2e7fbc12948445ba6ca11c320b3bf7abc937e12ec0f524144cf4a908fff0a6b496c9e23fd03da7f9b87ddd797cec432ac6ba8a48bda0b35559002c04557e7512c7592a2a27274000421ec9822544cb510d63a8d190af9f8ae973e0dcc319d4d34ddfdf6c2d6cc90329192722c78005aa1abe0f8729356d615abfb08daabae633e61eed4c3831869e7d4859ef3a67d1cfb24e8ddd14e5e3e75aaec1eb1c148776e7705533b3188f161fef4ece79a2af9ffa72f0e01c75a47cbe9c276c30eea386db17829c76cdf28fca3dec9a6b1c6bd3776905486b409cce506c6ef080bbc8601981d71a185dfc9fd332522c96b52fbc90d8a76e7dfca06b7d92248bf1aee8e72f115c8d6f519f6361b90cb3b52e8de05322121fb27735b9f1bae76e696aae0a905d41316b05959dd0d8ba5ae1dd5a1b867ea2182d113174f6963e7c83437af8c0da98167795c2725bca59d5cc726e01be0e0d43ae78fbc7cb9d370a26b80a1bf3136e9328752e750d241df3c9973d486e51ce054d0378b834973e22aa21887500a94d3769ac833d1d303236bed07958568b1a5fa8b1efd6eb6f942253feae44b36a1ca55643a2f0469f4997dd0a548bd4f8b24225209e5d7f14ffc2e4f6e995f8ae138015265b8ce28794ac756289900b14049a92c4ba3ceb098d3d50c113911a0f5266c6a9f9d1f55a1fefd6c187127a4f94bc5efdb9812492fbfad0241b4ba3c1f4cb78fd6212bf1718eaf22f509354a8bab7ac05d5e74b2bb640bc4d375b664cd834ffa84301b027126fade04c4dad8c6f9aca3a740ea5206572a9b68558ef8dc8b7f141a5ac2dd6fee7f4237cdfd481ebe6da1298f037ad05fe760ff82fdca30d3d738d8646b7bcfc1ba4db1f26451a29a882b67674495d521a69b7e3ca73d6170778ba168d073f8876ff04f63cee50a240b77b7902c46e489e75bf4c9169eb78906a2b23f3238c5f3f14a0f3a244aab6a65888e1c50c178e3d4a7cfbbb9dcc57dbc3432aacb2fe7e522ffbb11556086646619dd7b8f9ce1a6f3ad8ed37ab633047294a4905f117dad4f0e9d3f21d78dba9647854d252fb923c7065a25ce63cb8a337d38242eca70fff909f5546b80cc2318bde243781d44812918a7f1c1a3c5965e601d219bd62c484762900721f04ff031075068efbca0b02c3e624b2f956a63902f01e04f901bff7a099094c34c61b9abc570faa1962c07ae05ce7372a6f97eb5f46b1b
Here’s the execution time to find the key with the different methods.
Note 📝: time
is the built-in version of ZSH.
For the script of the naive sequential approach, the execution time is 32 minutes:
➜ time ruby scripts/ssh-bf.rb /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo /usr/share/wordlists/passwords/rockyou.txt
...
ruby scripts/ssh-bf.rb /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo 1340,63s user 621,71s system 101% cpu 32:21,10 total
For the script of the naive multi-thread approach (with sshPrivateKeyCrack
), the execution time is around 8 minutes (4 times less time in parallel (on 8 threads) than in single-thread):
➜ time python sshCrack.py -f /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo.jtr -w /usr/share/wordlists/passwords/rockyou.txt
Starting with 8 processes...
Working...
✓ test12345 ✓
CRACKED:
/tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo:test12345
python sshCrack.py -f /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo -w 2119,32s user 1008,62s system 656% cpu 7:56,77 total
For the JtR approach (CPU), execution time is around 5 seconds:
➜ time john /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo.jtr -w=/usr/share/wordlists/passwords/rockyou.txt --format=ssh
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
test12345 (clé_rsa_aes256-cbc_PEM_demo)
1g 0:00:00:00 DONE (2024-02-14 16:04) 12.50g/s 4822Kp/s 4822Kc/s 4822KC/s teubesk..terminator3
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
john /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo.jtr --format=ssh 18,71s user 0,07s system 371% cpu 5,050 total
For the HC (GPU) approach, execution time is about 3 seconds:
➜ time hashcat -m 22931 /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo.hc /usr/share/wordlists/passwords/rockyou.txt
...
hashcat -m 22931 /tmp/all-keys-ciphers/clé_rsa_aes256-cbc_PEM_demo.hc 0,50s user 1,15s system 47% cpu 3,476 total
To conclude, the optimized approaches of JtR and HC are extremely fast, several orders of magnitude faster than the naive approaches using ssh-keygen
command calls. However, the latter are still interesting for key types not supported by JtR, ssh2john
and HC.
The chef’s tease
To irritate attackers as much as possible who want to damage the confidentiality of your SSH private key, it’s a good idea to have a ed25519
key encrypted with the chacha20-poly1305@openssh.com
algorithm. Bonus? These are the most robust key type and encryption algorithm available via ssh-keygen
.
ssh-keygen -t ed25519 -Z chacha20-poly1305@openssh.com
Alternatively, you can store your key in the PKCS8
format with the ecdsa
key type.
ssh-keygen -t ecdsa -Z chacha20-poly1305@openssh.com -m PKCS8
If however your key is of type rsa
, encrypted with aes256-cbc
and stored in pem
format, please know that as an auditor I thank you and wish you good luck.
On a more serious note, here are some real recommendations:
- encrypt the private key,
- set system permissions to
600
(-rw-------
, only you must be able to read the key), - the decryption password must be :
- strong,
- stored in a password manager,
- different from the user’s.
Lexicon
Acronyms
- JtR: John the Ripper
- HC: Hashcat
- N/A: not applicable
- N/T : not tested
About the author 📝
Article written by Alexandre ZANNI alias noraj, Penetration Test Engineer at ACCEIS.