Preuve de concept pour une condition de course TOCTOU dans la configuration Socket.IO du premier lancement d'Uptime Kuma, permettant à un attaquant non authentifié de créer un second administrateur caché et de désactiver l'authentification.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · uptime-kuma-setup-toctou
Uptime Kuma 2.5.5 - Louis Lam
Lors du premier lancement, l'événement Socket.IO setup compte les utilisateurs (COUNT), effectue un hachage bcrypt avec un coût de 10, puis exécute un INSERT. Le nom d'utilisateur est seulement UNIQUE. Il n'y a ni transaction, ni mutex, ni contrainte d'une seule ligne. Deux sockets déjà connectés qui voient tous deux count === 0 vont tous deux hacher et tous deux insérer. L'administrateur supplémentaire est invisible : il n'existe aucune interface de liste des utilisateurs.
Un inconnu qui atteint une instance Kuma fraîchement démarrée lors du même cycle d'interrogation que l'opérateur peut implanter un second administrateur caché, désactiver l'authentification et profiter de l'auto-connexion pour accéder aux moniteurs de l'opérateur.
| ID | pas encore de CVE |
| CWE | CWE-362, CWE-367 |
| CVSS | Élevé : 7.4 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Produit | Uptime Kuma |
| Affecté | jusqu'à 2.5.5 (c98982a), table user vide au premier lancement |
| Authentification | non authentifié |
| Licence | GNU Affero GPL v3.0 |
| Labo | 127.0.0.1 uniquement |
Atteindre une instance Kuma fraîchement démarrée au moment où l'opérateur soumet /setup. La liaison Docker par défaut est 0.0.0.0:3001. Insérer une seconde ligne user avec un nom d'utilisateur différent. Se connecter en tant que cet utilisateur, définir disableAuth, et profiter de l'auto-connexion R.findOne("user") pour accéder aux moniteurs, notifications, clés API et paramètres de l'opérateur.
Une fois la première ligne validée, un setup séquentiel ultérieur est rejeté. Il ne s'agit pas de « le setup reste ouvert ». C'est une condition de course pendant la fenêtre bcrypt. Les instances déjà initialisées avec un administrateur ne sont pas concernées par ce bug.
Un IDOR latent d'activation/désactivation de clé API (UPDATE api_key SET active=? WHERE id=? sans user_id) devient également exploitable dès que deux utilisateurs existent. Conséquence de la ligne supplémentaire, pas de l'oracle du labo.
J'ai d'abord lu SECURITY.md, puis la liste des exclusions : dix-huit avis GitHub publiés, plus le XSS public siteId de Matomo, plus le SSRF cloud-metadata wontfix du fournisseur. Ensuite, vingt-deux recherches sur le tag 2.5.5 (c98982a) : auth/setup/2FA, IDOR de socket, SSRF HTTP/moniteur, restes de SSTI, path/LFI, navigateur réel, plugin/tailscale, page de statut, docker.sock, pollution de prototype, push/prometheus, base de données, exécution de moniteur, XSS analytics, paramètres, CSRF websocket, XSS frontend, jobs, Apprise, maintenance, master-delta. La majeure partie de ce périmètre n'a rien renvoyé de non publié. L'authentification, si.
Le setup du premier utilisateur est un événement Socket.IO public. Pas de connexion. setup fait COUNT(user), puis await passwordHash.generate, puis R.store. passwordHash.generate est bcrypt.hash avec un coût de 10. C'est une sieste, pas un verrou. needSetup est en mémoire et n'est utilisé que pour émettre "setup" à la connexion. Le chemin d'écriture l'ignore et recompte.
Le schéma n'a que le nom d'utilisateur UNIQUE. knex_init_db.js n'impose pas une seule ligne. Un même nom d'utilisateur entre en collision (SQLITE_CONSTRAINT). Des noms d'utilisateur différents passent tous les deux.
J'ai lancé louislam/uptime-kuma:2.5.5 sur loopback avec UPTIME_KUMA_DB_TYPE=sqlite afin de contourner l'assistant de base de données v2. Le coût bcrypt 10 est une course, pas une garantie du premier coup. run.sh efface le volume et réessaie. Un code de sortie 2 est un échec. Table user vide fraîche ensuite.
Quand ça a fonctionné : sqlite 1|labadmin et 2|UK-SETUP-TOCTOU-WITNESS. Setup séquentiel tardif : Uptime Kuma has been initialized. Utilisateur supplémentaire connecté, setSettings disableAuth=true. L'auto-connexion à la reconnexion est R.findOne("user") : l'id le plus bas, l'opérateur. L'identité otpauth de prepare2FA est labadmin.
Le GHSA-23q2-5gf8-gjpp voisin concerne la réactivation de disableAuth qui échoue à fermer les sockets. Bug différent.
Fausses pistes déjà consignées : une seule ligne utilisateur après un setup concurrent (échec de course, pas un correctif) ; UK-SETUP-TOCTOU-WITNESS manquant parce que les deux sockets utilisaient le même nom ; considérer le HTML du tableau de bord 200 comme SUCCESS ; un reverse shell. Du théâtre. L'oracle, ce sont deux lignes sqlite, puis l'auto-connexion disableAuth en tant qu'id 1.
cd lab
./run.sh
Cible uniquement http://127.0.0.1:18141. Image officielle louislam/uptime-kuma:2.5.5, sqlite, premier lancement. Ne pas exposer le port hors 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
Sérialiser setup : une transaction plus une contrainte d'une seule ligne, ou un mutex autour de COUNT+INSERT. Ne pas traiter l'auto-connexion disableAuth comme un findOne de l'id le plus bas. En attendant, ne pas exposer le setup du premier lancement sur un réseau partagé.
server.js setup · password-hash.js · knex_init_db.jsGNU Affero GPL v3.0. Voir LICENSE. Labo loopback uniquement. Aucune garantie.