HTB Titanic Walkthrough
文章描述了一次网络安全渗透测试的过程:使用Nmap扫描目标IP 10.10.11.55,发现开放端口22和80;通过LFI漏洞读取系统文件并获取用户信息;枚举子域名发现dev.titanic.htb并利用Gitea配置文件获取数据库信息;破解开发者密码并通过ImageMagick漏洞获得root权限。 2025-9-12 05:23:24 Author: infosecwriteups.com(查看原文) 阅读量:5 收藏

1. Nmap Scan

We use nmap to discover open ports and services. Initially we use only the -sV option along with the -p- option to check for all ports but not make the scan too long by adding additional options.

nmap -sV -p- 10.10.11.55
  • -sV : Detect the service and version which is running.
  • -p- : Run the scan on all ports

Press enter or click to view image in full size

Nmap output

Now that we know that only 2 ports are open, we use the -p option for the 2 ports and the -sC option.

nmap -p22,80 -sV -sC 10.10.11.55
  • -p22,80 : Run the scan on only these 2 ports.
  • -sC : Run a default set of NSE scripts to get information about the open services.

Also we see the host name is titanic.htb which we have to add to our /etc/hosts file because of virtual hosting set up.

Press enter or click to view image in full size

Nmap output

1.1 Change host name in /etc/hosts

Currently we cannot access the IP directly because of virtual hosting set up on the server. We need the domain to be mapped to the IP in the /etc/hosts file so that the host header is set as titanic.htb when we make requests to the web server.

Press enter or click to view image in full size

/etc/hosts

2. Enumerate Port 80

Port 80 provides only a form which takes in some data and allows us to download our input back as a JSON file once we submit the form.

Press enter or click to view image in full size

Port 80 form

Running gobuster to search for directories did not reveal much other than the API paths which are being called when we submit the form.

gobuster dir -u http://titanic.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
  • dir : Uses directory/file enumeration mode
  • -u : The target URL
  • -w : The wordlist to use

Press enter or click to view image in full size

gobuster run

We start looking at the API’s in burp. The URL for downloading our data is http://titanic.htb/download?ticket=<RANDOM_ID>.json and this lets us download the file to our local machine as you can see in the attachment part in the Content-Disposition response header.

Press enter or click to view image in full size

GET request to download attachment

Since the ticket parameter in the GET request refers to a file in the filesystem, we can try to check if there is LFI.

2.1 LFI and user.txt

We try to access /etc/passwd file and we see that we do get the file as our output. In the output we see the user developer in the file as well.
Maybe we can use this as a username later on ? 🤔

Press enter or click to view image in full size

/etc/passwd

Since we have LFI, we can also try getting the user’s flag from the developer’s home directory.

Press enter or click to view image in full size

user.txt

user.txt -> c3d59***************************

Great, now lets see if we can try to escalate this and get a shell or RCE in some form 😊.

Spoiler alert, the only thing I escalated was my own expectations for getting a shell . No RCE for me yet 😞.

Some of the things which I tried but did not work.

  • Getting a shell via log poisoning was not possible as PHP was not being used.
  • There was no id_rsain home/developer/.ssh/id_rsa which we could use to SSH inside.
  • Enumeration of other files in the filesystem were not useful.

2.2 Subdomain enumeration

Maybe we missed something else during our enumeration phase. We go back and search for subdomains using the gobuster vhost command and we get a hit: dev.titanic.htb

gobuster vhost -u http://titanic.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain -t 100 --exclude-length 300-325
  • vhost : Uses VHOST enumeration mode
  • -u : The target URL
  • -w : Path to the wordlist
  • --append-domain : Append main domain from URL to words from wordlist. Eg: append titanic.htb to dev
  • -t : Number of concurrent threads
  • --exclude-length : Exclude the following content lengths. We are using this as there are many invalid responses which just redirect back to titanic.htb.

Press enter or click to view image in full size

We add this subdomain to our hosts file as well and now we can access it.

3. Getting a shell

We open dev.titanic.htb and we see that it is running Gitea.

