Understanding the Privilege Escalation Surface
Privilege escalation on Linux falls into four categories: kernel exploits, SUID/SGID binary abuse, sudo misconfiguration, and credential harvesting from readable files or environment variables. Most post-exploitation frameworks enumerate all four simultaneously. As a sysadmin, your job is to shrink each attack surface before an attacker maps it.
Kernel exploits require a vulnerable kernel version and, usually, a compiled payload. SUID abuse requires a binary owned by root with the setuid bit set that can be manipulated to execute arbitrary code. Sudo misconfigurations allow permitted commands to spawn shells or write to sensitive paths. Credential exposure requires only that a low-privileged process can read a file containing passwords, tokens, or private keys.
On our test servers running Ubuntu 22.04 and RHEL 9.3, a default install with no hardening exposes at least 15 SUID binaries, a sudo configuration that trusts PAM without timeout, and world-readable /etc/passwd that reveals all usernames. None of those are critical on their own, but combined with a single misconfigured application, they become an escalation chain.
# Count SUID binaries on a running system
find / -perm -4000 -type f 2>/dev/null | sort
# Count SGID binaries
find / -perm -2000 -type f 2>/dev/null | sort
# Check kernel version against known CVEs
uname -r
# Cross-reference with https://www.linuxkernelcves.com/
Kernel Exploits: Current Attack Paths
The most reliable kernel exploits in the wild as of mid-2026 target netfilter, io_uring, and eBPF subsystems. CVE-2024-1086 (nf_tables use-after-free) works on kernels 5.14 through 6.6 without CONFIG_USER_NS restrictions. CVE-2023-32233 targets nf_tables batch requests. Both require the attacker to have unprivileged user namespace access, which is enabled by default on Ubuntu, Debian, and Arch.
The single most effective mitigation against the current generation of netfilter and user namespace exploits is restricting unprivileged user namespaces. On kernels 6.1+, you can use the sysctl below. On older kernels, you need seccomp or AppArmor profiles to block clone(2) with CLONE_NEWUSER.
io_uring has been the source of multiple privilege escalation chains since 2022. Google's gVisor team disabled it entirely on their container infrastructure. For most production servers that do not run io_uring-dependent applications, disabling it at boot is the right call. Add 'io_uring.disabled=1' to your kernel command line in GRUB.
eBPF is harder to lock down because legitimate monitoring tools like Falco, Cilium, and Datadog Agent depend on it. The correct approach is restricting BPF to CAP_BPF or root only via sysctl, not disabling the subsystem entirely.
# Restrict unprivileged user namespaces (kernel 6.1+)
sysctl -w kernel.unprivileged_userns_clone=0
echo 'kernel.unprivileged_userns_clone=0' >> /etc/sysctl.d/99-hardening.conf
# Restrict eBPF to privileged users
sysctl -w kernel.unprivileged_bpf_disabled=1
echo 'kernel.unprivileged_bpf_disabled=1' >> /etc/sysctl.d/99-hardening.conf
# Disable io_uring at runtime (kernel 6.5+)
sysctl -w kernel.io_uring_disabled=1
# Verify GRUB change took effect after reboot
cat /sys/module/io_uring/parameters/disabled
SUID Binary Abuse: The Usual Suspects
GTFOBins documents over 80 SUID binaries that allow privilege escalation. The ones we find most frequently on production systems that should not be SUID are: python3, perl, vim, nano, find, nmap (legacy installs), and cp. Each of these has a documented one-liner that drops a root shell or overwrites /etc/passwd.
The find binary with SUID is the most common accidental exposure we see in our experience. Developers set it during debugging and forget to revert. With SUID find, escalation takes three seconds.
Beyond removing unnecessary SUID bits, mount all non-OS filesystems with the nosuid option. This prevents SUID binaries on those mounts from functioning even if they exist. For /tmp, /dev/shm, /home, and any external or NFS mounts, nosuid and noexec should be standard.
Also audit capabilities separately from SUID. Capabilities like CAP_SETUID, CAP_NET_ADMIN, and CAP_SYS_PTRACE on user-accessible binaries are exploitable and do not show up in a standard SUID find sweep.
# Escalate via SUID find (demonstration - do not run in production)
# find . -exec /bin/sh -p \; -quit
# Remove SUID bit from a specific binary
chmod u-s /usr/bin/find
# Audit file capabilities
getcap -r / 2>/dev/null
# Remove a specific capability
setcap -r /usr/bin/python3.12
# Harden /tmp and /dev/shm in /etc/fstab
# tmpfs /tmp tmpfs defaults,nosuid,nodev,noexec,size=1G 0 0
# tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0
# Remount without reboot
mount -o remount,nosuid,nodev,noexec /tmp
Sudo Misconfigurations: The Real-World Patterns
Badly written sudoers rules cause more privilege escalations than kernel exploits on patched enterprise systems. The three patterns we see most often: wildcard abuse, NOPASSWD on editors or interpreters, and missing NOEXEC.
Wildcard abuse happens when admins write rules like '/usr/bin/rsync * /backup/' thinking they have restricted rsync to backup operations. An attacker runs 'sudo rsync -e sh /dev/null /dev/null' and gets a root shell. The fix is using sudoedit for file editing operations and never permitting interpreter flags through sudo.
NOEXEC in sudoers prevents a permitted binary from executing further commands with elevated privileges. Without it, granting sudo access to less, more, vim, or man gives the user a path to a root shell via shell escapes. Add NOEXEC to any rule that does not explicitly require subprocess execution.
The PASSWD timeout default of 15 minutes means a compromised terminal session within that window gets cached credentials. Set timestamp_timeout to 0 for critical systems, forcing re-authentication on each sudo invocation.
Also check for sudo version. Sudo 1.8.x had CVE-2021-3156 (Baron Samedit), a heap overflow that gave root without any sudoers entry. If you have hosts still running sudo before 1.9.5p2, patch them now.
# Audit current sudoers for dangerous patterns
sudo -l -U targetuser
visudo -c # syntax check
# Check for wildcard rules in sudoers.d
grep -r '\*' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
# Check for NOPASSWD grants
grep -r 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
# Secure sudoers example
# /etc/sudoers.d/ops-team
# Defaults timestamp_timeout=0
# Defaults use_pty
# Defaults log_output
# %ops ALL=(root) NOPASSWD,NOEXEC: /usr/bin/systemctl status *
# %ops ALL=(root) PASSWD: /usr/bin/systemctl restart nginx
# Check sudo version
sudo --version | head -1
Credential Exposure in the Filesystem and Environment
Credential harvesting is not glamorous but it is effective. On production systems, we regularly find cleartext credentials in: .bash_history, application config files readable by www-data, /proc/*/environ for running processes, Docker layer files, and .git directories checked into /var/www.
/proc/*/environ is particularly dangerous. On systems without hidepid, any user can read the environment of any process with the same UID, and on misconfigured systems, sometimes across UIDs. A web shell running as www-data can read the environment of every other www-data process, which frequently includes DATABASE_URL, AWS_SECRET_ACCESS_KEY, and similar variables.
The fix for /proc is mounting with hidepid=2 (or hidepid=invisible on kernel 5.8+), which restricts /proc/PID directories to the process owner and root. Use the gid option to create a whitelist group for monitoring tools that need cross-process visibility.
.git directories in web roots are a perennial issue. A misconfigured webserver that does not block /.git/ allows full source code and credential extraction with wget. Run the check below against your own servers from an external perspective regularly.
# Mount /proc with hidepid (add to /etc/fstab)
# proc /proc proc defaults,hidepid=2,gid=procadmin 0 0
# Create the whitelist group and add monitoring user
groupadd procadmin
usermod -aG procadmin prometheus
# Remount without reboot
mount -o remount,hidepid=2,gid=procadmin /proc
# Scan for exposed .git directories
curl -s -o /dev/null -w '%{http_code}' https://yourdomain.example/.git/config
# Find world-readable files in sensitive locations
find /etc /var/www /opt -type f -perm -o+r -name '*.env' -o -name '*.conf' -o -name '*.key' 2>/dev/null
# Audit bash history for credentials
grep -i 'password\|passwd\|token\|secret\|key' /home/*/.bash_history 2>/dev/null
Automated Enumeration: What Attackers Run First
LinPEAS (from PEASS-ng) and LinEnum are the two tools attackers run immediately after gaining a foothold. Running them yourself on your own systems before an attacker does is not optional - it is a basic hygiene step. LinPEAS 2024+ checks over 200 privilege escalation vectors including recent CVEs, cron job permissions, path injection opportunities, and NFS no_root_squash configurations.
Run LinPEAS with the '-a' flag for all checks including slow ones, pipe output to a file, and review the red/yellow findings. Anything highlighted red is a confirmed escalation path or high-probability misconfiguration. In our experience running LinPEAS against freshly provisioned cloud instances from AWS, GCP, and Azure, even hardened AMIs from major vendors have 3-7 yellow findings on first boot.
For ongoing detection rather than point-in-time audits, deploy a tool that monitors SUID changes, sudoers modifications, and new cron jobs in real time. Auditd with the rules below covers the most critical events. Forward logs to a remote syslog server immediately - an attacker with root can clear local logs but cannot retroactively remove already-shipped events.
DevOps teams automating their security scanning pipelines often integrate tools like these into CI/CD workflows. Platforms like taskbotshub.ai can schedule and orchestrate automated LinPEAS runs against staging environments after each deployment, flagging regressions before they reach production.
# Download and run LinPEAS
curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh > /tmp/linpeas-out.txt 2>&1
# Key auditd rules for privilege escalation detection
# /etc/audit/rules.d/privesc.rules
-a always,exit -F arch=b64 -S setuid -F a1=0 -F exe=/usr/bin/sudo -k priv_esc
-w /etc/sudoers -p wa -k sudoers_change
-w /etc/sudoers.d/ -p wa -k sudoers_change
-a always,exit -F arch=b64 -S chmod -F a1=0x800 -k suid_set
-w /etc/crontab -p wa -k cron_change
-w /var/spool/cron/ -p wa -k cron_change
-a always,exit -F arch=b64 -S ptrace -k ptrace_call
# Reload rules
augenrules --load
# Test: check auditd is catching sudoers reads
auditctl -l | grep sudoers
Cron Job and PATH Injection Attacks
Cron-based privilege escalation works when a root-owned cron job executes a script or binary that a lower-privileged user can modify or replace. This happens three ways: the script itself is world-writable, the directory containing the script is world-writable, or the script uses a relative command that can be intercepted via PATH manipulation.
Check every cron job running as root for file and directory permissions. The cron job may be correctly permissioned, but if it calls 'python backup.py' without an absolute path and a user-controlled directory appears earlier in root's PATH, that is an escalation path.
Another common pattern: cron jobs that run files from /tmp or /var/tmp, or that source variables from world-writable config files. We have seen this in third-party monitoring agents that were otherwise well-maintained.
Wildcard injection in cron scripts is a separate vector. A cron job running 'tar czf /backup/archive.tar.gz /var/data/*' is vulnerable if an attacker can create files in /var/data with names like '--checkpoint=1' and '--checkpoint-action=exec=sh shell.sh'. tar interprets these as command-line flags. Use absolute paths and quote variables properly, or use find with -print0 and xargs -0 instead of shell globs.
# Enumerate root crontabs and system cron jobs
crontab -l -u root 2>/dev/null
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/
# Check permissions on scripts called by root cron jobs
# Example: if /etc/cron.d/backup contains /opt/scripts/backup.sh
ls -la /opt/scripts/backup.sh
ls -la /opt/scripts/ # parent directory also matters
# Find world-writable scripts anywhere in cron paths
find /etc/cron* /var/spool/cron /opt -type f -perm -o+w 2>/dev/null
# Safe tar invocation that prevents wildcard injection
find /var/data -type f -print0 | xargs -0 tar czf /backup/archive.tar.gz --
NFS, Docker, and Container Privilege Escalation
NFS exports with no_root_squash allow a root user on the client machine to access files as root on the server. This is a standard privilege escalation path for lateral movement in environments where workstations mount server shares. Audit your /etc/exports and replace no_root_squash with root_squash or all_squash everywhere it is not operationally required.
Docker group membership is equivalent to root. Any user in the docker group can run 'docker run -v /:/mnt --rm -it alpine chroot /mnt sh' and get a root shell with full access to the host filesystem. This is documented, well-known, and still appears on production systems regularly. If users need to run containers without sudo, use Podman in rootless mode instead.
In Kubernetes environments, the equivalent escalation path is through privileged pod specs, hostPID, hostNetwork, or hostPath volume mounts to sensitive directories. A pod running with hostPID:true can send SIGKILL to PID 1 or use nsenter to enter the host's namespaces. Enforce PodSecurity admission at the Restricted level for all non-system namespaces.
For container runtime security, set seccomp to RuntimeDefault or a custom profile at the pod level. This blocks the clone(2) and unshare(2) syscalls that most local kernel exploits depend on.
# Audit NFS exports
cat /etc/exports
showmount -e localhost
# Safe NFS export example
# /etc/exports
# /data 192.168.1.0/24(ro,sync,root_squash,all_squash,anonuid=65534,anongid=65534)
# Check who is in the docker group
getent group docker
# Rootless Podman - no group membership needed
podman run --rm -it alpine sh
# Docker container hardening flags
docker run \
--security-opt=no-new-privileges \
--security-opt seccomp=/etc/docker/seccomp/default.json \
--cap-drop ALL \
--read-only \
--user 1000:1000 \
myimage:latest
# nsenter escalation from privileged container (demonstration)
# nsenter --target 1 --mount --uts --ipc --net --pid -- bash
Hardening Checklist: Specific Controls with Verification
Every hardening step needs a verification command, otherwise you are hoping rather than confirming. Below is the checklist we use on new Linux servers before they go into production. This is not exhaustive but covers the highest-frequency privilege escalation vectors.
Kernel parameters should be set in /etc/sysctl.d/99-hardening.conf and verified with 'sysctl -a | grep' after reboot. Do not rely on runtime sysctl changes surviving a reboot without the conf file entry.
For sudo, the 'use_pty' Defaults option prevents sudo from being used in background scripts and non-interactive shells - this stops a class of injection attacks where malware calls sudo from a context where PTY detection would catch it. Combined with 'log_output', you get a full recording of every sudo session.
For PAM, ensure faillock or pam_tally2 is configured to lock accounts after failed authentication attempts. On systems using SSSD for LDAP or Active Directory authentication, verify the local PAM stack still enforces lockout even when the directory is unreachable.
If your team manages naming conventions for internal services, scripts, or project identifiers alongside these hardening tasks, keeping that work organized matters too. Services like nicename.me help teams standardize naming across infrastructure projects without the usual back-and-forth, which reduces the chance of name collisions that can sometimes affect service routing configurations.
# Kernel hardening - add to /etc/sysctl.d/99-hardening.conf
kernel.unprivileged_userns_clone=0
kernel.unprivileged_bpf_disabled=1
kernel.kptr_restrict=2
kernel.dmesg_restrict=1
kernel.yama.ptrace_scope=2
net.core.bpf_jit_harden=2
fs.protected_hardlinks=1
fs.protected_symlinks=1
fs.suid_dumpable=0
# Apply and verify
sysctl --system
sysctl kernel.unprivileged_userns_clone # should return 0
# Sudo hardening in /etc/sudoers (via visudo)
Defaults use_pty
Defaults log_output
Defaults logfile=/var/log/sudo.log
Defaults timestamp_timeout=0
Defaults passwd_timeout=1
# Verify PAM faillock is active
grep -r 'faillock\|pam_tally' /etc/pam.d/
# Check systemd service hardening (NoNewPrivileges)
systemctl show nginx | grep -i privil
# Should show: NoNewPrivileges=yes
# Verify AppArmor or SELinux is enforcing
aa-status 2>/dev/null | head -5
getenforce 2>/dev/null
Detection: Indicators of Active Privilege Escalation
Detection after exploitation is a failure mode, but detection during exploitation can stop a breach before it completes. The behavioral indicators of privilege escalation attempts are consistent across techniques: unusual SUID binary execution, unexpected sudo invocations, /proc filesystem reads from non-owner processes, and compiler or wget/curl activity from service accounts.
Falco with the default ruleset catches the highest-signal events. The rule 'Sudo Potential Privilege Escalation' fires when sudo is executed with unusual arguments or from an atypical parent process. The 'Read sensitive file untrusted' rule catches reads of /etc/shadow, /etc/sudoers, and SSH private keys from non-privileged processes.
For endpoint detection without a full SIEM, auditd with the rules from the earlier section ships events to journald, which you can query with journalctl. For a SIEM, ship auditd logs via rsyslog or Fluentd to your central log platform and alert on: multiple failed sudo attempts within 60 seconds, setuid system calls from non-root processes, and ptrace calls targeting processes with different UIDs.
OSquery is another tool worth deploying for scheduled privilege escalation checks. The queries below run continuously and alert on new SUID files or changes to sudoers, giving you a persistent audit trail without requiring a full security platform.
# Falco - check if running and loaded default rules
falco --list | grep -i 'sudo\|privilege'
# OSquery - detect new SUID files (run as scheduled query)
# SELECT path, permissions, uid, gid FROM file
# WHERE (permissions LIKE '%s%' OR permissions LIKE '%S%')
# AND type = 'regular'
# AND uid = 0;
# journalctl - query auditd events for sudo usage last 1 hour
journalctl -t audit --since '1 hour ago' | grep 'SYSCALL\|sudo'
# Real-time monitoring of /etc/sudoers changes via inotifywait
inotifywait -m -e modify,attrib /etc/sudoers /etc/sudoers.d/ 2>/dev/null
# Check for unexpected SUID changes since last week
find / -perm -4000 -newer /var/log/lastweek.marker -type f 2>/dev/null
# Create the marker: touch -d 'last week' /var/log/lastweek.marker