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.

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 + 3DES
  • 1: RSA/DSA + AES-128
  • 2: RSA/DSA/EC + Bcrypt PBKDF + AES-256-CBC
  • 3: EC + AES-128
  • 4: RSA/DSA + AES-192
  • 5: RSA/DSA + AES-256
  • 6: 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 for bcrypt_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 in rockyout.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.