
Exploit para CVE-2025-44203 que apunta a una condición de carrera en HotelDruid 3.0.0/3.0.7 que filtra las credenciales de administrador y provoca denegación de servicio. Incluye un script de fuerza bruta para la recuperación offline de contraseñas.
HotelDruid 3.0.0 y 3.0.7 contienen una condición de carrera en el endpoint de configuración creadb.php, accesible antes de que la instalación haya terminado. Tiene dos impactos, y ambos provienen de la misma carrera perdida: una divulgación de información sensible (a través de mensajes de error SQL verbosos) y una denegación de servicio.
Al enviar muchas peticiones POST a la vez, un atacante puede provocar una condición de carrera y leer datos sensibles de la respuesta, incluyendo el nombre de usuario del administrador, el hash de la contraseña y el salt. Si la contraseña elegida durante la configuración del paquete Debian es débil, puede recuperarse posteriormente sin conexión con una lista de palabras.
Cuando el ataque tiene éxito, el administrador no puede iniciar sesión con las credenciales que estableció durante la instalación (denegación de servicio).
El paquete Debian deja creadb.php accesible sin iniciar sesión hasta que la instalación termina, y con el valor predeterminado del paquete C_UTILIZZA_SEMPRE_DEFAULTS="AUTO" vuelve a ejecutar toda la creación de la base de datos en cada petición. Esa rutina emite más de 100 sentencias SQL (creando y poblando las tablas) sin ningún bloqueo alrededor, por lo que varias peticiones que llegan juntas la ejecutan al mismo tiempo. Esa es la carrera.
En una máquina lenta u ocupada, las ejecuciones paralelas se solapan y chocan en la base de datos SQLite. Una vez que una petición ha comenzado a crear las tablas y filas, las sentencias de una petición posterior fallan en masa por varias razones, como filas ya existentes o la base de datos bloqueada por otra escritura. En cada fallo, el envoltorio de consultas de HotelDruid esegui_query() imprime la sentencia fallida completa en la respuesta HTTP. Esto significa que una sola respuesta llega con decenas de esos errores SQL, y uno de ellos contiene la información sensible de la cuenta de administrador.
update ...utenti set password = '<hash>', salt = '<salt>', tipo_pass = '5' where idutenti = '1'
Así es, más o menos, como el exploit detecta el éxito. Busca en la respuesta set password, que solo aparece cuando esa sentencia exacta se muestra, y eso nunca ocurre en la salida normal de creadb.php.
El bloqueo de acceso proviene del mismo fallo. La fila del administrador se inserta primero con tipo_pass='n', y solo el UPDATE anterior la convierte en una cuenta funcional (tipo_pass='5' con una contraseña establecida). El código que se ejecuta justo después del UPDATE nunca comprueba si funcionó. Crea el archivo abilita_login, que activa el inicio de sesión, elimina ini.php, que contenía las credenciales del administrador, y más tarde escribe ultimo_accesso, que cierra la instalación definitivamente. Así que cuando el UPDATE pierde la carrera, la cuenta permanece en tipo_pass='n', que la página de inicio de sesión siempre rechaza incluso con la contraseña correcta, mientras el inicio de sesión está activado y el archivo de credenciales ha desaparecido. En ese punto no hay vuelta atrás sin reinstalar.
Por eso una fuga exitosa y el bloqueo de acceso son en realidad el mismo evento. Ambos ocurren cuando ese único UPDATE pierde la carrera. Cuando obtienes solo el nombre de usuario, significa que una sentencia anterior colisionó mientras el UPDATE de la contraseña aún se completaba, por lo que nada quedó bloqueado.
Ejecuta el exploit contra el objetivo desde la máquina del atacante:
python3 exploit.py 192.168.1.1
donde 192.168.1.1 es la dirección IP de la máquina que ejecuta HotelDruid.
Si funciona, usa brute.py con una lista de palabras para intentar recuperar la contraseña en texto plano. Abre brute.py, establece salt con el salt que obtuviste y final_hash con el hash que obtuviste, y luego ejecuta:
python3 brute.py rockyou.txt
donde rockyou.txt es tu lista de palabras.
Ten en cuenta que incluso con el nombre de usuario y la contraseña correctos no podrás iniciar sesión, y tampoco el administrador. La contraseña que recuperas es correcta, pero la carrera exitosa dejó la cuenta atascada en tipo_pass='n', por lo que la página de inicio de sesión la rechaza. Consulta la sección Causa raíz.
La vulnerabilidad fue corregida en HotelDruid 3.0.8. Aquí está el registro de cambios; simplemente busca «CVE-2025-44203».
La función auxiliar de bloqueo que utiliza (crea_lock_file(), un flock(LOCK_EX) bloqueante) ya estaba presente en la versión 3.0.7 y se usaba en otras partes del código. Sin embargo, creadb.php nunca lo llamaba. El parche hace dos cosas:
Toma un bloqueo exclusivo alrededor de la rutina de configuración, de modo que las peticiones que llegan juntas esperan su turno en lugar de competir. El arreglo lo implementa finalmente llamando a esa función auxiliar en creadb.php: toma el bloqueo justo antes del aprovisionamiento del administrador y lo libera con distruggi_lock_file() después. Lo que hace que funcione es la comprobación justo después de tomar el bloqueo. La petición que entra primero elimina ini.php mientras aún mantiene el bloqueo, y cada petición vuelve a comprobar si ini.php existe antes del aprovisionamiento, así que las que están en cola detrás toman el bloqueo, encuentran que el archivo ya no existe y omiten todo el bloque. Cuando la segunda se ejecuta, la configuración ya está completa y no hace nada.
Pasa el indicador de silencio a las dos consultas UPDATE del administrador, de modo que incluso si una de ellas falla, ya no imprime las credenciales en la respuesta. El arreglo lo implementa añadiendo un segundo argumento, 1, a esas dos llamadas esegui_query(), la que establece el nombre de usuario y la que establece la contraseña y el salt. Ese indicador le dice al envoltorio de consultas que permanezca en silencio cuando una consulta falla. Todavía registra el error en el log del servidor, pero omite el echo que de otro modo pondría la sentencia fallida, con el hash y el salt incluidos, en la página.
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);