OpenSSH is a common tool for most of network and system administrators. It is used daily to open remote sessions on hosts to perform administrative tasks. But, it is also used to automate tasks between trusted hosts. Based on public/private key pairs, hosts can exchange data or execute commands via a safe (encrypted) pipe. When you ssh to a remote server, your ssh client records the hostname, IP address and public key of the remote server in a flat file called “known_hosts“. The next time you start a ssh session, the ssh client compares the server information with the one saved in the “known_hosts” file. If they differ, an error message is displayed. The primary goal of this mechanism is to block MITM (“Man-In-The-Middle“) attacks.
But, this file (stored by default in your “$HOME/.ssh” directory) introduces security risks. If an attacker has access to your home directory, he will have access to the file which may contains hundreds of hosts on which you also have an access. Did you ever eared about “Island Hopping” attack? Wikipedia defines this attack as following:
“In computer security, for example in intrusion detection and penetration testing, island hopping is the act of entering a secured system through a weak link and then “hopping” around on the computer nodes within the internal systems. In this field, island hopping is also known as pivoting.“
A potential worm could take advantage of the information stored in the file to spread across multiple hosts. OpenSSH introduced a countermeasure against this attack since the version 4.0. The ssh client is able to store the host information in a hash format. The old format was:
host.rootshell.be,10.0.0.2 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0ei6KvTUHnmCjdsEwpCCaOHZWvjS \ jytm/5/Vv1Dc6ToaxTnqJ7ocBb7NI/HUQEc23eUYjFrZQDS0JRml3RnsG0UzvtIfAPDP1x7h6HHy4ixjAP7slXgqj3c \ fOV5ThNjYI0mEbIh1ezGWovwoy0IxRK9Lq29CacqQH8407b1jEj/zfOzUi3FgRlsKZTsc3UIoWSY0KPSSPlcSTInviG \ oNi+9gC8eqXHURsvOWyQMH5K5isvc/Wp1DiMxXSQ+uchBl6AoqSj6FTkRAQ9oAe8p1GekxuLh2PJ+dMDIuhGeZ60fIh \ eq15kzZGsDWkNF6hc/HmkJDSPn3bRmo3xmFP02sNw==
With the version 4.0, hosts are stored in this new format:
|1|U8gOHG/S5rH9uRH3cXgdUNF13F4=|cNimv6148Swl6QcwqBOjgRnHnKs= ssh-rsa AAAAB3NzaC1yc2EAAAABIw \ AAAQEAvAtd04lhxzzqW57464mhkubDixZpy+qxvXBVodNmbM8culkfYtmq0Ynd+1G1s3hcBSEa8XHhNdcxTx51MbIjO \ dCbFyx6rbvTIU/5T2z0/TMjeQyL3SZttbYWM2U0agKp/86FdaQF6V87loNcDq/26JLBSaZgViZS4gKZbflZCdD6aB2s \ 2sqEV4k7zU2OMHPy7W6ghNQzEu+Ep/44w4RCdI5OYFfids9B0JSUefR9eiumjRwyI0dCPyq9jrQZy47AI7oiQJqSjvu \ eMIwZrrlmECYSvOru0MiyeKwsm7m8dyzAE+f2CkdUh6tQleLRLnEMH+25EAB56AhkpWSuMPJX1w==
As you can see, the hostname is not readable anymore. To achieve this result, a new configuration directive has been added in version 4.0 and above: “HashKnownHosts [Yes|No]“. Note that this feature is not enabled by default. Some Linux (or other UNIX flavors) enable it by default. Check your configuration. If you switch the hashing feature on, do not forget to hash your existing known_hosts file:
$ ssh-keygen -H -f $HOME/.ssh/known_hosts
Hashing ssh keys is definitively the right way to go but introduce problems. First, the good guys cannot easily manage their SSH hosts! How to perform a cleanup? (My “known_hosts” file has 239 entries!). In case of security incident management or forensics investigations, it can be useful to know the list of hosts where the user connected. It’s also an issue for pentesters. If you have access to a file containing hashed SSH hosts, it can be interesting to discover the hostnames or IP addresses and use the server to “jump” to another target. Remember: people are weak and re-use the same passwords on multiple servers.
By looking into the OpenSSH client source code (more precisely in “hostfile.c“), I found how are hashed the hostnames. Here is an example:
“|1|” is the HASH_MAGIC. The first part between the separators “|” is the salt encoded in Base64. When a new host is added, the salt is generated randomly. The second one is the hostname HMAC (“Hash-based Message Authentication Code“) generated via SHA1 using the decoded salt and then encoded in Base64. Once the hashing performed, it’s not possible to decode it. Like UNIX passwords, the only way to find back a hostname is to apply the same hash function and compare the results.
I wrote a Perl script to bruteforce the “known_hosts” file. It generates hostnames or IP addresses, hash them and compare the results with the information stored in the SSH file. The script syntax is:
$ ./known_hosts_bruteforcer.pl -h
Usage: known_hosts_bruteforcer.pl [options]
Â -d <domain>Â Â Specify a domain name to append to hostnames (default: none) Â -f <file>Â Â Â Â Specify the known_hosts file to bruteforce (default: /.ssh/known_hosts) Â -iÂ Â Â Â Â Â Â Â Â Â Â Bruteforce IP addresses (default: hostnames) Â -l <integer>Â Specify the hostname maximum length (default: 8 ) Â -s <string>Â Â Specify an initial IP address or password (default: none) Â -vÂ Â Â Â Â Â Â Â Â Â Â Verbose output Â -hÂ Â Â Â Â Â Â Â Â Â Â Print this help, then exit
Without arguments, the script will bruteforce your $HOME/.ssh/known_hosts by generating hostnames with a maximum length of 8 characters. If a match is found, the hostname is displayed with the corresponding line in the file. If your hosts are FQDN, a domain can be specify using the flag “-d“. It will be automatically appended to all generated hostnames. By using the “-i” flag, the script generates IP addresses instead of hostnames. To spread the log across multiple computers or if you know the first letters of the used hostnames or the first bytes of the IP addresses, you can specify an initial value with the “-s” flag.
Examples: If your server names are based on the template “srvxxx” and belongs to the rootshell.be domain, use the following syntax:
$ ./known_hosts_bruteforcer.pl -d rootshell.be -s srv000
If your DMZ uses IP addresses in the range 192.168.0.0/24, use the following syntax:
$ ./known_hosts_bruteforcer.pl -i -s 192.168.0.0
When hosts are found, there are displayed as below:
$ ./known_hosts_bruteforcer.pl -i -s 10.255.0.0 *** Found host: 10.255.1.17 (line 31) *** *** Found host: 10.255.1.74 (line 165) *** *** Found host: 10.255.1.75 (line 69) *** *** Found host: 10.255.1.76 (line 28) *** *** Found host: 10.255.1.78 (line 56) *** *** Found host: 10.255.1.91 (line 51) *** ^C
My first idea was to bruteforce using a dictionary. Unfortunately, hostnames are sometimes based on templates like “svr000” or “dmzsrv-000” which make the dictionary unreliable. And about the performance? I’m not a developer and my code could for sure be optimized. The performance is directly related to the size of your “known_hosts” file. Be patient! The script is available here. Comments are always welcome.
Usual disclaimer: this code is provided “as is” without any warranty or support. It is provided for educational or personal use only. I’ll not be held responsible for any illegal activity performed with this code.