Hacking Hashed SSH known_hosts file

On Ubuntu, by default, the hosts in .ssh/known_hosts are hashed. This can theoretically help with security. If an attacker compromises a host, they will not be able to tell the IP addresses of other hosts in the known_hosts file.

https://security.stackexchange.com/questions/56268/ssh-benefits-of-using-hashed-known-hosts

Anatomy of the hashed known_hosts

Here is an example of a hashed entry in the known_hosts file.

|1|ma8KL2XrNYkNnknf68N4IuZ+c+I=|PmR+n2i0/epUGZZh2S+LB6OaowQ= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEjqG8/el8c669FxcvEw5mMfDRTDxsjgLiz44dCTtchs

There are three main parts.

The first part ma8KL2XrNYkNnknf68N4IuZ+c+I= is the salt to use.

PmR+n2i0/epUGZZh2S+LB6OaowQ= This is our hashed IP address/hostname

ssh-ed25519 is the key type

AAAAC3NzaC1lZDI1NTE5AAAAIEjqG8/el8c669FxcvEw5mMfDRTDxsjgLiz44dCTtchs Is the public SSH key of the remote host.

SSH-KEYSCAN

We can use ssh-keyscan to check the keys of hosts. The -t ssh-ed25519 option only shows ed25519 keys. Remove or change to show all key types e.g. RSA/DSA

For example:

┌──(kali㉿localhost)-[~]
└─$ ssh-keyscan -t ssh-ed25519 localhost
# localhost:22 SSH-2.0-OpenSSH_9.6p1 Debian-3
localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEjqG8/el8c669FxcvEw5mMfDRTDxsjgLiz44dCTtchs

We can compare the SSH public key with the one in our known_hosts file to verify we have the correct host.

As a side note, we can also use the -H option to show us a hashed version. The salt changes each time it is run, so it is not useful for comparing the hashed IP address.

Example:

┌──(kali㉿localhost)-[~]
└─$ ssh-keyscan -H -t ssh-ed25519 localhost
# localhost:22 SSH-2.0-OpenSSH_9.6p1 Debian-3
|1|j2j9iv/GkPfnG9Yv4WzJsy/L1pc=|wethKgsGBH0Mi+rFW3zSNSWiGso= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEjqG8/el8c669FxcvEw5mMfDRTDxsjgLiz44dCTtchs

Hacking known_hosts hashes

There are a few different techniques that can be used to identify known hosts IP addresses even if they are hashed.

https://stackoverflow.com/questions/427979/how-do-you-extract-ip-addresses-from-files-using-a-regex-in-a-linux-shell

Search through bash history

history | egrep '([0-9]{1,3}\.){3}[0-9]{1,3}'

Example output

┌──(kali㉿localhost)-[~]
└─$ history | egrep '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n2
1 ssh kali@127.0.0.1

Check if SSH Public Key is on Shodan

Since the SSH public key is um, well, public, we can search for it on Shodan to see if it’s a known public server. https://www.shodan.io

Copy the public ssh key from the known_hosts file. It is the last portion of the line i.e.
AAAAC3NzaC1lZDI1NTE5AAAAIEjqG8/el8c669FxcvEw5mMfDRTDxsjgLiz44dCTtchs

|1|ma8KL2XrNYkNnknf68N4IuZ+c+I=|PmR+n2i0/epUGZZh2S+LB6OaowQ= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEjqG8/el8c669FxcvEw5mMfDRTDxsjgLiz44dCTtchs

Brute force

Since the address space for IPv4 is fairly small, and the private IP address space even smaller, brute forcing all the addresses is perfectly feasible.

Here is a quick example on how you would hash an IP address. Commands are taken from the above Stack Exchange link.

Take the salt and put it into a variable

key=`echo j2j9iv/GkPfnG9Yv4WzJsy/L1pc= | base64 -d | xxd -p`

Next we can run the following command to hash the result. The IP (127.0.0.1) is where we would want to enumerate the IP address.

echo -n "127.0.0.1" | openssl sha1 -mac HMAC -macopt hexkey:$key|awk '{print $2}' | xxd -r -p | base64

The output is PmR+n2i0/epUGZZh2S+LB6OaowQ= which is the correct hash.

Automating should be fairly simple.

A note on SSH ports. If the host is using a non standard ssh port, you will need to update the above command with the port, but the address needs to be wrapped in square brackets []

echo -n "[127.0.0.1]:2222" | openssl sha1 -mac HMAC -macopt hexkey:$key|awk '{print $2}' | xxd -r -p | base64

Use ssh-keyscan

A final way we can discover known-hosts, is by using ssh-keyscan. The man page says the following

