Prueba de concepto de una condición de carrera TOCTOU en la configuración inicial de Socket.IO de Uptime Kuma, que permite a un atacante no autenticado plantar un segundo administrador oculto y deshabilitar la autenticación.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · uptime-kuma-setup-toctou
Uptime Kuma 2.5.5 - Louis Lam
El setup de Socket.IO en la primera ejecución hace COUNT de usuarios, aplica bcrypt con coste 10 y luego hace INSERT. El nombre de usuario es UNIQUE únicamente. No hay transacción, mutex ni restricción de una sola fila. Dos sockets ya conectados que ambos ven count === 0 ambos hashean, ambos insertan. El admin extra es invisible: no hay UI de lista de usuarios.
Un desconocido que golpee un Kuma recién iniciado en el mismo sondeo que el operador puede plantar un segundo admin oculto, desactivar la autenticación y aprovechar el auto-login hacia los monitores del operador.
| ID | sin CVE todavía |
| CWE | CWE-362, CWE-367 |
| CVSS | Alto: 7.4 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Producto | Uptime Kuma |
| Afectado | hasta 2.5.5 (c98982a), tabla user vacía en la primera ejecución |
| Auth | no autenticado |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Golpear un Kuma recién iniciado al mismo tiempo que el operador envía /setup. El bind por defecto de Docker es 0.0.0.0:3001. Insertar una segunda fila user con un nombre de usuario diferente. Iniciar sesión como ese usuario, establecer disableAuth y aprovechar el auto-login de R.findOne("user") hacia los monitores, notificaciones, claves API y ajustes del operador.
Después de que se confirme la primera fila, un setup secuencial posterior es rechazado. Esto no es "el setup permanece abierto". Es una carrera durante la ventana de bcrypt. Las instancias ya inicializadas con un admin no tienen este bug.
Un IDOR latente de habilitar/deshabilitar claves API (UPDATE api_key SET active=? WHERE id=? sin user_id) también se activa una vez que existen dos usuarios. Es una consecuencia de la fila extra, no del oráculo del lab.
Leí primero SECURITY.md, luego la lista de omisiones: dieciocho avisos publicados de GitHub, más el XSS público de siteId de Matomo, más el SSRF de metadatos de nube que el proveedor no arreglará. Luego veintidós cacerías sobre el tag 2.5.5 (c98982a): auth/setup/2FA, IDOR de socket, SSRF de HTTP/monitor, restos de SSTI, path/LFI, navegador real, plugin/tailscale, página de estado, docker.sock, prototype pollution, push/prometheus, base de datos, ejecución de monitor, XSS de analytics, ajustes, CSRF de websocket, XSS de frontend, jobs, Apprise, mantenimiento, master-delta. La mayor parte de ese listón no devolvió nada sin publicar. Auth no.
El setup del primer usuario es un evento público de Socket.IO. Sin login. setup hace COUNT(user), luego await passwordHash.generate, luego R.store. passwordHash.generate es bcrypt.hash con coste 10. Eso es una siesta, no un candado. needSetup está en memoria y solo se usa para emitir "setup" al conectar. La ruta de escritura lo ignora y vuelve a contar.
El esquema es username UNIQUE únicamente. knex_init_db.js no impone una sola fila. El mismo nombre de usuario colisiona (SQLITE_CONSTRAINT). Nombres de usuario diferentes ambos entran.
Levanté louislam/uptime-kuma:2.5.5 en loopback con UPTIME_KUMA_DB_TYPE=sqlite para que se omita el asistente de base de datos v2. bcrypt con coste 10 es una carrera, no una garantía al primer intento. run.sh borra el volumen y reintenta. Salida 2 es un fallo. Siguiente tabla user vacía.
Cuando acertó: sqlite 1|labadmin y 2|UK-SETUP-TOCTOU-WITNESS. Setup secuencial tardío: Uptime Kuma has been initialized. Usuario extra logueado, setSettings disableAuth=true. El auto-login al reconectar es R.findOne("user"): el id más bajo, el operador. La identidad otpauth de prepare2FA es labadmin.
Cerca está GHSA-23q2-5gf8-gjpp, que es disableAuth re-enable fallando al cerrar sockets. Bug diferente.
Giros equivocados ya registrados: una fila de usuario tras setup concurrente (fallo de carrera, no un parche); UK-SETUP-TOCTOU-WITNESS ausente porque ambos sockets usaron el mismo nombre; tratar el HTML del dashboard 200 como SUCCESS; una reverse shell. Teatro. El oráculo son dos filas de sqlite, luego auto-login con disableAuth como id 1.
cd lab
./run.sh
Objetivo solo http://127.0.0.1:18141. Imagen oficial louislam/uptime-kuma:2.5.5, sqlite, primera ejecución. No publiques el puerto fuera de loopback.
user-rows [('1', 'labadmin'), ('2', 'UK-SETUP-TOCTOU-WITNESS')]
race-count=2 witness=True
sequential-late-setup initialized=True
setSettings-disableAuth ack ok
reconnect autoLogin=True loginRequired=False
bonus-autologin-identity operator=True id1=labadmin auto_is_id1=True
SUCCESS uptime-kuma-setup-toctou
Serializar setup: una transacción más una restricción de una sola fila, o un mutex alrededor de COUNT+INSERT. No tratar el auto-login de disableAuth como findOne del id más bajo. Hasta entonces, no expongas el setup de primera ejecución en una red compartida.
server.js setup · password-hash.js · knex_init_db.jsGNU Affero GPL v3.0. Ver LICENSE. Solo lab en loopback. Sin garantía.