Gitea is a self-hostable web service for managing Git repositories. The version 1.22.1 is shown at the bottom of the page.

Press enter or click to view image in full size

Gitea

We look at the repositories present and we see two of them under the developer user.

  1. flask-app — This refers to the flask-app source code running on titanic.htb.
  2. docker-config — This hosts some docker-compose.yaml files of gitea and mysql.

Press enter or click to view image in full size

Gitea repositories

We look at the source code for the flask app but do not find anything special there to go forward on.
(See the POST ROOT BONUS 5.1 section at the end to see how the LFI issue is introduced in the flask-app 😉)

Next, we see the docker-config repository and this has some interesting information.

  1. The mysql docker-compose.yaml has some credentials which we could use later on. We take note of these 📝.

Press enter or click to view image in full size

MYSQL docker-compose.yaml

2. The gitea docker-compose.yaml has its volumes mapped to the developer’s home directory. We can use the LFI to read data present here.

Press enter or click to view image in full size

Gitea docker-compose.yaml

3.1 Gitea enumeration

Lets try enumerating more information from this directory with the LFI. The below URL can be used to get the gitea configuration values with LFI.
/download?ticket=../../../home/developer/gitea/data/gitea/conf/app.ini

Press enter or click to view image in full size

Gitea app.ini

Q) How did I know about the app.ini configuration file?
A) I ran gitea’s docker image locally and exec’d into the container to see the folder structure inside.

Press enter or click to view image in full size

Gitea local image

In the app.ini, we see a lot of configuration values but mainly we find the location of gitea’s sqlite database.

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

Now we can pull this gitea.db to our local machine by going to http://titanic.htb/download?ticket=../../../../../../../home/developer/gitea/data/gitea/gitea.db

3.2 Hash cracking

We open the gitea.db file using sqlite3 and we see that there is table called user which we can enumerate on. The table contains the password hashes, salts and the hashing algorithm used.

Press enter or click to view image in full size

user table

Press enter or click to view image in full size

user schema

The hashing algorithm used is pbkdf2$50000$50. Some information about this algorithm is given here. Lets try to crack these hashes using hashcat.

  • Iterations — 50000
  • Key length — 50

This page gives us the format hashcat needs the hash to be so that it can crack it. The hashing algos could be sha1,sha256 or sha512. We can test it one by one.

Example format: sha1:[iteration count]:[salt in base64]:[digest in base64]

We can convert this to the base64 format using:
echo “<HASH/SALT>” | xxd -r -p | base64

Press enter or click to view image in full size

Convert hash to base64 format

We put these hashes inside a file and after testing out the different SHA algorithms, we cracked the password for the developer user using:
hashcat -m 10900 -a 0 developer.txt /usr/share/wordlists/rockyou.txt

  • -m 10900 -> PBKDF2-HMAC-SHA256
  • -a 0 -> Attack mode (Dictionary attack)
  • developer.txt -> sha256:50000:[salt in base64]:[digest in base64]

Password cracked -> 25282528

Nice, we can use this to SSH in as the developer user 🥳.

4. Root Flag

Now, lets try to escalate our privileges to root.

  • Running sudo -l did not give anything.
  • There was no SQL service running where we could use our previously found credentials.
  • There were no cron jobs running when we run the cat /etc/crontab command.
  • In the first glance, there did not seem to be any interesting files in the file system.

We run linpeas to find any privilege escalation vectors if any.

Linpeas output:

  • /home/developer/.local/bin is the first entry in the PATH variable and we can write to it.
  • /opt/app/app.py is where the flask app is being run.
    Unexpected to see code in the /opt directory.
  • Modified interesting files in the last 5 mins
    - /opt/app/static/assets/images/metadata.log

We have a few interesting points to go ahead with. Firstly, we get to the /opt directory and we see a app folder(for running the flask-app), containerd folder and a scripts folder as well. We cannot open the containerd folder because of the permissions.

Press enter or click to view image in full size

identify_images.sh

