Privilege escalation
Privilege escalation turns a low-priv foothold into root or SYSTEM by abusing a misconfig the host already shipped — a SUID binary, a sudo gap, a writable service, a stale kernel. Learn the escalation path the attacker enumerates, so you close the rung first.
On an authorized test, the foothold is unglamorous: a deploy script leaked a service-account password, and now there’s a shell running as www-data on a Linux box. That account can read the web root and not much else — it can’t touch /etc/shadow, can’t read other tenants’ data, can’t install a persistent backdoor. A junior tester stops here and writes “low severity, limited account.” A senior keeps going, because the foothold is rarely where the value is. Within ten minutes the enumeration spits out a list: a backup script owned by root is world-writable and runs from cron every five minutes; a custom binary has the SUID bit set and shells out to tar without an absolute path; sudo is configured to let www-data run one “harmless” admin tool — that happens to spawn a subshell. None of these is an exploit. Each is a configuration the host shipped that way. The attacker doesn’t break in again — they walk up a staircase the system built for them, and step out as root.
By the end of this lesson you’ll be able to explain how an attacker enumerates the local escalation path on Linux and Windows, why each rung is a misconfiguration and not a “hack,” and the defender’s reflex that removes the rung before it’s climbed.
Authorization first: this is a defender’s autopsy
Everything below assumes a signed engagement with explicit scope — a deliberately vulnerable lab, a CTF box, a machine you own, or a system named in a bug-bounty program. Running enumeration tooling against a host you have no authorization for, or pivoting from a low account to root on someone else’s infrastructure, is unauthorized access in nearly every jurisdiction, full stop. We study the attacker’s enumeration for exactly one reason: you cannot reliably close an escalation path you only understand as a checklist item. Read this as the autopsy a defender runs on their own host — learning to see the SUID bit and the sudo rule the way the attacker does, so the rung is gone from the design before any tester finds it. No weaponized payloads here, only the mechanism that tells you which rung to remove.
The shape of the attack: foothold, enumerate, abuse, persist
Privilege escalation is not a single exploit; it is a phase. The attacker already has code execution as some low-privilege principal — a web user like www-data or IIS APPPOOL, a compromised service account, a CI runner. The goal is vertical movement: from that principal to a higher one (root, SYSTEM, Administrator, a domain admin), which unlocks reading every file, installing persistence, and pivoting to other hosts. The loop is always the same: enumerate the local environment for anything misconfigured, find a primitive that crosses a trust boundary, abuse it to execute code as the higher principal, then establish persistence so the foothold survives a reboot or a password reset.
The crucial mental model is that escalation lives in the gap between who runs a thing and who controls its inputs. Every rung is some component that executes with high privilege but takes an input — a path, a file, a binary, an environment variable — that a low-privilege user can influence. The attacker’s whole job in this phase is to enumerate where those two facts diverge.
Linux: the four rungs that recur
On Linux the same handful of misconfigurations appear over and over, and a senior defender learns to spot each in their own fleet before a tester does.
SUID/SGID binaries. A file with the SUID bit runs as its owner regardless of who launches it — so a SUID-root binary executes as root even when www-data runs it. That is by design for tools like passwd that need a privileged moment. The danger is a SUID binary that does something it shouldn’t: shells out to a helper by relative name (so the attacker prepends a malicious directory to PATH), reads or writes a file the caller names, or is a general-purpose interpreter that should never be SUID at all. A SUID-root program that calls system("tar ...") hands the attacker root the moment they put a fake tar earlier in PATH. The fix is least privilege on the binary: drop the SUID bit unless it is genuinely required, and if it is, the binary must use absolute paths, sanitize the environment, and expose no file or command the caller can steer.
sudo misconfiguration. sudo is meant to grant narrow elevation, but a sloppy /etc/sudoers grants wide. Allowing a low account to run an editor, a pager, or a scripting interpreter as root is effectively granting a root shell, because those programs can spawn subshells or read arbitrary files. NOPASSWD on a binary with a shell-escape is the classic gift. The defender’s reflex: every sudo rule is an exception to deny-by-default, so audit each one by asking “can this exact command, as configured, be turned into arbitrary code execution?” — and if yes, it is a root grant in disguise.
Writable privileged jobs and services. A cron job, systemd unit, or service script that runs as root but lives in a world-writable file (or calls a script in a world-writable directory) lets any user rewrite what root will execute next. The misconfig is the file permission, not the scheduler. The fix is to lock ownership and permissions on everything a privileged scheduler touches, so a low account can never edit the next thing root runs.
Kernel and stale-component exploits. When configuration is clean, the attacker falls back to a known vulnerability in the kernel or a privileged daemon — a CVE in a setuid helper, a kernel bug that grants a local user root. This is the rung you close not with permissions but with patching: an unpatched, end-of-life kernel is a standing local-root primitive for anyone who already has a shell. Kernel exploits are noisier and can crash the box, so a careful attacker tries them last — which is exactly why they signal a host that fell behind on updates.
| Rung | What the attacker enumerates | Why it crosses the boundary | Defender’s fix |
|---|---|---|---|
| SUID / SGID binary | Files with the setuid bit set | Runs as owner (root) but trusts caller’s PATH/args | Drop the bit; absolute paths; clean env |
| sudo rule | What this account may run as root | Program with a shell-escape = root shell | Narrow rules; no escapable binaries |
| Writable cron / service | Permissions on root-run scripts | Low user rewrites what root runs next | Lock ownership + perms on the path |
| Kernel / stale daemon | Kernel version, EOL components | A known local-root CVE in privileged code | Patch; retire end-of-life kernels |
| Windows: unquoted path | Service binary paths with spaces | SYSTEM service runs an attacker file first | Quote paths; lock the install dir |
| Windows: token / DLL | Privileges, weak service ACLs, DLL paths | Token abuse or a planted DLL runs as SYSTEM | Least privilege; tight ACLs; signed DLLs |
Windows: same gap, different names
Windows escalation rhymes with Linux — the same “runs high, trusts low” gap — under different mechanisms. Unquoted service paths are the textbook example: a service registered as C:\Program Files\My App\svc.exe without quotes, running as SYSTEM, makes Windows try C:\Program.exe first; if a low user can write to C:\, their binary runs as SYSTEM. Weak service permissions let a low user reconfigure the binary a SYSTEM service launches. DLL hijacking plants a malicious library where a privileged process loads it by an unsafe search order. Token impersonation abuses certain privileges (held by some service accounts) to impersonate a SYSTEM token. AlwaysInstallElevated — a policy that runs MSI installers as SYSTEM — turns any user-supplied installer into a root grant. Different surface, identical root cause: a high-privilege component trusts a path, a permission, or a token that a low-privilege user can influence.
▸Why this works
Why do attackers try a clean-config primitive (SUID, sudo, writable cron) before a kernel exploit, when the kernel bug is the more powerful tool? Two reasons, both operational. First, reliability: a misconfiguration is deterministic — abuse the SUID binary and you get root every time — whereas a kernel exploit can fail against an unexpected patch level and, worse, panic the box, ending the engagement and lighting up monitoring. Second, stealth: walking up a configuration the host shipped looks like normal use of normal tools, while loading a kernel exploit is loud and detectable. The defender’s takeaway is the mirror image: a clean, least-privilege configuration removes the quiet rungs an attacker prefers, forcing them onto the noisy kernel path where your patching and your detection actually get a chance to win.
How a defender removes the staircase
The unifying defense is least privilege at every layer, because every rung is a place where privilege was granted more broadly than the task required. Assume a foothold will happen — phishing, a leaked credential, an app bug — and design so the foothold is a dead end. Audit SUID/SGID inventory and strip the bit from anything that doesn’t earn it; treat each sudo rule as an explicit exception and reject any program with a shell-escape; lock ownership and permissions on every file a privileged scheduler or service touches; keep kernels and privileged daemons patched so there is no standing local-root CVE; on Windows, quote service paths, tighten service ACLs, and disable AlwaysInstallElevated. Then add detection for the climb itself — alert on new SUID files, on edits to privileged units, on a service account spawning a shell — so even a rung you missed gets caught. The senior reflex across all of it is the same as everywhere in security: deny by default, grant the minimum, and assume the lower boundary will be breached.
On an authorized test, a SUID-root binary shells out to `tar` by relative name, and the low account controls `PATH`. Pick the fix that actually closes the escalation.
A Windows service runs as SYSTEM with the binary path `C:\Program Files\My App\svc.exe` registered without quotes, and a low user can write to `C:\`. What's the class and the precise fix?
Why does a careful attacker try misconfiguration-based escalation (SUID, sudo, writable cron) before reaching for a kernel exploit?
Order the phases of a local privilege-escalation attack, from the starting foothold to a durable hold on the host:
- 1 Attacker has code execution as a low-privilege account (e.g. www-data)
- 2 Enumerate the host for misconfigurations (SUID, sudo, writable jobs, kernel version)
- 3 Find a component that runs privileged but trusts low-privilege input
- 4 Abuse that input to execute code as root / SYSTEM
- 5 Establish persistence so the elevated access survives reboot or reset
- 01Explain the mental model of local privilege escalation and why every rung is a misconfiguration rather than a 'hack'.
- 02Name the recurring Linux and Windows escalation rungs and the single defensive principle that closes them, and explain why attackers prefer misconfig over kernel exploits.
Local privilege escalation is a phase, not a single exploit: an attacker with a low-privilege foothold (www-data, a service account) enumerates the host for a component that runs with high privilege but trusts an input a low user controls, abuses that gap to execute code as root or SYSTEM, and then persists. Every rung is a misconfiguration the host already shipped, not a magic hack — on Linux a SUID binary that trusts a relative path or the caller’s PATH, a sudo rule for a program with a shell-escape, a world-writable cron or service file, or a stale kernel with a known local-root CVE; on Windows an unquoted service path, weak service ACLs, DLL hijacking, token impersonation, or AlwaysInstallElevated. They all share one shape — high privilege trusting low-privilege input — and one fix: least privilege at every layer, so a foothold is a dead end, plus patching to remove standing kernel CVEs and detection to catch the climb you missed. Attackers reach for misconfigurations before kernel exploits because they’re deterministic and quiet, which is exactly why a clean config removes the rungs they prefer. Now when you read a privileged binary, sudo rule, cron job, or service definition, your first question is: does this run with more privilege than its task needs, and can a low account influence what it does?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.