Proof-of-concept per una race TOCTOU nella configurazione Socket.IO al primo avvio di Uptime Kuma, che consente a un attaccante non autenticato di inserire un secondo amministratore nascosto e disabilitare l'autenticazione.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · uptime-kuma-setup-toctou
Uptime Kuma 2.5.5 - Louis Lam
Al primo avvio, il setup di Socket.IO esegue un COUNT degli utenti, applica bcrypt a costo 10, poi esegue l'INSERT. Lo username è UNIQUE soltanto. Non c'è transazione, mutex o vincolo di riga singola. Due socket già connessi che vedono entrambi count === 0 eseguono entrambi l'hash ed entrambi l'insert. L'admin in più è invisibile: non esiste una UI per la lista utenti.
Uno sconosciuto che colpisce una Kuma appena avviata nello stesso poll dell'operatore può piazzare un secondo admin nascosto, disattivare l'autenticazione e sfruttare l'auto-login per accedere ai monitor dell'operatore.
| ID | nessuna CVE al momento |
| CWE | CWE-362, CWE-367 |
| CVSS | High: 7.4 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Prodotto | Uptime Kuma |
| Affetto | fino alla 2.5.5 (c98982a), tabella user vuota al primo avvio |
| Auth | non autenticato |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Colpire una Kuma appena avviata nello stesso momento in cui l'operatore invia /setup. Il bind Docker predefinito è 0.0.0.0:3001. Inserire una seconda riga user con uno username diverso. Accedere come quell'utente, impostare disableAuth e sfruttare l'auto-login di R.findOne("user") per accedere ai monitor, alle notifiche, alle API key e alle impostazioni dell'operatore.
Dopo che la prima riga viene confermata, un successivo setup sequenziale viene rifiutato. Non è "il setup resta aperto". È una race durante la finestra di bcrypt. Le istanze già inizializzate con un admin non sono affette da questo bug.
Un IDOR latente di abilitazione/disabilitazione delle API key (UPDATE api_key SET active=? WHERE id=? senza user_id) diventa anch'esso sfruttabile una volta che esistono due utenti. Conseguenza della riga in più, non dell'oracolo del lab.
Ho letto prima SECURITY.md, poi la skip list: diciotto advisory GitHub pubblicati, più l'XSS pubblico di Matomo siteId, più l'SSRF su cloud-metadata con vendor-wontfix. Poi ventidue ricerche sul tag 2.5.5 (c98982a): auth/setup/2FA, socket IDOR, HTTP/monitor SSRF, residui SSTI, path/LFI, real-browser, plugin/tailscale, status-page, docker.sock, prototype pollution, push/prometheus, database, monitor exec, analytics XSS, settings, websocket CSRF, frontend XSS, jobs, Apprise, maintenance, master-delta. La maggior parte di quell'area non ha restituito nulla di non pubblicato. L'auth no.
Il setup del primo utente è un evento Socket.IO pubblico. Nessun login. setup esegue COUNT(user), poi await passwordHash.generate, poi R.store. passwordHash.generate è bcrypt.hash a costo 10. È un pisolino, non un lock. needSetup è in memoria e viene usato solo per emettere "setup" alla connessione. Il percorso di scrittura lo ignora e riesegue il count.
Lo schema ha UNIQUE solo sullo username. knex_init_db.js non impone una riga singola. Lo stesso username va in collisione (SQLITE_CONSTRAINT). Username diversi passano entrambi.
Ho avviato louislam/uptime-kuma:2.5.5 su loopback con UPTIME_KUMA_DB_TYPE=sqlite così da saltare la procedura guidata del database v2. bcrypt a costo 10 è una race, non una garanzia al primo tentativo. run.sh cancella il volume e riprova. Exit 2 è un tentativo fallito. Poi tabella user vuota da capo.
Quando ha funzionato: sqlite 1|labadmin e 2|UK-SETUP-TOCTOU-WITNESS. Setup sequenziale tardivo: Uptime Kuma has been initialized. Utente in più loggato, setSettings disableAuth=true. L'auto-login alla riconnessione è R.findOne("user"): id più basso, l'operatore. L'identità otpauth di prepare2FA è labadmin.
La vicina GHSA-23q2-5gf8-gjpp riguarda il fallimento della riattivazione di disableAuth nel chiudere i socket. Bug diverso.
Vicoli ciechi già registrati: una sola riga utente dopo setup concorrente (race mancata, non una patch); UK-SETUP-TOCTOU-WITNESS mancante perché entrambi i socket usavano lo stesso nome; considerare l'HTML della dashboard 200 come SUCCESS; una reverse shell. Teatro. L'oracolo sono due righe sqlite, poi l'auto-login con disableAuth come id 1.
cd lab
./run.sh
Target solo http://127.0.0.1:18141. Immagine ufficiale louislam/uptime-kuma:2.5.5, sqlite, primo avvio. Non pubblicare la porta fuori dal 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
Serializzare setup: una transazione più un vincolo di riga singola, oppure un mutex attorno a COUNT+INSERT. Non trattare l'auto-login con disableAuth come findOne dell'id più basso. Fino ad allora, non esporre il setup del primo avvio su una rete condivisa.
server.js setup · password-hash.js · knex_init_db.jsGNU Affero GPL v3.0. Vedi LICENSE. Solo lab su loopback. Nessuna garanzia.