Prova de conceito para uma corrida TOCTOU na configuração inicial do Socket.IO do Uptime Kuma, permitindo que um atacante não autenticado plante um segundo administrador oculto e desative a autenticação.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · uptime-kuma-setup-toctou
Uptime Kuma 2.5.5 - Louis Lam
O setup do Socket.IO na primeira execução faz COUNT dos usuários, aplica bcrypt com custo 10 e então faz INSERT. O nome de usuário é apenas UNIQUE. Não há transação, mutex ou restrição de linha única. Dois sockets já conectados que ambos veem count === 0 fazem o hash e o insert. O admin extra é invisível: não há UI de lista de usuários.
Um estranho que acessa um Kuma recém-iniciado na mesma sondagem que o operador pode plantar um segundo admin oculto, desativar a autenticação e aproveitar o auto-login nos monitores do operador.
| ID | ainda sem CVE |
| 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 |
| Produto | Uptime Kuma |
| Afetado | até 2.5.5 (c98982a), tabela user vazia na primeira execução |
| Autenticação | não autenticado |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Acessar um Kuma recém-iniciado ao mesmo tempo que o operador envia /setup. O bind padrão do Docker é 0.0.0.0:3001. Inserir uma segunda linha em user com um nome de usuário diferente. Fazer login como esse usuário, definir disableAuth e aproveitar o auto-login de R.findOne("user") nos monitores, notificações, chaves de API e configurações do operador.
Depois que a primeira linha é confirmada, um setup sequencial posterior é rejeitado. Isso não é "o setup permanece aberto". É uma corrida durante a janela do bcrypt. Instâncias já inicializadas com um admin não são afetadas por este bug.
Um IDOR latente de ativação/desativação de chave de API (UPDATE api_key SET active=? WHERE id=? sem user_id) também se torna ativo quando dois usuários existem. É uma consequência da linha extra, não do oráculo do lab.
Li primeiro o SECURITY.md, depois a lista de exclusão: dezoito avisos publicados no GitHub, mais o XSS público de siteId do Matomo, mais o SSRF de metadados de nuvem com vendor-wontfix. Depois vinte e duas caçadas na 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 status, docker.sock, prototype pollution, push/prometheus, banco de dados, execução de monitor, XSS de analytics, configurações, CSRF de websocket, XSS de frontend, jobs, Apprise, manutenção, master-delta. A maior parte dessa barra não retornou nada não publicado. A autenticação retornou.
O setup do primeiro usuário é um evento público do Socket.IO. Sem login. O setup faz COUNT(user), depois await passwordHash.generate, depois R.store. O passwordHash.generate é bcrypt.hash com custo 10. Isso é uma soneca, não um lock. needSetup está em memória e só é usado para emitir "setup" na conexão. O caminho de escrita o ignora e refaz a contagem.
O schema tem apenas o nome de usuário como UNIQUE. O knex_init_db.js não impõe uma única linha. O mesmo nome de usuário colide (SQLITE_CONSTRAINT). Nomes de usuário diferentes ambos entram.
Subi o louislam/uptime-kuma:2.5.5 em loopback com UPTIME_KUMA_DB_TYPE=sqlite para que o assistente de banco de dados v2 seja ignorado. O bcrypt com custo 10 é uma corrida, não uma garantia na primeira tentativa. O run.sh limpa o volume e tenta novamente. Saída 2 é um erro. Em seguida, tabela user vazia e nova tentativa.
Quando funcionou: sqlite 1|labadmin e 2|UK-SETUP-TOCTOU-WITNESS. Setup sequencial tardio: Uptime Kuma has been initialized. Usuário extra logado, setSettings disableAuth=true. O auto-login na reconexão é R.findOne("user"): menor id, o operador. A identidade otpauth do prepare2FA é labadmin.
O GHSA-23q2-5gf8-gjpp próximo é a falha de reativação do disableAuth em derrubar sockets. Bug diferente.
Caminhos errados já registrados: uma linha de usuário após setup concorrente (erro de corrida, não um patch); UK-SETUP-TOCTOU-WITNESS ausente porque ambos os sockets usaram o mesmo nome; tratar HTML 200 do dashboard como SUCCESS; um reverse shell. Teatro. O oráculo são duas linhas no sqlite, depois auto-login com disableAuth como id 1.
cd lab
./run.sh
Alvo apenas http://127.0.0.1:18141. Imagem oficial louislam/uptime-kuma:2.5.5, sqlite, primeira execução. Não publique a porta fora do 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 o setup: uma transação mais uma restrição de linha única, ou um mutex em torno de COUNT+INSERT. Não tratar o auto-login de disableAuth como findOne do menor id. Até então, não exponha o setup de primeira execução em uma rede compartilhada.
server.js setup · password-hash.js · knex_init_db.jsGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.