# FacturaScripts: Account takeover of any 2FA-enabled user

FacturaScripts 2FA validation endpoint accepts unauthenticated TOTP guesses without password, CSRF token or rate limiting, enabling brute-force account takeover of any 2FA user.

- Payout: Research
- Severity: critical
- Program: FacturaScripts
- Found by: janssensjelle
- Disclosed: 2026-07-13
- CVE: CVE-2026-47677
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-c67f-gmxw-mj93
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-c67f-gmxw-mj93
- Tags: area:web domain:web impact:ato impact:dos impact:privesc kind:research platform:ghsa sev:critical tech:php technique:race-window technique:source-review vuln:account-takeover vuln:auth-bypass vuln:mfa-bypass

**Target:** PHP web app login controller /login?action=two-factor-validation
**Root cause:** twoFactorValidationAction() issues a full session after only checking the TOTP code, without verifying the password step, CSRF token, or incident/rate-limit guard; VERIFICATION_WINDOW=8 keeps ~17 codes valid at once.
**Steps:**
1. POST to `/login` with `action=two-factor-validation`, `fsNick=<victim>`, `fsTwoFactorCode=<6-digit guess>`
2. Loop random 6-digit codes across threads; no rate limit blocks requests
3. Detect success by 302 response with `fsLogkey` in Set-Cookie
4. Reuse returned `fsNick`/`fsLogkey` cookies to access `/ListUser` as victim
**How found:** Code review of Login.php comparing action handlers: noticed loginAction and changePasswordAction call validateFormToken() and userHasManyIncidents() before work, while twoFactorValidationAction does neither; then checked TwoFactorManager::VERIFICATION_WINDOW=8 vs google2fa default 1.
**Exploitation:** Unauthenticated attacker knowing only the victim's nick sends random TOTP guesses; with ~17 valid codes per window and no rate limiting, ~40k attempts (50%) succeed in minutes from one IP, yielding full session cookies for admin takeover.
**Escalation:** Admin takeover grants full DB read/write, API key issuance, plugin upload and code install; failed guesses also lock out the legitimate user (DoS).
**Insight:** When auditing multi-step auth flows, diff each step handler against the primary login handler for missing guards (CSRF, incident counters, pending-state nonce); a 2FA step that doesn't require proof of the password step is a standalone auth bypass, and inflated TOTP verification windows multiply brute-force odds.

## Proof of concept (copied exactly from the original report)

**Observed successful brute-force run output**

```
[*] target=http://localhost:9999  nick=victim  threads=32
  [  2.2s] attempts=   1094  rps=  493
  [ 24.4s] attempts=  11535  rps=  473
  [ 50.0s] attempts=  23420  rps=  468
  [100.7s] attempts=  41247  rps=  410
  [144.9s] attempts=  55418  rps=  383

[+] FOUND code=055473 after 55,773 attempts in 146.0s
[+] Cookies: fsNick=victim; fsLogkey=47qZDmjcHaS2z2pLsqKWsKbb8vlGfZaYEiUUfcvWHlDXSZlI9LFg8ux7EYX1fzTkeNSgM5ASQ7s5ohr8ROAclvlK1GCxACia21N; fsLang=en_EN
```

**Reproduction command for brute force**

```
# brute force (only nick known) — secret on the server can be anything
NICK=victim THREADS=32 .venv/bin/python poc_2fa_brute.py
```