Inside the scripts folder, we see the identify_images.sh script. The content is given below.

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

The script does the below actions:

  1. Moves to the /opt/app/static/assets/images directory.
  2. Empties the metadata.log file using the truncate command.
  3. Finds all jpg files in that directory and passes each of them to the magick command.
  4. Magick gets some metadata about the image and stores it in metadata.log.

This script seems to run as root periodically every couple of minutes because the last modified time for metadata.log also keeps updating every few minutes. I was unable to find a crontab for this as my current user tho. (See the POST ROOT BONUS 5.3 section to find out where is the crontab set 😉).

Press enter or click to view image in full size

script run

Great, now that we know that the script is being run as root, we can try to create a fake truncate command inside the /home/developer/local/bin directory and that would run when the script runs as it is the first entry in the $PATH environment variable.

Welp, that did not end up working but you can find out about why in the POST ROOT BONUS 5.2 section 😉.

4.1 Magick CVE-2024-41817 and ROOT

Right so we move on to the next approach. Since we know that PATH based binaries were not working, we enumerate the other binary which is in the script i.e ImageMagick.

Press enter or click to view image in full size

magick version

Googling about magick exploits 7.1.1–35 leads us to CVE-2024–41817. A overview of the exploit is given here.

Brief overview of the exploit — While ImageMagick is executing, it might use the current working directory as the path to search for the configuration file(delegates.xml) or shared libraries(.so). So if an attacker can control the file in the current working directory while ImageMagick is executing, it might lead to arbitrary code execution by loading a malicious configuration file or shared library.

Since we can write files inside the /opt/app/static/assets/images/ directory, how about we put a shared object file there ?

Lets try it out. We compile the below C code into shared library and wait for magick to pick it up in a couple of minutes.

Press enter or click to view image in full size

Exploit
  • -x c : Treat the input as C source code
  • -shared : Create a shared object file (.so)
  • -fPIC : Generate position-independent code, which is required for shared libraries.
  • -o : Output file name

We set up a listener on our machine and we get a callback 🥳.

root.txt -> 34efe***************************

Press enter or click to view image in full size

nc listener and root

5. POST ROOT BONUS 😉

5.1 How was the LFI introduced ?

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)

The issue arises in the /download API which takes in the ticket parameter and does no input sanitization on it. Next, the value in ticket is appended to the string value tickets.

TICKETS_DIR = "tickets"
ticket = request.args.get('ticket')
json_filepath = os.path.join(TICKETS_DIR, ticket)

If the user input is ../../../../etc/passwd, the json_file output becomes tickets/../../../../etc/passwd -> /etc/passwd . This is how we exploited the LFI in this box.

5.2 Why did the $PATH based binaries not work ?

We created a fake truncate command which would give us a shell in the /home/developer/.local/binand setup a listener on our machine.

Press enter or click to view image in full size

We waited for some time but did not receive a shell. Tried different payloads but it did not work so was kinda scratching my head. Found the reason once I became root.

As you know, the script identify_images.sh is being run as root but it is not necessary for the root user’s $PATH to be the same as the developer’s $PATH. The root user’s $PATH did not have the /home/developer/.local/bin entry as shown below. Hence, the script when run did not pick up the truncate command which we had created.

Press enter or click to view image in full size

PATH variable for root

The reason the developer user had that entry in his path is because of the /home/developer/.profile which set this specifically for this user.

Press enter or click to view image in full size

developer .profile

5.3 How is the cron running for the identify_images.sh script ?

Once we become root, we can run crontab -l for the root user to see if there are any cronjobs.

Press enter or click to view image in full size

root crontab

Press enter or click to view image in full size

cleanup.sh

We see that identify_images.sh is being run every minute and a cleanup of the shared library also takes place using cleanup.sh.

Every 10 minutes, the /opt/app/static/assets/images directory is being reset to a predefined directory using the revert.sh script.

Press enter or click to view image in full size

revert.sh

文章来源: https://infosecwriteups.com/htb-titanic-walkthrough-3557be1fb2a4?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh