Skip to content
All posts
miscrevised January 27, 20266 min read

Linux privilege escalation: the shell is the middle of the challenge

A CTF that drops you on a box as a low-privileged user has only started. The enumeration order that finds the way up fast - SUID binaries, sudo rules, cron, capabilities, writable PATH - and how each one becomes root.

Half of all boot2root challenges, and a surprising number of web and pwn challenges, end the same way: you get a shell as www-data or some throwaway user, and the flag is in /root/flag.txt. The shell is not the finish line. It is the start of a second, entirely separate puzzle whose answer is almost always a misconfiguration the author left on purpose.

The good news is that Linux privilege escalation is not creative work. It is enumeration followed by pattern-matching against a small catalogue of misconfigurations. Run the checks in the right order, recognise what comes back, and the path up reveals itself. This article is that order, and what each finding turns into.

First: know where you are

Before hunting for a way up, establish the ground truth - you usually arrive here from a command injection or a web shell, and what that gave you shapes everything. Thirty seconds here saves you from chasing a vector that does not exist on this kernel or this user.

id                      # who am I, and what groups do I have?
sudo -l                 # what can I run as another user? (asks for a password
                        #   unless NOPASSWD - but the list itself is often shown)
uname -a; cat /etc/os-release   # kernel and distro, for kernel-exploit hunting
cat /etc/passwd         # who else exists; any UID 0 besides root?
ls -la /home/*          # other users' homes, sometimes world-readable
hostname; ip a          # am I in a container? a 172.17.x address is a Docker tell
The single highest-value command here is sudo -l. It frequently hands you the whole escalation on its own.

The five checks that solve most boxes

1. SUID and SGID binaries

A SUID binary runs with the privileges of its owner, not of you. When the owner is root, the binary is a small piece of root you are allowed to run - and if that binary can be talked into executing a command, reading a file, or spawning a shell, those actions happen as root.

# Find files with the SUID bit (4000) and the SGID bit (2000).
find / -perm -4000 -type f 2>/dev/null      # SUID
find / -perm -2000 -type f 2>/dev/null      # SGID
find / -perm -u=s -type f 2>/dev/null        # equivalent, clearer form
Discard the usual suspects (passwd, sudo, mount, su). What you want is the unusual entry: a copy of find, vim, python, or a custom binary the author added.

Once you have an unexpected SUID binary, look it up on GTFOBins. That project catalogues, for each common binary, the exact incantation that turns it into a shell or a file read when it is SUID. A SUID find becomes root with find . -exec /bin/sh -p ; -quit. A SUID python becomes root with python -c 'import os; os.execl("/bin/sh","sh","-p")'. The -p matters - it stops the shell from dropping the elevated privileges.

2. sudo rules

If sudo -l shows you may run a command as root, GTFOBins again knows the escape. Almost any program that can run a subcommand, open an editor, or read a file becomes a root shell through sudo: sudo vim -c ':!/bin/sh', sudo awk 'BEGIN{system("/bin/sh")}', sudo less /etc/profile then !/bin/sh. Even a seemingly harmless sudo tar or sudo tcpdump has a documented path, because both can run a command as a side effect.

3. Cron jobs

Scheduled tasks run as whoever owns them, usually root, and on a schedule you can wait out. The vulnerability is a cron job that runs a script you can write to, or that calls a program by an unqualified name so you can shadow it.

cat /etc/crontab; ls -la /etc/cron.*     # system cron
cat /var/spool/cron/crontabs/* 2>/dev/null

# The winning condition: a root cron job whose script you can edit.
find / -writable -type f 2>/dev/null | grep -vE '^/proc|^/sys'
# If a root job runs /opt/backup.sh and you can write to it, append a
# reverse shell or a chmod +s /bin/bash and wait for the next tick.
Watch a suspected cron job actually fire with a process monitor like pspy - it shows root processes appearing on schedule without needing to read the crontab.

4. Linux capabilities

Capabilities are SUID's finer-grained cousin: instead of granting all of root, a binary can be granted one specific power. That sounds safer and often is not, because a few capabilities are equivalent to root on their own.

getcap -r / 2>/dev/null

# cap_setuid on python is game over:
#   python -c 'import os; os.setuid(0); os.system("/bin/sh")'
# cap_dac_read_search reads any file regardless of permissions.
# cap_sys_admin is close to full root.
GTFOBins lists capability escapes alongside the SUID ones. cap_setuid+ep on an interpreter is the classic planted vector.

5. A writable PATH, or a writable service

When a root-owned program calls another program by name rather than by full path - system("service ...") inside a SUID binary, say - the shell searches $PATH to find it. If you can prepend a directory you control to $PATH, you decide which service runs. Write a malicious service script, put its directory first, and trigger the program.

# A SUID binary that calls "ps" without a path:
echo -e '#!/bin/sh\n/bin/sh -p' > /tmp/ps
chmod +x /tmp/ps
export PATH=/tmp:$PATH
/opt/the-suid-binary        # now its internal ps is your shell

Reading the box like a script

Doing these checks by hand is fine, but the point of a Linux enumeration script is that it runs all of them at once and highlights the anomalies. LinPEAS and linux-smart-enumeration encode exactly the catalogue above plus dozens of rarer vectors, and they colour-code findings by how likely each is to matter. In a CTF, run one first, read its red-and-yellow lines, and only then start thinking.

The reason to know the manual checks anyway is that the automated scripts flag *candidates*, not answers. LinPEAS telling you a binary has an odd capability means nothing until you know that cap_setuid on an interpreter is a root shell. The script finds the door; you still have to know it is a door.

When nothing is misconfigured: the kernel

Occasionally the userland is clean and the intended path is a kernel exploit. This is less common in modern CTFs because kernel exploits are version-fragile and crash boxes, but an old kernel is a real signal. Match uname -r against known local-root exploits - Dirty COW (CVE-2016-5195) for older kernels, Dirty Pipe (CVE-2022-0847) for 5.8 through 5.16, PwnKit (CVE-2021-4034 in polkit's pkexec) for almost any distro of that era. PwnKit in particular is userland and reliable enough to be worth trying early when a SUID pkexec is present.

The order, on one screen

  1. id, sudo -l, uname -a, /etc/passwd, your groups.
  2. SUID/SGID binaries → GTFOBins for anything unusual.
  3. sudo rules → GTFOBins, and check the sudo version.
  4. Cron jobs → any root job running a file or PATH name you control.
  5. Capabilities → getcap -r /, watch for cap_setuid on interpreters.
  6. Writable PATH entries and writable service scripts.
  7. Only if all else fails: match the kernel to a known local-root CVE.