Signal9Signal9 — home
online
all lessons

SSH Persistence & Backdoors

Intermediate35 min6 steps
What you'll learn
  • Audit every user's ~/.ssh/authorized_keys for a planted public key that grants passwordless re-entry
  • Read sshd's effective configuration to spot dangerous options like PermitRootLogin yes or a hijacked AuthorizedKeysFile
  • Remember to check the sshd_config.d drop-in directory, not just the main config file
  • Find attacker key material and use known_hosts to see where this box has connected out
  1. Why SSH is the attacker's favorite back door

    Frame SSH persistence: changing the password doesn't help if the lock accepts a second key.

    An attacker who breaks into a box wants to come back quietly, even after you notice the intrusion and reset every password. SSH is the perfect vehicle: it's almost always running, almost always allowed through the firewall, and it offers key-based login — authentication with no password at all.

    That's the whole game. With key auth, your account trusts anyone holding the matching private key. The list of keys allowed into an account lives in a single file: ~/.ssh/authorized_keys. An attacker who can write one line into that file has a permanent, passwordless way back in.

    Think of authorized_keys as a ring of house keys you've handed out. A burglar who gets inside once can quietly cut themselves a copy and add it to the ring. Later you change the locks — you reset your password — but the burglar's key still works, because the door was built to honor every key on the ring, not just yours.

  2. Audit authorized_keys for EVERY user

    Loop over all accounts, not just root, and read each line of authorized_keys.

    Every user that can be logged into via SSH has their own authorized_keys file. Human accounts live under /home/<user>/.ssh/; root's lives at /root/.ssh/. A backdoor key in any of them is a way in — so you must check them all, not just root.

    Each line in the file is one allowed public key: a type (ssh-ed25519, ssh-rsa), the key blob itself, and a free-text comment at the end. That comment is usually user@hostname — a breadcrumb showing where the key was generated.

    sudo grep -r "" /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null
    /home/webadmin/.ssh/authorized_keys:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK8w webadmin@workstation
    /home/webadmin/.ssh/authorized_keys:ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB7Qx9 root@kali
    /root/.ssh/authorized_keys:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINv2 backup@ansible-ctl
    -r
    Recurse — but here it mainly lets one command read across every path the glob expands to.
    ""
    An empty pattern matches every line, so grep prints each file's contents WITH the filename prefixed — exactly what you want when sweeping many files.
    2>/dev/null
    Discard 'No such file' errors for users who simply have no .ssh directory, so the real results aren't buried in noise.
    The filename prefix on each line tells you which account trusts the key. Two of webadmin's keys look normal — but the second, commented `root@kali`, is the tell: Kali is a penetration-testing distro, and no legitimate teammate generates the company's keys as root@kali. That line is the burglar's cut key. Notice the attacker appended it to webadmin's existing file — the genuine webadmin@workstation key is still there, so nothing looks broken to the user.

    Check yourself

    An attacker adds their key to a compromised box. Why is appending it to an existing user's authorized_keys (rather than replacing the file) the stealthier move?

  3. Audit sshd_config for dangerous options

    Read the effective sshd settings and flag options that weaken or backdoor authentication.

    Beyond planting a key, an attacker can weaken the SSH daemon itself by editing /etc/ssh/sshd_config. A handful of directives turn a hardened daemon into an open door:

    • `PermitRootLogin yes` — lets root log in directly over the network, so a stolen root password or key is instant full control.
    • `PermitEmptyPasswords yes` — accepts accounts with a blank password. Pair it with a created no-password account and that's a backdoor with no key needed at all.
    • `AuthorizedKeysFile` pointed somewhere world-writable (e.g. /tmp/keys/%u) — now anyone on the box can grant anyone SSH access by dropping a key there.
    • `ForceCommand` — runs a command on every login. Legitimately used to restrict sessions; weaponized, it can launch a reverse shell each time someone connects.

    Don't read the raw file by eye, though — it's full of comments and defaults. Ask sshd what it actually resolved to with sshd -T.

    sudo sshd -T | grep -iE 'permitrootlogin|permitemptypasswords|authorizedkeysfile|forcecommand'
    permitrootlogin yes
    permitemptypasswords no
    authorizedkeysfile /tmp/.ssh-keys/%u
    forcecommand none
    -T
    Test mode: print the FULL effective configuration after merging the main file, all drop-ins, and built-in defaults — the real running config, not what a comment claims.
    grep -i
    Case-insensitive — sshd normalizes directive names to lowercase in -T output, but -i means your search matches regardless.
    -E
    Extended regex so the | alternations let you pull several directives in one pass.
    Two findings. `permitrootlogin yes` means root can log in straight over the network — most hardened systems set this to no or prohibit-password. And `authorizedkeysfile /tmp/.ssh-keys/%u` is the real backdoor: it moves the trusted-keys list into /tmp, a world-writable directory. Any local user (or any process the attacker controls) can now create /tmp/.ssh-keys/root and add their own key — granting SSH access without ever touching /root/.ssh/. The %u expands to the username being logged into.

    Check yourself

    Why is pointing AuthorizedKeysFile at a path under /tmp a backdoor even if no malicious key is present yet?

  4. Don't forget sshd_config.d drop-ins

    Check the drop-in directory, where a single small file can override the main config.

    Modern OpenSSH reads /etc/ssh/sshd_config and then every *.conf file in /etc/ssh/sshd_config.d/. Because SSH honors the first value it sees for most directives, a drop-in included near the top of the main config can silently win over what you read lower down. This is a favorite hiding spot: you audit the big, familiar sshd_config, see nothing alarming, and miss a five-line file in the drop-in directory that flips PermitRootLogin back to yes.

    sudo grep -riE 'permitrootlogin|authorizedkeysfile|forcecommand|permitemptypasswords' /etc/ssh/sshd_config.d/
    /etc/ssh/sshd_config.d/99-cloud-init.conf:PermitRootLogin yes
    /etc/ssh/sshd_config.d/zz-update.conf:ForceCommand /usr/local/bin/sync-agent
    -r
    Recurse through the whole drop-in directory — you don't know in advance how many files an attacker left.
    -i
    Case-insensitive: directives are not case-sensitive, so PermitRootLogin and permitrootlogin both match.
    -E
    Extended regex for the | alternation, pulling all four dangerous directives in one sweep.
    Here the backdoor was never in the main file — it's in two drop-ins. 99-cloud-init.conf re-enables direct root login, and zz-update.conf adds a ForceCommand pointing at /usr/local/bin/sync-agent, an innocuous-sounding binary that runs on every login. Confirm by checking what sshd -T actually resolved (from the previous step), then inspect that binary — a real C2 implant often masquerades behind a boring 'agent' or 'update' name.
  5. Hunt key material and outbound connections

    Find attacker-generated keys and read known_hosts to see where this box has reached OUT.

    Two more places tell a story. First, attackers often generate a key pair on the victim with ssh-keygen — to authenticate onward to other machines from this one. That leaves private/public key files (id_*) in .ssh directories, sometimes with odd names or timestamps that don't match the account's history.

    Second, ~/.ssh/known_hosts records every host this box has connected out to. For a server that should only ever receive connections, an entry there is a red flag: it can reveal lateral movement (the attacker pivoting deeper into your network) or a C2 hop.

    sudo ls -la /home/*/.ssh/ /root/.ssh/ 2>/dev/null
    /home/webadmin/.ssh/:
    total 24
    -rw-r--r-- 1 webadmin webadmin  1100 Mar 02 09:14 authorized_keys
    -rw------- 1 webadmin webadmin   411 Jun 23 02:47 id_ed25519
    -rw-r--r-- 1 webadmin webadmin    98 Jun 23 02:47 id_ed25519.pub
    -rw-r--r-- 1 webadmin webadmin   444 Jun 23 02:51 known_hosts
    
    /root/.ssh/:
    total 8
    -rw------- 1 root root 399 Jan 14 11:02 authorized_keys
    -l
    Long listing — exposes owner, permissions, size, and the modification time you'll use to spot freshly planted files.
    -a
    Include dotfiles. Inside .ssh nearly everything is normal-named, but -a ensures nothing hidden is skipped.
    An id_ed25519 private key in webadmin's directory, created Jun 23 02:47 — months after the account's authorized_keys (Mar 02) — is suspicious. webadmin is a service login that shouldn't be generating new keys at 2:47 AM. The matching known_hosts, written four minutes later, says this box then connected out somewhere using that fresh key. That sequence — generate key, then reach out — is the signature of lateral movement.

    Example — not runnable on your lab box

    sudo cat /home/webadmin/.ssh/known_hosts
    198.51.100.42 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0r9mQ
    198.51.100.77 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKp3xZ2
    Two hosts this machine reached out to: 198.51.100.42 and 198.51.100.77. If this server's job is only to accept SSH, it has no business initiating SSH to anything — so these are your next two boxes to investigate for the attacker pivoting through them. known_hosts is append-only in practice and survives even when shell history is wiped, which makes it a durable record of where the intruder went next.

    Check yourself

    On a server that is only supposed to receive SSH connections, why is any entry in a user's known_hosts file worth investigating?

  6. Try it in your live terminal

    Run the SSH-persistence audit yourself on the real sandbox to your right.

    You have a real root shell. Run this audit in order — the same sweep you'd do on a 'did they leave a way back in?' call after any SSH-exposed box is suspected. Every command here is read-only: it inspects, it changes nothing. There may be nothing malicious on this box; the goal is to make the moves reflexive.

    sudo grep -r "" /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null
    Every trusted key for every account, with the filename prefix showing which user trusts it. Read each comment — does this person/host belong?
    sudo sshd -T | grep -iE 'permitrootlogin|permitemptypasswords|authorizedkeysfile|forcecommand'
    The effective daemon config. Watch for root login allowed, empty passwords, an AuthorizedKeysFile outside the user's own home, or any ForceCommand.
    sudo grep -riE 'permitrootlogin|authorizedkeysfile|forcecommand|permitemptypasswords' /etc/ssh/sshd_config.d/ 2>/dev/null
    The drop-in directory the main config pulls in. A small file here can override everything — don't skip it.
    sudo ls -la /home/*/.ssh/ /root/.ssh/ 2>/dev/null
    List every .ssh directory. Look for recently-created id_* key pairs and a known_hosts that shouldn't exist on a receive-only server, especially with a tight cluster of recent timestamps.
