Linux privilege escalation hunting

Four audits: SUID binary baseline comparison, sudoers misconfiguration scan, capability enumeration, and container escape precondition checks.

SUID binary audit

Hypothesis: a SUID binary has been added or modified outside the system baseline, providing a privilege escalation path.

Data source: filesystem scan.

# generate current SUID/SGID inventory
find / -type f \( -perm -4000 -o -perm -2000 \) -ls 2>/dev/null | \
  awk '{print $3, $5, $11}' | sort > /tmp/suid_current.txt

# compare against baseline
# to create baseline: cp /tmp/suid_current.txt /etc/security/suid_baseline.txt
if [ -f /etc/security/suid_baseline.txt ]; then
    echo "=== new or changed SUID/SGID binaries ==="
    diff /etc/security/suid_baseline.txt /tmp/suid_current.txt | grep '^>'
    echo "=== removed SUID/SGID binaries ==="
    diff /etc/security/suid_baseline.txt /tmp/suid_current.txt | grep '^<'
fi

Beyond new entries, flag any binary in the current inventory whose name matches known GTFOBins escalation paths regardless of when it appeared:

# binaries that allow shell escape or arbitrary command execution when run as SUID
gtfobins_list="find vim vi nano perl python python3 ruby awk gawk nawk mawk \
  bash sh ash dash ksh zsh less more man env tee tar curl wget lua php node"

find / -type f -perm -4000 2>/dev/null | while read f; do
    name=$(basename "$f")
    for b in $gtfobins_list; do
        if [ "$name" = "$b" ]; then
            echo "GTFOBins SUID match: $f"
        fi
    done
done

Most Linux distributions do not ship these binaries with SUID. Findings here are post-installation changes: application installers, misconfigured provisioning scripts, or manual additions. Each finding warrants a check of who owns the binary and when the permission was last modified (stat -c "%y %U" <path>).

sudoers misconfiguration scan

Hypothesis: sudo rules allow a lower-privileged user to execute commands that permit shell escape or arbitrary file access.

Data source: /etc/sudoers and /etc/sudoers.d/.

# parse all sudo rules; flag NOPASSWD, unrestricted ALL, wildcard arguments
grep -h -v '^[[:space:]]*#\|^$\|^Defaults' \
  /etc/sudoers /etc/sudoers.d/* 2>/dev/null | \
  grep -v 'Cmnd_Alias\|User_Alias\|Host_Alias\|Runas_Alias' | \
  while IFS= read -r line; do
    flag=""
    echo "$line" | grep -q 'NOPASSWD'          && flag="$flag NOPASSWD"
    echo "$line" | grep -qE '\(ALL.*\).*ALL'    && flag="$flag UNRESTRICTED"
    echo "$line" | grep -qF '*'                 && flag="$flag WILDCARD"
    [ -n "$flag" ] && echo "[$flag ] $line"
  done

# separately flag env_keep preserving LD_PRELOAD or LD_LIBRARY_PATH
grep -h 'env_keep' /etc/sudoers /etc/sudoers.d/* 2>/dev/null | \
  grep -i 'LD_PRELOAD\|LD_LIBRARY'

NOPASSWD entries warrant documentation of purpose and a check that the account is still active and scoped. Wildcard argument rules merit review of the permitted binary against GTFOBins: if the binary appears there, the rule may allow unrestricted escalation regardless of the intended scope.

Capability audit

Hypothesis: a binary or running process holds Linux capabilities beyond its functional requirement, allowing privilege escalation without a SUID bit.

Data source: filesystem and /proc.

# file capabilities: binaries with elevated capabilities
getcap -r / 2>/dev/null | \
  grep -E 'cap_setuid|cap_sys_ptrace|cap_dac_override|cap_sys_admin|cap_net_raw'
# process capabilities: find running processes with elevated effective capability sets
# CapEff in /proc/<pid>/status is a hex bitmask
# cap_setuid = bit 7 (0x80), cap_sys_admin = bit 21 (0x200000)
# cap_sys_ptrace = bit 19 (0x80000), cap_dac_override = bit 1 (0x2)

for status in /proc/[0-9]*/status; do
    capeff=$(grep '^CapEff:' "$status" 2>/dev/null | awk '{print $2}')
    [ -z "$capeff" ] && continue
    capval=$((16#$capeff))
    if (( (capval & 0x80) != 0 || (capval & 0x200000) != 0 )); then
        pidnum=$(echo "$status" | grep -oP '\d+')
        comm=$(cat "/proc/$pidnum/comm" 2>/dev/null)
        user=$(stat -c '%U' "/proc/$pidnum" 2>/dev/null)
        printf 'pid=%-6s comm=%-20s user=%-15s CapEff=%s\n' \
          "$pidnum" "$comm" "$user" "$capeff"
    fi
done

cap_setuid on a Python or Perl interpreter is functionally equivalent to a SUID root shell. cap_sys_admin on any process inside a container is a container escape precondition. Either finding from a process running as a non-root user, outside an expected service context, is worth immediate investigation.

Container escape precondition check

Hypothesis: a container is running with configuration that allows escape to the host.

Run from inside the container to assess the escape surface:

# confirm container context
if [ -f /.dockerenv ] || grep -q 'docker\|lxc\|containerd' /proc/1/cgroup 2>/dev/null; then
    echo "[context] running inside container"
fi

# privileged mode: cap_sys_admin present in effective capabilities
capeff=$(grep '^CapEff:' /proc/self/status | awk '{print $2}')
capval=$((16#$capeff))
if (( (capval & 0x200000) != 0 )); then
    echo "[HIGH] cap_sys_admin present: container likely running --privileged"
fi

# Docker socket exposure
if [ -S /var/run/docker.sock ]; then
    echo "[HIGH] Docker socket accessible: host daemon reachable from container"
fi

# writable cgroup release_agent
agent=$(find /sys/fs/cgroup -name release_agent 2>/dev/null | head -1)
if [ -n "$agent" ] && [ -w "$agent" ]; then
    echo "[HIGH] cgroup release_agent writable: $agent"
fi

# host PID namespace sharing
host_pid=$(readlink /proc/1/ns/pid 2>/dev/null)
self_pid=$(readlink /proc/self/ns/pid 2>/dev/null)
[ -n "$host_pid" ] && [ "$host_pid" = "$self_pid" ] && \
  echo "[HIGH] sharing host PID namespace"

# host network namespace sharing
host_net=$(readlink /proc/1/ns/net 2>/dev/null)
self_net=$(readlink /proc/self/ns/net 2>/dev/null)
[ -n "$host_net" ] && [ "$host_net" = "$self_net" ] && \
  echo "[HIGH] sharing host network namespace"

# sensitive host path mounts
mount | awk '{print $3}' | grep -E '^/etc$|^/var/run$|^/proc$|^/sys$' | \
  while read mp; do echo "[MEDIUM] host path mounted at $mp"; done

Any HIGH flag is a concrete escape path. Privileged mode and Docker socket exposure are the most frequently observed. Cgroup release_agent writability and host namespace sharing appear more often in older Kubernetes node configurations and development environments where isolation was relaxed for convenience.