ssh-keyscan is a utility for gathering the public SSH host keys of a number of hosts. It was designed to aid in building and verifying ssh_known_hosts files

ssh-keyscan uses non-blocking socket I/O to contact as many hosts as possible in parallel, so it is very efficient. The keys from a domain of 1,000 hosts can be collected in tens of seconds, even when some of those hosts are down or do not run sshd(8). For scanning, one does not need login access to the machines that are being scanned, nor does the scanning process involve any encryption.

Hosts to be scanned may be specified by hostname, address or by CIDR network range (e.g. 192.168.16/28). If a network range is specified, then all addresses in that range will be scanned.

This makes it super convenient to do a network scan using ssh-keyscan and then compare the public ssh keys with those in the known_hosts file.

Example:

ssh-keyscan 192.168.0.0/16

To scan all private IP ranges (RFC1912), we just run the scan with all three IP ranges

ssh-keyscan 192.168.0.0/16
ssh-keyscan 172.12.0.0/12
ssh-keyscan 10.0.0.0/8

Check for backdoored version of xz (CVE-2024-3094) (Ansible/Bash)

Info on the xc backdoor

https://www.openwall.com/lists/oss-security/2024/03/29/4

https://tukaani.org/xz-backdoor/

https://www.tenable.com/blog/frequently-asked-questions-cve-2024-3094-supply-chain-backdoor-in-xz-utils

Kostas on Twitter posted a helpful one-liner to check the xz version without running the actual command.

https://twitter.com/kostastsale/status/1773890846250926445

Versions 5.6.0 and 5.6.1 are backdoored.

Bash one liner

The following Bash commands were taken and modified from the above Twitter link

Here is a one liner that will check the version of xz binaries and return if they are safe or vulnerable. You’ll need to run this in a Bash shell. May have issues in sh.

for xz_p in $(type -a xz | awk '{print $NF}' ); do  if ( strings "$xz_p" | grep "xz (XZ Utils)" | grep '5.6.0\|5.6.1' ); then echo $xz_p Vulnerable; else echo $xz_p Safe ; fi ; done 

Ansible Playbooks

Here are two different Ansible Playbooks to check if the xz package(s) are backdoored.

This one uses the above Bash commands to check the xz binaries.

---
- name: Check if XZ tools are compromised
# https://twitter.com/kostastsale/status/1773890846250926445
  hosts: all

  tasks: 
    - name: Run Bash command
      shell : 
        for xz_p in $(type -a xz | awk '{print $NF}' ); do 
          if ( strings "$xz_p" | grep "xz (XZ Utils)" | grep '5.6.0\|5.6.1' ); 
            then echo $xz_p Vulnerable!; 
          else 
            echo $xz_p Safe ; 
          fi ; 
        done
      args: 
        executable: /bin/bash
      register: result

    - name: Show output
      ansible.builtin.debug:
        msg: "{{ result.stdout_lines }}"

The following playbook uses the package manager to check the xz version. On RHEL/Fedora this is the xc package. On Debian/Ubuntu, it is part of the liblzma5 package.

---
- name: Check if XZ tools are compromised
  hosts: all

  tasks:
    - name: Collect package info
      ansible.builtin.package_facts:
        manager: auto

    - name: Check if liblzma5 is vulnerable (Ubuntu/Debian)
      ansible.builtin.debug:
        msg: "Installed version of liblzma5/xz: {{ ansible_facts.packages['liblzma5'] | map(attribute='version') | join(', ') }} Vulnerable!"
      when: ('liblzma5' in ansible_facts.packages) and (ansible_facts.packages['liblzma5'][0].version.split('-')[0] is version('5.6.0', '==') or ansible_facts.packages['liblzma5'][0].version.split('-')[0] is version('5.6.1', '=='))

    - name: Check if xz is vulnerable (RHEL/Fedora/Rocky/Alma)
      ansible.builtin.debug:
        msg: "Installed version of xz: {{ ansible_facts.packages['xz'] | map(attribute='version') | join(', ') }} is vulnerable"
      when: ('xz' in ansible_facts.packages) and (ansible_facts.packages['xz'][0].version is version('5.6.0', '==') or ansible_facts.packages['xz'][0].version is version('5.6.1', '=='))

Handling Spaces in File Names on Linux

Using ls to parse file names is not recommended for multiple reasons

https://mywiki.wooledge.org/ParsingLs

Let’s say we have a directory with two files in it.

Helloworld.txt
Hello, world.txt

Now we want to loop over the files. If we use ls in our for loop,

for file in $(ls); do echo "$file" ; done

