
Exploit per CVE-2025-44203 che sfrutta una condizione di gara in HotelDruid 3.0.0/3.0.7, esponendo le credenziali di amministratore e causando denial of service. Include uno script di forza bruta per il recupero offline delle password.
HotelDruid 3.0.0 e 3.0.7 contengono una race condition sull'endpoint di setup creadb.php, raggiungibile prima che il setup sia completato. Ha due impatti, entrambi derivanti dalla stessa race persa: una divulgazione di informazioni sensibili (attraverso messaggi di errore SQL dettagliati) e un denial of service.
Inviando molte richieste POST contemporaneamente, un attaccante può innescare una race condition e leggere dati sensibili dalla risposta, inclusi nome utente dell'amministratore, hash della password e salt. Se la password scelta durante la configurazione del pacchetto Debian è debole, può poi essere recuperata offline con una wordlist.
Quando l'attacco riesce, l'amministratore non è più in grado di accedere con le credenziali impostate durante l'installazione (denial of service).
Il pacchetto Debian lascia creadb.php raggiungibile senza login finché il setup non è completato e, con il default del pacchetto C_UTILIZZA_SEMPRE_DEFAULTS="AUTO", esegue di nuovo l'intera creazione del database a ogni richiesta. Questa routine esegue oltre 100 istruzioni SQL (creando e popolando le tabelle) senza alcun lock attorno, quindi più richieste che arrivano insieme la eseguono contemporaneamente. Questa è la race.
Su una macchina lenta o occupata, le esecuzioni parallele si sovrappongono e collidono sul database SQLite. Una volta che una richiesta ha iniziato a creare le tabelle e le righe, le istruzioni di una richiesta successiva falliscono in massa per vari motivi, come righe già esistenti o database bloccato da un'altra scrittura. A ogni errore, il wrapper delle query di HotelDruid esegui_query() stampa l'intera istruzione fallita nella risposta HTTP. Ciò significa che una singola risposta arriva con dozzine di questi errori SQL, e uno di essi contiene le informazioni sensibili dell'account amministratore.
update ...utenti set password = '<hash>', salt = '<salt>', tipo_pass = '5' where idutenti = '1'
È più o meno così che l'exploit rileva il successo. Cerca nella risposta la stringa set password, che compare solo quando quella precisa istruzione viene stampata, cosa che non accade mai nell'output normale di creadb.php.
Il blocco dell'account deriva dallo stesso errore. La riga dell'amministratore viene prima inserita con tipo_pass='n', e solo l'UPDATE sopra la trasforma in un account funzionante (tipo_pass='5' con password impostata). Il codice che viene eseguito subito dopo l'UPDATE non controlla mai se sia riuscito. Crea il file abilita_login, che attiva il login, elimina ini.php, che conteneva le credenziali dell'amministratore, e successivamente scrive ultimo_accesso, che chiude definitivamente il setup. Quindi quando l'UPDATE perde la race, l'account rimane a tipo_pass='n', che la pagina di login rifiuta sempre anche con la password corretta, mentre il login è attivo e il file delle credenziali è sparito. A quel punto non c'è via di ritorno senza reinstallare.
Questo è il motivo per cui una divulgazione riuscita e il blocco dell'account sono in realtà lo stesso evento. Entrambi accadono quando quell'unico UPDATE perde la race. Quando ottieni solo il nome utente, significa che un'istruzione precedente è collidata mentre l'UPDATE della password è comunque passato, quindi non è stato bloccato nulla.
Esegui l'exploit contro il target dalla macchina dell'attaccante:
python3 exploit.py 192.168.1.1
dove 192.168.1.1 è l'indirizzo IP della macchina che esegue HotelDruid.
Se funziona, usa brute.py con una wordlist per provare a recuperare la password in chiaro. Apri brute.py, imposta salt sul salt ottenuto e final_hash sull'hash ottenuto, quindi esegui:
python3 brute.py rockyou.txt
dove rockyou.txt è la tua wordlist.
Nota che anche con il nome utente e la password corretti non potrai accedere, e nemmeno l'amministratore. La password che recuperi è corretta, ma la race riuscita ha lasciato l'account bloccato a tipo_pass='n', quindi la pagina di login lo rifiuta. Vedi la sezione Causa principale.
La vulnerabilità è stata corretta in HotelDruid 3.0.8. Ecco il changelog: basta cercare "CVE-2025-44203".
L'helper di lock che usa (crea_lock_file(), un flock(LOCK_EX) bloccante) era già presente nella versione 3.0.7 e veniva usato in altre parti del codice. Tuttavia, creadb.php non lo chiamava mai. La patch fa due cose:
creadb.php: acquisisce il lock subito prima del provisioning dell'amministratore e lo rilascia con distruggi_lock_file() successivamente. Ciò che lo fa funzionare è il controllo subito dopo l'acquisizione del lock. La richiesta che entra per prima elimina ini.php mentre ha ancora il lock, e ogni richiesta rilegge se ini.php esiste prima del provisioning, quindi quelle in coda dietro di essa acquisiscono il lock, trovano il file già sparito e saltano l'intero blocco. Quando la seconda viene eseguita, il setup è già completato e non fa nulla.1, a quelle due chiamate esegui_query(), quella che imposta il nome utente e quella che imposta password e salt. Quel flag dice al wrapper delle query di restare in silenzio quando una query fallisce. Registra comunque l'errore nel log del server, ma salta l'echo che altrimenti metterebbe l'istruzione fallita, hash e salt inclusi, nella pagina.creadb.php:
879: esegui_query("update $tableutenti set nome_utente = '".aggslashdb($admin)."' where idutenti = '1'",1);
897: esegui_query("update $tableutenti set password = '$passw', salt = '$salt', tipo_pass = '5' where idutenti = '1'",1);