Linux Capabilities Abuse: Escalating Through cap_setuid
Updated August 4, 2026 · Written by PWNMI — see About.
Linux capabilities exist specifically to avoid the all-or-nothing problem with SUID: instead of granting a binary full root privilege, you grant it one narrow kernel-level power — bind to a low port, override file ownership checks, change its own UID — without the rest of what root can do. That's a genuine security improvement over SUID in principle. In practice, it moves the same class of mistake to a different mechanism: grant a capability to something broader than intended, and you've built the same problem with extra steps.
What you'll practice: enumerating file capabilities the way you'd enumerate SUID binaries, understanding what a specific capability actually grants instead of treating "has a capability" as binary, and recognizing why granting a capability to an interpreter is categorically different from granting it to a single-purpose compiled binary.
Set up the lab
New to Docker or git, or unsure what the commands below are actually doing? See Docker for Security Labs and Git for Security Work first.
Get the lab files from pwnmihq/resources:
git clone --filter=blob:none --sparse https://github.com/pwnmihq/resources
cd resources
git sparse-checkout set technique-labs/linux-capabilities-abuse
cd technique-labs/linux-capabilities-abuse
docker compose up -d --build
Drop into a shell as analyst, an ordinary unprivileged user:
docker compose exec -u analyst app bash
id
Understanding the technique
CAP_SETUID is the specific kernel capability that governs whether a process can call setuid() — change its own user ID — without needing full root. It's a real, narrow, legitimate thing to grant a purpose-built helper: a service manager that needs to drop from root to a service account, for instance, doesn't need full root to do that one thing.
The mistake in this lab: someone needed a Python script to change its own UID for a specific task, and instead of writing (or reaching for) a small compiled helper binary scoped to exactly that operation, they ran setcap cap_setuid+ep on the python3 interpreter binary itself. The capability isn't attached to the script — it's attached to the interpreter. Every script python3 ever runs, for any user who can invoke it, now inherits the ability to call setuid(0).
Exploitation
Enumerate file capabilities the same disciplined way you'd enumerate SUID binaries — don't assume, check:
getcap -r / 2>/dev/null
/usr/bin/python3.11 cap_setuid=ep
ep means the capability is both effective (active immediately) and permitted (available to be used) — not just present but usable. Confirm what that actually lets you do:
python3 -c "import os; os.setuid(0); os.system('id')"
uid=0(root) gid=1000(analyst) groups=1000(analyst)
Note the asymmetry: uid=0 but gid is still 1000. cap_setuid grants control over the process's user ID specifically — it doesn't touch group membership, which is governed by a separate capability (CAP_SETGID) that isn't granted here. That distinction matters on a real engagement: a uid=0 process can still be meaningfully restricted by group-based permissions, though in practice uid=0 alone is enough to read most of what matters, including files that check ownership rather than group.
Confirm that directly:
python3 -c "import os; os.setuid(0); os.system('head -1 /etc/shadow')"
root:*:20647:0:99999:7:::
/etc/shadow is owned by root:shadow with permissions that block both other users and non-shadow-group members — uid=0 alone is sufficient to bypass that, regardless of gid.
Cleanup
Nothing persists past the process itself — this exploit runs a one-off Python interpreter invocation and exits, creating no files and modifying no state. As with the other labs in this series, cleanup only becomes relevant once you use the access for something that leaves a trace (a planted file, a new account, a persistent shell).
Tear the lab down when you're done:
docker compose down -v
Lessons worth keeping
- A capability is exactly as broad as the binary it's attached to.
cap_setuidon a purpose-built helper is a narrow grant;cap_setuidon a general-purpose interpreter is close to unrestricted root, because the interpreter can run arbitrary code that uses that capability. getcap -r /deserves the same reflexive check asfind -perm -4000. Capabilities are a newer, less-memorized mechanism than SUID, which makes them easy to skip during enumeration — don't.- uid and gid escalate independently.
os.setuid(0)alone doesn't make you fully root in every sense; know which specific capability you have and what it actually controls, rather than treating "some privilege escalation happened" as a single undifferentiated outcome.
Next step
To close out the series, see Writable /etc/passwd — a technique that needs no exploit primitive at all, just a permissions bug. For the broader mental model, see Privilege Escalation Fundamentals.
Get new write-ups in your inbox
New roadmaps, tool walkthroughs, and lab write-ups. No spam. Unsubscribe anytime.