Recap
  • A planted key in authorized_keys is passwordless re-entry that survives every password reset — resetting credentials is not incident response.
  • Audit EVERY account (loop over /home/* and /root), not just root — attackers prefer low-privilege accounts and append to existing key files to stay invisible.
  • Trust sshd -T over the raw config, and always sweep /etc/ssh/sshd_config.d/ — a drop-in can override the file you read.
  • Fresh id_* keys and known_hosts entries reveal where the box reached OUT — likely lateral movement or C2, and your next leads to chase.
Where to look → what for → why it's a backdoor
LocationWhat to look forWhy it's a backdoor
~/.ssh/authorized_keys (every user + root)An unfamiliar key, esp. a comment like root@kali; lines appended to existing filesAny listed public key = passwordless login for whoever holds the private key
sshd -T → permitrootloginyesDirect network root login — a stolen root credential is instant full control
sshd -T → authorizedkeysfileA world-writable path like /tmp/...Anyone on the box can grant anyone SSH access by dropping a key there
sshd -T → forcecommand / permitemptypasswordsAny ForceCommand; permitemptypasswords yesForceCommand runs on every login (reverse shell); empty passwords = no auth at all
/etc/ssh/sshd_config.d/*Drop-ins re-enabling dangerous directivesOverrides the main file you audited — the classic 'config looked clean' miss
~/.ssh/id_*Recently created key pairs on a service accountAttacker-generated key to pivot onward from this host
~/.ssh/known_hostsOutbound hosts (e.g. 198.51.100.x)Shows where the box connected OUT — lateral movement or C2 hops