Once an attacker has a low-privileged shell on a Linux host — through a web app vuln, a phished SSH key, a leaked container token, or a compromised service account — privilege escalation to root or to another user is usually the next step. Linux has a rich set of mechanisms for delegating power (SUID binaries, capabilities, sudo, cron, systemd, NFS, containers), and almost all of them have well-known misuse patterns. Understanding what attackers look for, and the corresponding hardening, makes Linux hosts substantially harder to fully compromise.
Enumeration: what attackers look at first
Privilege escalation is mostly a hunt for misconfigurations. Tooling like linpeas, LinEnum, and pspy automates the enumeration, but the underlying questions are the same as those a careful sysadmin would ask:
- Who am I, and where can I go? –
id,groups,sudo -l,~/.ssh, readable home directories of other users, world-readable backups. - What runs as root? – Processes (
ps -ef), cron jobs (/etc/cron*, user crontabs), systemd timers, init scripts, and anything that wakes up periodically. - What is SUID/SGID or has capabilities? –
find / -perm -4000 -type f 2>/dev/null,getcap -r / 2>/dev/null. - What files are writable that I shouldn't be able to touch? – Scripts in
PATH, config files, log files that root reads, files owned by other users with group-write. - What is the kernel and distribution? –
uname -a,cat /etc/os-release. Old kernels and unpatched distros are a goldmine. - Where are credentials? – Bash history,
.netrc,.aws/credentials,.kube/config, environment variables, world-readable.envfiles, mounted NFS shares.
GTFOBins (gtfobins.github.io) is the standard reference for "I can run X as root via sudo or SUID — how do I get a shell?" Almost every common Unix binary has at least one entry.
SUID, SGID, and capabilities
SUID binaries run with the file owner's privileges regardless of who invokes them. They exist for a reason (/usr/bin/passwd must update /etc/shadow), but a misconfigured or unnecessary SUID binary is one of the most common escalation paths. The classic pattern is something like /usr/bin/find with the SUID bit set, where find . -exec /bin/sh \; gives an immediate root shell. Anything in GTFOBins flagged for SUID misuse — vim, less, awk, python, perl, nmap (old versions), cp, tar — is an instant win if marked SUID root.
Capabilities are a finer-grained alternative to "all-or-nothing root", but they are equally dangerous when misused. capsetuid+ep on /usr/bin/python3 lets any user become root with a one-liner. capdacreadsearch lets the binary read any file on the system. Hardening:
- Audit SUID/SGID binaries on every build of every image. Remove the bit from anything that does not need it; many distributions ship with too many by default.
- Treat any capability on an interpreter (
python,perl,node,ruby) or shell utility as a bug. - Mount filesystems with
nosuidwhere possible (especially/home,/tmp,/var/tmp, removable media, and container mounts). - Use
setpriv,nsenter, and similar tools sparingly and audit who can invoke them.
Sudo, cron, and "trusted" scripts
The single most common Linux escalation vector is a misconfigured sudoers file. Specific anti-patterns:
(ALL) NOPASSWD: ALL– Often added "temporarily" to scripts or build users. Game over if any of them is compromised.- NOPASSWD on a single binary that GTFOBins covers – e.g.
NOPASSWD: /usr/bin/lessor/usr/bin/vim. Both shell out trivially. - Wildcards in sudo rules –
NOPASSWD: /usr/bin/systemctl restart *or path wildcards lets attackers pass unexpected arguments and escalate. envkeepforLDPRELOAD/PYTHONPATH/BASH_ENV– Lets a low-priv user inject code into the sudo'd process.
Hardening: keep sudo rules minimal, prefer specific binaries with specific arguments, never grant NOPASSWD to shell-capable binaries, and review /etc/sudoers and /etc/sudoers.d/ regularly with visudo -c. Consider sudo replacements like doas for simpler audit.
Cron jobs and systemd timers are the other classic path. If a cron job runs /usr/local/bin/backup.sh as root and that script is writable by anyone, or sources files from a writable directory, you are one append away from root. The same applies to systemd unit files in writable paths. Always check:
- Are the script and every directory in its path owned by root and not world-writable?
- Does it reference relative paths or rely on
PATH? An attacker can drop a binary earlier inPATH. - Does it read config from a writable file? Many scripts source
.envor YAML from/etcwith weak permissions.
pspy quietly watches for short-lived processes, and is invaluable for spotting cron jobs and timers from a low-privileged shell.
Kernel exploits and container escapes
When userspace misconfigurations are dead ends, attackers turn to the kernel. Old kernels accumulate exploitable CVEs (Dirty Pipe CVE-2022-0847, Dirty COW CVE-2016-5195, OverlayFS issues, nftables, iouring, eBPF verifier bugs). On unpatched systems, a public exploit and a few minutes of work is often enough to go from nobody to root.
Containers add another dimension. Even a "non-privileged" container can sometimes escape if:
- It runs with
--privileged,--pid=host, or excessive capabilities (CAPSYSADMIN,CAPSYSPTRACE). - The Docker socket (
/var/run/docker.sock) is mounted inside — root in a container with the socket equals root on the host. - Sensitive host paths (
/,/proc,/sys,/etc) are mounted writable. - The container is running with a vulnerable runc / containerd version.
Hardening at this layer: keep the kernel patched (or use live-patching like kpatch/livepatch), restrict who can read /proc/kallsyms and load kernel modules, disable unused subsystems where you can, run containers non-privileged with dropped capabilities and a seccomp profile, and never mount the Docker/Containerd socket into a workload.
A practical hardening baseline
You will not eliminate every escalation path, but a focused baseline catches most of them.
- Patch kernels and packages – Unattended-upgrades,
dnf-automatic, or a managed patch tool; track end-of-support distros and replace them. - Strip SUID and capabilities – Build images and golden hosts with only the SUID set you actually need; mount user-writable filesystems
nosuid,nodev. - Minimal, audited sudo – No
NOPASSWD: ALL; no shell-capable binaries in narrow rules; review periodically. - Strict file permissions on automation – Scripts run by root must be root-owned, 755 or stricter, in non-writable directories. Same for the directories above them.
- Restrict shells and interactive access – Service accounts get
/usr/sbin/nologin. SSH allows key-based login only, withAllowUsersorAllowGroups, and root login disabled. - Mandatory access control – AppArmor or SELinux in enforcing mode; do not disable them "to make the app work" without writing a proper profile.
- Auditing and detection –
auditdrules for execve, sudo, mount, ptrace, and changes to/etc/passwd,/etc/shadow,/etc/sudoers, and~/.ssh. Ship to a SIEM. EDR or Falco/Tetragon catches kernel-level behaviours. - Containers and namespaces – Non-root user, dropped capabilities, read-only root filesystem, no host mounts, seccomp default; treat any privileged container as a tier-0 asset.
Most Linux compromises rely on something boring: a forgotten SUID binary, a sudo wildcard, a writable cron script, an unpatched kernel, or a developer-friendly container that turned out to be a foothold. Working through this list on every host and image you ship eliminates the great majority of those paths, and turns the rest into noisy, detectable events.