We receive the following output

Hello,
world.txt
Helloworld.txt

The space in “Hello, world.txt” is translated as a new line. This could break our script.

Here is a better way

for file in * ; do echo "$file" ; done

Helpful links

https://mywiki.wooledge.org/BashPitfalls

Harden SSH for AlmaLinux 9 (RHEL, Fedora)

These steps are taken from the following link. They have other guides for hardening Ubuntu, Debian etc.

https://www.sshaudit.com/hardening_guides.html#rocky9

You will need to become the root user, use either su – or sudo -i

First we need to regenerate the RSA and ED25519 keys

rm /etc/ssh/ssh_host_*
ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_rsa_key -N ""

Next, remove the small Diffie-Hellman moduli. The moduli file contains prime numbers and generators. Removing the smaller numbers should help increase security as it makes attempting to factor the private keys harder.

awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe
mv /etc/ssh/moduli.safe /etc/ssh/moduli

We can now specify which key exchange, ciphers, and algorithms to use.

Add the following to “/etc/crypto-policies/back-ends/opensshserver.config”

# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com
# hardening guide.
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256

Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr

MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com

HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256

RequiredRSASize 3072

CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256

GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-

HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256

PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256

Finally, restart the ssh server

systemctl restart sshd

Other helpful links

https://www.ssh.com/academy

https://www.redhat.com/en/blog/primes-parameters-and-moduli

https://security.stackexchange.com/questions/79043/is-it-considered-worth-it-to-replace-opensshs-moduli-file

Migrate CentOS 8 Stream to AlmaLinux 8

https://wiki.almalinux.org/documentation/migration-guide

Update CentOS 8 Stream

sudo dnf update -y

Download and run the almalinux-deploy script

curl -O https://raw.githubusercontent.com/AlmaLinux/almalinux-deploy/master/almalinux-deploy.sh
sudo bash almalinux-deploy.sh -d

You’ll need to run with the -d “downgrade” option if you are migrating from CentOS 8 Stream. https://github.com/AlmaLinux/almalinux-deploy/tree/master?tab=readme-ov-file#roadmap

You may need to remove packages if there are conflicts. On one instance, there were issues and I needed to remove grafana and llvm-compat-libs.

sudo yum remove grafana llvm-compat-libs

After those errors are fixed, rerun.

sudo bash almalinux-deploy.sh -d

Once the script finishes

sudo reboot

Once it comes back up, check the Linux version

