#!/usr/bin/env python3 """ CVE-2025-59718 Fortinet Authentication Bypass Checker Detecta vulnerabilidad en FortiOS, FortiProxy, FortiSwitchManager y FortiWeb Autor: m10sec Versión: 1.2 """ import argparse import json import re import socket import ssl from dataclasses import dataclass, asdict from typing import Optional, Tuple, List, Dict from urllib.parse import urlparse from http.client import HTTPSConnection from datetime import datetime import paramiko from colorama import init, Fore, Style init() # ==================== BANNER Y UTILIDADES ==================== def banner(): print(Fore.GREEN + Style.BRIGHT + r""" ░▒▓████████▓▒░▒▓██████▓▒░░▒▓███████▓▒░▒▓████████▓▒░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ """ + Style.RESET_ALL) def print_banner(): print(" Fortinet Checker CVE-2025-59718 v1.3 ") print(" by m10sec (2025) m10sec@proton.me ") print(" CVEs: CVE-2025-59718 / CVE-2025-59719 ") print("=" * 55) print() def c_ok(msg): return f"{Fore.GREEN}[+]{Style.RESET_ALL} {msg}" def c_warn(msg): return f"{Fore.YELLOW}[!]{Style.RESET_ALL} {msg}" def c_err(msg): return f"{Fore.RED}[X]{Style.RESET_ALL} {msg}" def c_info(msg): return f"{Fore.CYAN}[*]{Style.RESET_ALL} {msg}" def c_crit(msg): return f"{Fore.RED + Style.BRIGHT}[!!!]{Style.RESET_ALL} {msg}" # ==================== VERSION HELPERS ==================== def parse_ver(v: str) -> Tuple[int, int, int]: """Parse version string to tuple (major, minor, patch)""" m = re.match(r"^\s*(\d+)\.(\d+)\.(\d+)", v) if not m: raise ValueError(f"Version no parseable: {v!r}") return tuple(map(int, m.groups())) def in_range(v: Tuple[int, int, int], lo: Tuple[int, int, int], hi: Tuple[int, int, int]) -> bool: """Check if version is within range (inclusive)""" return lo <= v <= hi # ==================== FINDING MODEL ==================== @dataclass class Finding: target: str mode: str # ssh|passive host: str port: int product: Optional[str] = None version: Optional[str] = None vulnerable_version: Optional[bool] = None forticloud_sso_enabled: Optional[bool] = None # Final verdict: EXISTS / NOT_FOUND / POTENTIAL / UNKNOWN verdict: str = "UNKNOWN" # Additional metadata confidence: str = "unknown" # high, medium, low indicators: List[str] = None notes: List[str] = None def __post_init__(self): if self.notes is None: self.notes = [] if self.indicators is None: self.indicators = [] # ==================== CVE DETECTION LOGIC ==================== def detect_product_and_version(status_output: str) -> Tuple[Optional[str], Optional[str]]: """Detect Fortinet product and version from system status output""" # FortiOS/FortiProxy: m = re.search(r"Version:\s*(FortiOS|FortiProxy)\s*v?(\d+\.\d+\.\d+)", status_output, re.IGNORECASE) if m: return m.group(1), m.group(2) # FortiSwitchManager: m2 = re.search(r"(FortiSwitchManager).*?v?(\d+\.\d+\.\d+)", status_output, re.IGNORECASE) if m2: return "FortiSwitchManager", m2.group(2) # FortiWeb: m3 = re.search(r"(FortiWeb).*?v?(\d+\.\d+\.\d+)", status_output, re.IGNORECASE) if m3: return "FortiWeb", m3.group(2) return None, None def is_vulnerable(product: Optional[str], version: Optional[str]) -> bool: """ Check if product version is vulnerable to CVE-2025-59718 Based on official Fortinet advisory FG-IR-25-647 """ if not product or not version: return False try: v = parse_ver(version) except ValueError: return False p = product.lower() # FortiOS vulnerable ranges if p == "fortios": return any([ in_range(v, parse_ver("7.6.0"), parse_ver("7.6.3")), in_range(v, parse_ver("7.4.0"), parse_ver("7.4.8")), in_range(v, parse_ver("7.2.0"), parse_ver("7.2.11")), in_range(v, parse_ver("7.0.0"), parse_ver("7.0.17")), ]) # FortiProxy vulnerable ranges if p == "fortiproxy": return any([ in_range(v, parse_ver("7.6.0"), parse_ver("7.6.3")), in_range(v, parse_ver("7.4.0"), parse_ver("7.4.10")), in_range(v, parse_ver("7.2.0"), parse_ver("7.2.14")), in_range(v, parse_ver("7.0.0"), parse_ver("7.0.21")), ]) # FortiSwitchManager vulnerable ranges if p == "fortiswitchmanager": return any([ in_range(v, parse_ver("7.2.0"), parse_ver("7.2.6")), in_range(v, parse_ver("7.0.0"), parse_ver("7.0.5")), ]) # FortiWeb vulnerable ranges if p == "fortiweb": return any([ in_range(v, parse_ver("8.0.0"), parse_ver("8.0.0")), in_range(v, parse_ver("7.6.0"), parse_ver("7.6.4")), in_range(v, parse_ver("7.4.0"), parse_ver("7.4.9")), # 7.2 and 7.0 NOT affected ]) return False def get_patch_version(product: Optional[str], version: Optional[str]) -> Optional[str]: """Get recommended patch version for vulnerable products""" if not product or not version: return None try: v = parse_ver(version) except ValueError: return None p = product.lower() if p == "fortios": if in_range(v, parse_ver("7.6.0"), parse_ver("7.6.3")): return "7.6.4" elif in_range(v, parse_ver("7.4.0"), parse_ver("7.4.8")): return "7.4.9" elif in_range(v, parse_ver("7.2.0"), parse_ver("7.2.11")): return "7.2.12" elif in_range(v, parse_ver("7.0.0"), parse_ver("7.0.17")): return "7.0.18" elif p == "fortiproxy": if in_range(v, parse_ver("7.6.0"), parse_ver("7.6.3")): return "7.6.4" elif in_range(v, parse_ver("7.4.0"), parse_ver("7.4.10")): return "7.4.11" elif in_range(v, parse_ver("7.2.0"), parse_ver("7.2.14")): return "7.2.15" elif in_range(v, parse_ver("7.0.0"), parse_ver("7.0.21")): return "7.0.22" elif p == "fortiswitchmanager": if in_range(v, parse_ver("7.2.0"), parse_ver("7.2.6")): return "7.2.7" elif in_range(v, parse_ver("7.0.0"), parse_ver("7.0.5")): return "7.0.6" elif p == "fortiweb": if in_range(v, parse_ver("8.0.0"), parse_ver("8.0.0")): return "8.0.1" elif in_range(v, parse_ver("7.6.0"), parse_ver("7.6.4")): return "7.6.5" elif in_range(v, parse_ver("7.4.0"), parse_ver("7.4.9")): return "7.4.10" return None def infer_forticloud_sso_enabled(global_output: str) -> Optional[bool]: """ Parse FortiCloud SSO login setting from system global output Returns: True if enabled, False if disabled, None if unknown """ m = re.search(r"set\s+admin-forticloud-sso-login\s+(enable|disable)", global_output, re.IGNORECASE) if not m: return None return m.group(1).lower() == "enable" # ==================== TARGET PARSING ==================== def normalize_target_to_host_port(t: str, default_port: int) -> Tuple[str, int, str]: """ Parse target string to (host, port, original_string) Accepts: IP, hostname, host:port, https://host[:port]/path, ssh://host[:port] """ t = t.strip() if "://" in t: u = urlparse(t) host = u.hostname or t port = u.port or (443 if u.scheme in ("https",) else default_port) return host, port, t # host:port format if ":" in t and not re.match(r"^\[.*\]$", t): host, p = t.rsplit(":", 1) if p.isdigit(): return host.strip(), int(p), t # host only return t, default_port, t def read_targets_file(path: str, default_port: int) -> List[Tuple[str, int, str]]: """Read targets from file (one per line, # for comments)""" out = [] with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue host, port, raw = normalize_target_to_host_port(line, default_port) out.append((host, port, raw)) return out # ==================== SSH MODE ==================== def ssh_run(host: str, port: int, user: str, password: Optional[str], keyfile: Optional[str], cmd: str, timeout=10) -> str: """Execute SSH command and return output""" client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: if keyfile: pkey = paramiko.RSAKey.from_private_key_file(keyfile) client.connect(host, port=port, username=user, pkey=pkey, timeout=timeout, banner_timeout=timeout) else: client.connect(host, port=port, username=user, password=password, timeout=timeout, banner_timeout=timeout) _, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode(errors="ignore") err = stderr.read().decode(errors="ignore") return out + ("\n" + err if err.strip() else "") finally: client.close() def check_ssh(host: str, port: int, raw: str, user: str, password: Optional[str], keyfile: Optional[str], timeout: int) -> Finding: """ SSH-based verification (most reliable) Requires valid credentials """ notes: List[str] = [] indicators: List[str] = [] f = Finding(target=raw, mode="ssh", host=host, port=port, notes=notes, indicators=indicators) try: print(c_info(f"Conectando vía SSH a {host}:{port}...")) # Get system status status = ssh_run(host, port, user, password, keyfile, "get system status", timeout=timeout) product, version = detect_product_and_version(status) f.product, f.version = product, version if not product or not version: notes.append("No se pudo inferir producto/versión desde 'get system status'") f.verdict = "UNKNOWN" f.confidence = "low" return f indicators.append(f"Producto detectado: {product} v{version}") # Check if version is vulnerable f.vulnerable_version = is_vulnerable(product, version) if f.vulnerable_version: indicators.append(f"⚠️ Versión {version} está en rango VULNERABLE") patch_version = get_patch_version(product, version) if patch_version: notes.append(f"Actualizar a versión {patch_version} o superior") else: indicators.append(f"✓ Versión {version} NO es vulnerable") # Check FortiCloud SSO setting (only for FortiOS/FortiProxy) if product and product.lower() in ("fortios", "fortiproxy"): try: glob = ssh_run(host, port, user, password, keyfile, "show system global | grep -i admin-forticloud-sso-login", timeout=timeout) f.forticloud_sso_enabled = infer_forticloud_sso_enabled(glob) if glob else None if f.forticloud_sso_enabled is True: indicators.append("⚠️ FortiCloud SSO login está HABILITADO") elif f.forticloud_sso_enabled is False: indicators.append("✓ FortiCloud SSO login está DESHABILITADO") else: indicators.append("? No se pudo determinar estado de FortiCloud SSO") except Exception as e: notes.append(f"Error al verificar FortiCloud SSO: {e}") # Final verdict logic if f.vulnerable_version is True: if f.forticloud_sso_enabled is True: f.verdict = "EXISTS" f.confidence = "high" notes.append("🚨 CRÍTICO: Sistema VULNERABLE y FortiCloud SSO HABILITADO") notes.append("🚨 Este sistema está siendo explotado activamente in-the-wild") notes.append("🚨 ACCIÓN INMEDIATA REQUERIDA") elif f.forticloud_sso_enabled is False: f.verdict = "NOT_FOUND" f.confidence = "high" notes.append("Versión vulnerable pero FortiCloud SSO está deshabilitado") notes.append("Sistema no explotable por esta vía (aún así, actualizar ASAP)") else: f.verdict = "POTENTIAL" f.confidence = "medium" notes.append("⚠️ Versión vulnerable, estado FortiCloud SSO desconocido") notes.append("Verificar manualmente: System > Settings > FortiCloud SSO") elif f.vulnerable_version is False: f.verdict = "NOT_FOUND" f.confidence = "high" notes.append("Sistema no vulnerable (versión parcheada)") else: f.verdict = "UNKNOWN" f.confidence = "low" return f except (paramiko.SSHException, socket.error, TimeoutError) as e: f.verdict = "UNKNOWN" f.confidence = "low" notes.append(f"Error SSH: {type(e).__name__}: {e}") return f # ==================== ACTIVE VULNERABILITY TESTING ==================== def test_forticloud_sso_endpoint(host: str, port: int, timeout: int) -> Dict: """ Test if FortiCloud SSO authentication endpoint exists and is vulnerable CVE-2025-59718: Improper Verification of Cryptographic Signature Returns dict with: - endpoint_exists: bool - endpoint_accessible: bool - response_code: int - vulnerable_behavior: bool (indica comportamiento sospechoso) """ result = { 'endpoint_exists': False, 'endpoint_accessible': False, 'response_code': None, 'vulnerable_behavior': False, 'details': [] } ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE # Endpoints conocidos de FortiCloud SSO test_endpoints = [ '/api/v2/authentication/forticloud', '/api/v2/cmdb/system/admin', '/remote/logincheck', '/remote/fgt_lang', ] for endpoint in test_endpoints: try: conn = HTTPSConnection(host, port=port, timeout=timeout, context=ctx) # Test básico GET conn.request("GET", endpoint) resp = conn.getresponse() body = resp.read().decode('utf-8', errors='ignore') if resp.status != 404: result['endpoint_exists'] = True result['response_code'] = resp.status result['details'].append(f"{endpoint}: HTTP {resp.status}") # Comportamientos que indican FortiCloud SSO activo if endpoint == '/api/v2/authentication/forticloud': if resp.status in [200, 401, 403]: result['endpoint_accessible'] = True result['details'].append("FortiCloud auth endpoint responde") # Si responde 200 sin autenticación válida = sospechoso if resp.status == 200: result['vulnerable_behavior'] = True result['details'].append("⚠️ Endpoint responde 200 sin auth") # Buscar indicadores en respuesta if 'forticloud' in body.lower() or 'sso' in body.lower(): result['details'].append("Respuesta contiene referencias SSO") # Otros endpoints que confirman FortiCloud habilitado if 'forticloud' in body.lower(): result['details'].append(f"Referencias FortiCloud en {endpoint}") conn.close() except Exception as e: result['details'].append(f"{endpoint}: {type(e).__name__}") return result def test_authentication_bypass(host: str, port: int, timeout: int) -> Dict: """ Prueba activa (no destructiva) de bypass de autenticación CVE-2025-59718: Authentication bypass via cryptographic signature flaw NOTA: Esta es una prueba SEGURA que solo verifica el comportamiento, NO intenta explotar el sistema. """ result = { 'bypass_possible': False, 'evidence': [], 'test_performed': False } try: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE conn = HTTPSConnection(host, port=port, timeout=timeout, context=ctx) # Test 1: Intentar acceso a endpoint administrativo conn.request("GET", "/api/v2/monitor/system/status") resp = conn.getresponse() body = resp.read().decode('utf-8', errors='ignore') result['test_performed'] = True # Si el endpoint responde con información sin credenciales = problema if resp.status == 200: if any(keyword in body.lower() for keyword in ['version', 'hostname', 'serial']): result['bypass_possible'] = True result['evidence'].append("API status endpoint accesible sin auth") # Test 2: Verificar headers de respuesta if 'x-frame-options' not in [h.lower() for h in resp.getheaders()]: result['evidence'].append("Falta X-Frame-Options (configuración débil)") conn.close() except Exception as e: result['evidence'].append(f"Test error: {type(e).__name__}") return result # ==================== PASSIVE MODE ==================== def https_fetch_page(host: str, port: int, timeout: int) -> Tuple[dict, str, Optional[str]]: """ Fetch HTTPS page and return (headers, body, cert_subject) """ ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE conn = HTTPSConnection(host, port=port, timeout=timeout, context=ctx) conn.request("GET", "/") resp = conn.getresponse() headers = {k.lower(): v for k, v in resp.getheaders()} body = resp.read().decode('utf-8', errors='ignore') # Extract cert subject cert_subject = None try: sock = conn.sock if sock: cert = sock.getpeercert() if cert and "subject" in cert: cert_subject = str(cert["subject"]) except Exception: pass conn.close() return headers, body, cert_subject def analyze_fortinet_fingerprint(headers: dict, body: str, cert_subject: Optional[str]) -> Dict: """ Analyze page for Fortinet fingerprints Returns detection metadata """ indicators = [] product = None confidence = "none" # High confidence HTML fingerprints html_fingerprints = { "ftnt-fortinet-grid": "Fortinet icon class", "NEUTRINO_THEME": "Neutrino UI framework (Fortinet)", "fgt_lang": "FortiGate language variable", "/static/js/login.js": "Fortinet login script", "/favicon/fortinet": "Fortinet favicon" } for fp, desc in html_fingerprints.items(): if fp in body: indicators.append(desc) confidence = "high" # Detect specific product if "FortiGate" in body: product = "FortiOS" indicators.append("FortiGate title tag detected") elif "FortiProxy" in body or "FortiProxy" in body: product = "FortiProxy" indicators.append("FortiProxy detected in HTML") elif "FortiWeb" in body: product = "FortiWeb" indicators.append("FortiWeb detected in HTML") elif "FortiSwitchManager" in body: product = "FortiSwitchManager" indicators.append("FortiSwitchManager detected") # Check for FortiCloud SSO indicators in HTML forticloud_indicators = [ "/api/v2/authentication/forticloud", "forticloud-sso", "admin-forticloud-sso-login", "ftm_push_enabled" ] forticloud_present = any(ind in body.lower() for ind in forticloud_indicators) # Version hints in HTML/JS version_hints = re.findall(r'(?:version|ver|v)[\s:=]+["\']?(\d+\.\d+\.\d+)', body, re.IGNORECASE) # Fallback: check headers and cert if not indicators: server = headers.get("server", "") blob = f"{server} {cert_subject or ''}".lower() fortinet_keywords = ["fortigate", "fortinet", "fortiproxy", "fortios", "fgt"] if any(k in blob for k in fortinet_keywords): indicators.append("Fortinet fingerprint in headers/cert") confidence = "medium" is_fortinet = len(indicators) > 0 return { 'is_fortinet': is_fortinet, 'product': product, 'confidence': confidence, 'indicators': indicators, 'forticloud_present': forticloud_present, 'version_hints': list(set(version_hints[:3])) # Dedupe and limit } def check_passive(host: str, port: int, raw: str, timeout: int, active_test: bool = False) -> Finding: """ Passive verification (no credentials required) Less reliable but useful for initial recon If active_test=True, performs additional active vulnerability checks """ notes: List[str] = [] indicators: List[str] = [] f = Finding(target=raw, mode="passive", host=host, port=port, notes=notes, indicators=indicators) try: print(c_info(f"Analizando {host}:{port} en modo pasivo (HTTPS)...")) # Fetch page headers, body, cert_subject = https_fetch_page(host, port, timeout) # Analyze fingerprints analysis = analyze_fortinet_fingerprint(headers, body, cert_subject) if not analysis['is_fortinet']: f.verdict = "NOT_FOUND" f.confidence = "medium" notes.append("No se detectó fingerprint de Fortinet") indicators.append("Dispositivo no parece ser Fortinet") return f # Fortinet detected f.product = analysis['product'] f.confidence = analysis['confidence'] f.indicators.extend(analysis['indicators']) # Add version hints if found if analysis['version_hints']: notes.append(f"Posibles versiones en HTML: {', '.join(analysis['version_hints'])}") # Try to check if version is vulnerable for version_hint in analysis['version_hints']: try: if is_vulnerable(f.product, version_hint): f.vulnerable_version = True f.version = version_hint indicators.append(f"⚠️ Versión detectada {version_hint} es VULNERABLE") break except: pass # ACTIVE VULNERABILITY TESTING if active_test: print(c_info(" → Ejecutando pruebas activas de vulnerabilidad...")) # Test 1: FortiCloud SSO endpoint sso_test = test_forticloud_sso_endpoint(host, port, timeout) if sso_test['endpoint_exists']: indicators.append("✓ Endpoints FortiCloud SSO detectados") f.forticloud_sso_enabled = True for detail in sso_test['details']: indicators.append(f" {detail}") if sso_test['vulnerable_behavior']: f.verdict = "EXISTS" f.confidence = "high" notes.append("🚨 COMPORTAMIENTO VULNERABLE DETECTADO") notes.append("🚨 Endpoint FortiCloud muestra respuesta anómala") notes.append("🚨 Sistema probablemente EXPLOTABLE") indicators.append("⚠️ Respuesta de endpoint indica vulnerabilidad") else: f.verdict = "POTENTIAL" notes.append("⚠️ FortiCloud SSO habilitado pero comportamiento normal") notes.append("Verificar con modo SSH para confirmar versión") # Test 2: Authentication bypass test bypass_test = test_authentication_bypass(host, port, timeout) if bypass_test['test_performed']: if bypass_test['bypass_possible']: f.verdict = "EXISTS" f.confidence = "high" notes.append("🚨 PRUEBA DE BYPASS POSITIVA") notes.append("🚨 API accesible sin autenticación válida") for evidence in bypass_test['evidence']: indicators.append(f" {evidence}") # Si las pruebas activas no detectaron nada específico if not sso_test['endpoint_exists'] and not bypass_test['bypass_possible']: if f.vulnerable_version: f.verdict = "POTENTIAL" notes.append("⚠️ Versión vulnerable detectada pero pruebas activas negativas") notes.append("FortiCloud SSO podría estar deshabilitado") else: f.verdict = "POTENTIAL" notes.append("No se detectaron endpoints vulnerables en pruebas activas") notes.append("Esto NO descarta la vulnerabilidad (puede estar mal configurado)") # WITHOUT ACTIVE TESTING (original logic) else: if analysis['forticloud_present']: f.verdict = "POTENTIAL" notes.append("⚠️ Dispositivo Fortinet con indicadores de FortiCloud SSO") notes.append("⚠️ Sin credenciales no se puede confirmar versión ni estado SSO") notes.append("🔍 RECOMENDACIÓN: Usar --active-test para pruebas activas") notes.append("🔍 O usar modo SSH con credenciales para verificación completa") indicators.append("FortiCloud SSO references found in HTML") else: f.verdict = "POTENTIAL" notes.append("Dispositivo Fortinet detectado") notes.append("No se detectaron indicadores de FortiCloud SSO en HTML público") notes.append("⚠️ NOTA: FortiCloud SSO puede estar habilitado sin referencias públicas") notes.append("🔍 RECOMENDACIÓN: Usar --active-test o modo SSH") return f except Exception as e: f.verdict = "UNKNOWN" f.confidence = "low" notes.append(f"Error en análisis pasivo: {type(e).__name__}: {e}") return f # ==================== PRETTY OUTPUT ==================== def print_finding(f: Finding): """Print finding in colored, human-readable format""" print("\n" + "=" * 70) print(f"{Fore.CYAN}Target:{Style.RESET_ALL} {f.target}") print(f"{Fore.CYAN}Mode:{Style.RESET_ALL} {f.mode}") # Verdict with color if f.verdict == "EXISTS": verdict_str = c_crit(f"VULNERABLE - {f.verdict}") elif f.verdict == "POTENTIAL": verdict_str = c_warn(f"POTENTIAL - {f.verdict}") elif f.verdict == "NOT_FOUND": verdict_str = c_ok(f"NOT VULNERABLE - {f.verdict}") else: verdict_str = c_info(f"UNKNOWN - {f.verdict}") print(f"{Fore.CYAN}Verdict:{Style.RESET_ALL} {verdict_str}") print(f"{Fore.CYAN}Confidence:{Style.RESET_ALL} {f.confidence}") if f.product: print(f"{Fore.CYAN}Product:{Style.RESET_ALL} {f.product}") if f.version: print(f"{Fore.CYAN}Version:{Style.RESET_ALL} {f.version}") if f.vulnerable_version: patch = get_patch_version(f.product, f.version) if patch: print(f"{Fore.YELLOW}Patch Version:{Style.RESET_ALL} {patch} or higher") if f.forticloud_sso_enabled is not None: sso_str = "ENABLED" if f.forticloud_sso_enabled else "DISABLED" color = Fore.RED if f.forticloud_sso_enabled else Fore.GREEN print(f"{Fore.CYAN}FortiCloud SSO:{Style.RESET_ALL} {color}{sso_str}{Style.RESET_ALL}") if f.indicators: print(f"\n{Fore.CYAN}Indicators:{Style.RESET_ALL}") for ind in f.indicators: print(f" • {ind}") if f.notes: print(f"\n{Fore.CYAN}Notes:{Style.RESET_ALL}") for note in f.notes: print(f" • {note}") print("=" * 70) # ==================== MAIN ==================== def main(): banner() print_banner() ap = argparse.ArgumentParser( description="CVE-2025-59718 Fortinet Authentication Bypass Checker", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # SSH mode (recommended - requires credentials) %(prog)s --mode ssh --target 192.168.1.1 --user admin --password mypass %(prog)s --mode ssh --targets targets.txt --user admin --keyfile ~/.ssh/id_rsa # Passive mode (no credentials - less reliable) %(prog)s --mode passive --target https://firewall.example.com %(prog)s --mode passive --targets targets.txt # Passive mode with ACTIVE vulnerability testing (recommended) %(prog)s --mode passive --target 192.168.1.1 --active-test %(prog)s --mode passive --targets ips.txt --active-test # Output to JSON %(prog)s --mode ssh --target 192.168.1.1 --user admin --json output.json """ ) ap.add_argument("--mode", choices=["ssh", "passive"], default="passive", help="Verification mode: ssh (reliable, needs creds) or passive (recon)") ap.add_argument("--target", help="Single target (IP, hostname, or URL)") ap.add_argument("--targets", help="File with targets (one per line)") ap.add_argument("--timeout", type=int, default=10, help="Connection timeout in seconds") # SSH arguments ap.add_argument("--user", help="SSH username (required for ssh mode)") ap.add_argument("--password", default="-", help="SSH password (use '-' to skip)") ap.add_argument("--keyfile", default="-", help="SSH private key file (use '-' to skip)") # Port configuration ap.add_argument("--default-port", type=int, default=None, help="Default port if not specified (ssh=22, passive=443)") # Output options ap.add_argument("--json", help="Save results to JSON file") ap.add_argument("--quiet", action="store_true", help="Suppress pretty output") ap.add_argument("--active-test", action="store_true", help="Enable active vulnerability testing (passive mode only)") args = ap.parse_args() # Validation if not args.target and not args.targets: ap.error("Specify --target or --targets") if args.mode == "ssh" and not args.user: ap.error("SSH mode requires --user") # Set default port default_port = args.default_port or (22 if args.mode == "ssh" else 443) # Parse credentials password = None if args.password == "-" else args.password keyfile = None if args.keyfile == "-" else args.keyfile # Parse targets targets: List[Tuple[str, int, str]] = [] if args.target: host, port, raw = normalize_target_to_host_port(args.target, default_port) targets.append((host, port, raw)) if args.targets: targets.extend(read_targets_file(args.targets, default_port)) if not targets: ap.error("No valid targets found") # Warn about active testing if args.active_test and args.mode != "passive": print(c_warn("--active-test solo se aplica en modo passive, será ignorado")) if args.active_test and args.mode == "passive": print(c_warn("⚠️ ACTIVE TESTING habilitado - se realizarán pruebas activas")) print(c_warn("⚠️ Esto enviará requests adicionales al target")) print() print(c_info(f"Escaneando {len(targets)} target(s) en modo {args.mode}...\n")) # Scan targets findings: List[Finding] = [] for idx, (host, port, raw) in enumerate(targets, 1): print(c_info(f"[{idx}/{len(targets)}] Escaneando {raw}...")) try: if args.mode == "ssh": finding = check_ssh(host, port, raw, args.user, password, keyfile, args.timeout) else: # Pass active_test flag to check_passive active_testing = args.active_test if hasattr(args, 'active_test') else False finding = check_passive(host, port, raw, args.timeout, active_test=active_testing) findings.append(finding) # Print result immediately if not quiet if not args.quiet: print_finding(finding) except KeyboardInterrupt: print(c_warn("\nEscaneo interrumpido por usuario")) break except Exception as e: print(c_err(f"Error inesperado: {e}")) # Create error finding err_finding = Finding( target=raw, mode=args.mode, host=host, port=port, verdict="UNKNOWN", confidence="low", notes=[f"Error inesperado: {type(e).__name__}: {e}"] ) findings.append(err_finding) # Summary print("\n" + "=" * 70) print(f"{Fore.CYAN + Style.BRIGHT}RESUMEN{Style.RESET_ALL}") print("=" * 70) total = len(findings) exists = sum(1 for f in findings if f.verdict == "EXISTS") potential = sum(1 for f in findings if f.verdict == "POTENTIAL") not_found = sum(1 for f in findings if f.verdict == "NOT_FOUND") unknown = sum(1 for f in findings if f.verdict == "UNKNOWN") print(f"Total targets: {total}") print(c_crit(f"VULNERABLE (EXISTS): {exists}")) print(c_warn(f"POTENTIAL: {potential}")) print(c_ok(f"NOT VULNERABLE: {not_found}")) print(c_info(f"UNKNOWN: {unknown}")) # Save to JSON if requested if args.json: output_data = { "scan_info": { "mode": args.mode, "timestamp": datetime.now().isoformat(), "total_targets": total, "active_testing": getattr(args, 'active_test', False), "summary": { "exists": exists, "potential": potential, "not_found": not_found, "unknown": unknown } }, "findings": [asdict(f) for f in findings] } with open(args.json, 'w', encoding='utf-8') as jf: json.dump(output_data, jf, indent=2, ensure_ascii=False) print(c_ok(f"Resultados guardados en: {args.json}")) # Exit code if exists > 0: print(c_crit("\n⚠️ ¡SE ENCONTRARON SISTEMAS VULNERABLES!")) print(c_crit("⚠️ ACCIÓN INMEDIATA REQUERIDA")) exit(1) elif potential > 0: print(c_warn("\n⚠️ Se encontraron sistemas potencialmente vulnerables")) print(c_warn("⚠️ Verificar manualmente con modo SSH")) exit(2) else: print(c_ok("\n✓ No se encontraron sistemas vulnerables")) exit(0) if __name__ == "__main__": main()