
Detectar com segurança se um servidor UniFi OS é vulnerável ao CVE-2026-34908.
Um detector seguro para a cadeia de execução remota de código não autenticada no UniFi OS Server ≤ 5.0.6, divulgada no Boletim de Aviso de Segurança 064 da Ubiquiti:
| CVE | Classe | Papel na cadeia |
|---|---|---|
| CVE-2026-34908 / CVE-2026-34909 | Controle de acesso inadequado / path traversal | Bypass do gateway de autenticação (a base) |
| CVE-2026-34910 | Validação inadequada de entrada → injeção de comando | RCE não autenticada alcançada através do bypass |
Corrigido no UniFi OS Server 5.0.8.
Sim. O detector foi projetado para uso em produção e avaliação:
GET simples.VULNERABLE exige tanto que o bypass de autenticação tenha alcançado o manipulador vulnerável quanto que uma requisição de base (mesmo endpoint, sem bypass) tenha sido corretamente rejeitada com 401.O UniFi OS coloca o nginx na frente de seus serviços, que impõe autenticação por meio de uma subrequisição. Essa verificação de autenticação trata qualquer requisição cuja URI crua comece com /api/auth/validate-sso/ como pública, mas o nginx roteia para o backend usando a URI normalizada (ele decodifica percentuais %2f→/ e colapsa ../). Uma requisição como:
GET /api/auth/validate-sso/..%2f..%2f..%2fproxy/users/api/v2/ucs/update/latest_package
é portanto tratada como pública (prefixo cru) mas roteada para o endpoint interno autenticado de atualização de pacotes (caminho normalizado). Sem pkg_name, esse endpoint responde query param pkg_name required — provando que o bypass funciona e o manipulador vulnerável está acessível.
| Resposta da sonda | Veredito |
|---|---|
200 + marcadores de erro do manipulador | VULNERABLE |
400 (nginx rejeita a divergência) | PATCHED (5.0.8+) |
401, impressão digital do UniFi OS presente | INCONCLUSIVE (autenticação imposta / bypass bloqueado) |
401, sem impressão digital do UniFi OS | UNAFFECTED (não é um Servidor UniFi OS) |
| qualquer outra coisa, sem impressão digital do UniFi OS | UNAFFECTED (não é um Servidor UniFi OS) |
| qualquer outra coisa, impressão digital do UniFi OS presente | INCONCLUSIVE (resposta inesperada) |
O detector também envia uma requisição de base para o mesmo endpoint sem o bypass e exige que ela retorne 401 antes de declarar um host como vulnerável. A verificação testa o comportamento vulnerável diretamente — um veredito VULNERABLE significa que o bypass realmente alcançou o sink.
Sempre que a sonda não confirma positivamente VULNERABLE ou PATCHED, o detector busca a página raiz e procura pela impressão digital do portal de gerenciamento do UniFi OS (<title>UniFi OS</title>, window.UNIFI_OS_MANIFEST,id="portal-root", id="site-manager_portal-container"). Isso se aplica também a um 401: um host UniFi OS que impõe autenticação no bypass é INCONCLUSIVE, mas um proxy não-UniFi que simplesmente retorna 401 para tudo não carrega impressão digital e é reportado como UNAFFECTED. Portanto, todo resultado "não vulnerável" é fundamentado na impressão digital: marcadores presentes → INCONCLUSIVE; marcadores ausentes → UNAFFECTED (o CVE não se aplica).
# single host (default port 11443)
./cve_2026_34908_check.py 192.168.1.10
# explicit port / URL forms
./cve_2026_34908_check.py host-a:11443 https://host-b
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_34908_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_34908_check.py -f targets.txt --json > results.json
| Bandeira | Descrição |
|---|---|
targets | Um ou mais host, host:port, ou https://host:port |
-f, --targets-file FILE | Lê alvos de um arquivo (um por linha; comentários #) |
--brief | Uma única linha alinhada por alvo — ideal para escanear muitos hosts |
--json | Emite resultados JSON estruturados |
--timeout SECS | Timeout por requisição (padrão: 10) |
--no-color | Desativa saída colorida (também respeita NO_COLOR e não-TTY) |
Um console vulnerável (verboso, o padrão). A segunda linha é o detalhe do veredito; o marcador [!] e VULNERABLE são renderizados em vermelho em um TTY:
$ ./cve_2026_34908_check.py 192.168.1.10
[!] 192.168.1.10:11443: VULNERABLE
auth bypass reached the vulnerable handler (no command executed); baseline correctly 401
Um console corrigido (5.0.8+):
$ ./cve_2026_34908_check.py 192.168.1.11
[+] 192.168.1.11:11443: PATCHED
nginx rejected the normalized-path divergence (HTTP 400) — 5.0.8+ behavior
Um host UniFi OS confirmado que a sonda não pôde classificar → INCONCLUSIVE. A linha de detalhe distingue os dois casos (autenticação imposta vs. resposta inesperada):
$ ./cve_2026_34908_check.py 192.168.1.12 192.168.1.13
[?] 192.168.1.12:11443: INCONCLUSIVE
bypass blocked / auth enforced (HTTP 401) on a confirmed UniFi OS host — not confirmed vulnerable
[?] 192.168.1.13:11443: INCONCLUSIVE
UniFi OS detected, but probe returned an unexpected response: HTTP 302 text/html
Um host que não é UniFi OS → UNAFFECTED (o aviso não se aplica):
$ ./cve_2026_34908_check.py 203.0.113.9:443
[-] 203.0.113.9:443: UNAFFECTED
no UniFi OS web fingerprint on / (probe HTTP 404) — target is not a UniFi OS Server
Escaneie uma lista, uma linha alinhada por host (--brief). O código de saída é 1 se algum host for VULNERABLE, caso contrário 0 — útil em scripts:
$ ./cve_2026_34908_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE 192.168.1.10:11443
PATCHED 192.168.1.11:11443
INCONCLUSIVE 192.168.1.12:11443
UNAFFECTED 203.0.113.9:443
ERROR 10.0.0.1:11443 timeout
exit: 1
Saída legível por máquina para pipelines (--json). Cada resultado carrega o verdict mais o bypass.state/bypass.detail por trás dele; resultados ERROR carregam um campo error em vez de bypass:
$ ./cve_2026_34908_check.py 192.168.1.10 192.168.1.11 10.0.0.1 --json
[
{
"target": "192.168.1.10:11443",
"bypass": {
"state": "vulnerable",
"detail": "auth bypass reached the vulnerable handler (no command executed); baseline correctly 401"
},
"verdict": "VULNERABLE"
},
{
"target": "192.168.1.11:11443",
"bypass": {
"state": "patched",
"detail": "nginx rejected the normalized-path divergence (HTTP 400) — 5.0.8+ behavior"
},
"verdict": "PATCHED"
},
{
"target": "10.0.0.1:11443",
"verdict": "ERROR",
"error": "timeout"
}
]