cat /etc/*release

Example output

AlmaLinux release 8.9 (Midnight Oncilla)
AlmaLinux release 8.9 (Midnight Oncilla)
NAME="AlmaLinux"
VERSION="8.9 (Midnight Oncilla)"
ID="almalinux"
ID_LIKE="rhel centos fedora"
VERSION_ID="8.9"
PLATFORM_ID="platform:el8"
PRETTY_NAME="AlmaLinux 8.9 (Midnight Oncilla)"

Install Node.js 18 on AlmaLinux 8

List available Node.js versions available.

dnf module list nodejs
AlmaLinux 8 - AppStream
Name           Stream           Profiles                                     Summary
nodejs         10 [d][x]        common [d], development, minimal, s2i        Javascript runtime
nodejs         12 [x]           common [d], development, minimal, s2i        Javascript runtime
nodejs         14 [x]           common [d], development, minimal, s2i        Javascript runtime
nodejs         16 [x]           common [d], development, minimal, s2i        Javascript runtime
nodejs         18 [x]           common [d], development, minimal, s2i        Javascript runtime
nodejs         20 [x]           common [d], development, minimal, s2i        Javascript runtime

Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled

As we can see above, nodejs 18 is disabled. Enable it with

sudo dnf module enable nodejs:18

Now we can install with

sudo dnf install nodejs

You may need to uninstall older versions.

More space needed on the /boot filesystem. RHEL / Fedora / Alma / Rocky

Error Summary
-------------
Disk Requirements:
   At least 28MB more space needed on the /boot filesystem.

The above error is due to the /boot partition being out of space. We can fix this issue by removing older unused Linux kernels. You could also increase the disk space, but that is a little more involved.

First we need to list which kernels we have installed.

rpm -qa | grep kernel

Example output

[incredigeek@apache ~]$ rpm -qa | grep kernel
kernel-core-4.18.0-522.el8.x86_64
kernel-tools-4.18.0-529.el8.x86_64
kernel-modules-4.18.0-526.el8.x86_64
kernel-4.18.0-526.el8.x86_64
kernel-modules-4.18.0-529.el8.x86_64
kernel-4.18.0-522.el8.x86_64
kernel-4.18.0-529.el8.x86_64
kernel-core-4.18.0-529.el8.x86_64
kernel-devel-4.18.0-522.el8.x86_64
kernel-core-4.18.0-526.el8.x86_64
kernel-devel-4.18.0-529.el8.x86_64
kernel-tools-libs-4.18.0-529.el8.x86_64
kernel-devel-4.18.0-526.el8.x86_64
kernel-headers-4.18.0-529.el8.x86_64
kernel-modules-4.18.0-522.el8.x86_64

The kernel in bold is the one we will remove.

Next we remove erase the old kernel(s)/items.

sudo rpm -e kernel-4.18.0-522.el8.x86_64 kernel-core-4.18.0-522.el8.x86_64 kernel-devel-4.18.0-522.el8.x86_64 kernel-modules-4.18.0-522.el8.x86_64

And now we continue with our update

sudo dnf update

Helpful links.

https://www.cyberciti.biz/faq/installing-kernel-2-6-32-131-2-1-el6-x86_64-needs-8mb-on-boot-filesystem/

Ansible Playbook for Updating Linux (Debian/Ubuntu)

Video on using Ansible to Update Linux

The three steps to update a machine with Ansible

  1. Create Ansible Inventory/Hosts file
  2. Create Playbook
  3. Run Playbook

Create Inventory

The first thing we need to do is create an inventory file. This will contain a list of our servers along with the credentials.

touch hosts.txt

Now let’s encrypt the file with Ansible Vault.

ansible-vault encrypt hosts.txt

The file is now encrypted. To edit the file, we need to use `ansible-vault edit`.
If you want to, you can configure the hosts.txt file and then encrypt it when you are finished.

ansible-vault edit hosts.txt

Now add some hosts. In this example we add the local Kali machine, because why not. If you have Ubuntu servers, replace debian with ubuntu.

[debian]
kali ansible_host=127.0.0.1 ansible_ssh_user=kali ansible_ssh_port=22 ansible_ssh_password='kali pass' ansible_become_pass='kali sudo pass'

Add as many hosts as you need. For sake of simplicity, we are only adding one, and it is our localhost.

Create Playbook

Create a new playbook.

vi debian_update.yml

Put the following into the playbook. Edit as desired. Change hosts to match the above hosts in the inventory/hosts file.

---
- name: OS update
  hosts: debian
  gather_facts: yes
  become: yes

  tasks:
    - name: dist-upgrade
      ansible.builtin.apt:
        upgrade: dist
        update_cache: yes
      register: upgrade_result

    - name: Check if a reboot is required
      ansible.builtin.stat:
        path: /var/run/reboot-required
        get_checksum: no
      register: reboot_required_file

    - name: Reboot the server (if required).
      ansible.builtin.reboot:
      when: reboot_required_file.stat.exists
      register: reboot_result

    - name: Remove unneeded dependencies
      ansible.builtin.apt:
        autoremove: yes
      register: autoremove_result

    - name: Print errors if upgrade failed
      ansible.builtin.debug:
        msg: |
          Upgrade Result: {{ upgrade_result }}
          Reboot Result: {{ reboot_result }}
          Autoremove Result: {{ autoremove_result }}

A couple of notes

  1. On the 3rd line it defines which group to run this playbook against. In this case debian.
  2. This will check if a reboot is needed and reboot the machine. Reboots are usually needed when the kernel is updated
  3. The 5th line contains `become: yes` this means that the playbook will use sudo. You can specify the sudo password in the hosts file `ansible_become_pass=sudopass` or with the -k or –ask-become options
  4. The update and reboot are natively built into Ansible. Hence the ansible.builtin.

Run Playbook

Now that we have our inventory and playbook, we can upgrade our machines.

ansible-playbook debian_update.yml -i hosts.ini --ask-vault-password

Tip! If you have not specified a “ansible_ask_become” password (that is the sudo password), you can specify it with the -k or –ask-become options.

Run sudo Command over SSH. Single line.

When running an SSH command that uses sudo, something like

ssh admin@192.168.1.20 "sudo apt -y update && sudo apt -y upgrade"

You may receive the following error.

sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
sudo: a password is required

To work around this, you can use the -t option. -q is not needed, but makes thing quieter.

ssh -qt admin@192.168.1.20 "sudo apt -y update && sudo apt -y upgrade "

The sudo password will also be hidden.

https://unix.stackexchange.com/questions/134155/how-do-you-keep-the-password-hidden-when-invoked-during-the-su-command

https://stackoverflow.com/questions/233217/how-to-pass-the-password-to-su-sudo-ssh-without-overriding-the-tty