#!/usr/bin/env python3 """ fortinet_reuse_check.py ────────────────────────────────────────────────────────────────────────────── Professional scanner for CVE-2024-50562 – Fortinet SSL-VPN session reuse vulnerability (FG-IR-24-339). The tool: 1. Logs in with supplied credentials 2. Saves session cookies 3. Logs out 4. Re-uses saved cookies to verify whether the session is invalidated Developed by: Bugb Security Team Company: Bugb Technologies Pvt. Ltd. Website: https://bugb.io It now supports full CLI input: - `--username/-u` VPN username (REQUIRED) - `--password/-p` VPN password (REQUIRED) - `--realm/-r` Fortinet realm (optional) - `--target/-t` Host[:port] pair (repeatable, default port 443) - `--file/-f` File of Host[:port] pairs (one per line, # = comment) - `--output/-o` CSV path for results (default: fortinet_reuse_results.csv) Examples ──────── # Single target on default port 443 python3 fortinet-cve-2024-50562.py -u alice -p hunter2 -t 192.0.12.8 # Several explicit targets python3 fortinet-cve-2024-50562.py -u bob -p S3cre7 \ -t 192.0.2.11:4433 -t 192.0.2.8:15333 # Bulk scan from file plus one extra target python3 fortinet-cve-2024-50562.py -u bob -p 'S3cre7' \ -f targets.txt -t 192.0.2.8 """ import argparse import csv import json import re import sys from pathlib import Path from typing import Dict, List, Tuple import requests import urllib3 # ──────────────────────── STATIC SETTINGS ──────────────────────────────────── TIMEOUT: int = 10 PORTAL_PATH: str = "/sslvpn/portal.html" # endpoint requiring auth DEBUG_BODY: bool = False # dump bodies on unexpected 200s urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # ───────────────────────── HELPERS ─────────────────────────────────────────── def pretty_json(data: Dict[str, str]) -> str: return json.dumps(data, separators=(",", ":")) def verdict_from_body(body: str) -> str: """Return 'INVALIDATED' if we are pushed back to login; else 'REUSED'.""" if re.search(r"/remote/login|name=[\"']username[\"']", body, re.I): return "INVALIDATED" return "REUSED" def print_header() -> None: print("=" * 80) print("CVE-2024-50562 Scanner - Fortinet SSL-VPN Session Management Vulnerability") print("=" * 80) print("Testing for insufficient session expiration in FortiOS SSL-VPN portals") print("Reference: FG-IR-24-339 | CVSS: 4.4 (Medium)") print("=" * 80) def print_target_header(host: str, port: int, current: int, total: int) -> None: print(f"\n[{current}/{total}] TARGET: {host}:{port}") print("-" * 50) def log_step(step: str, status: str, details: str = "") -> None: symbols = { "SUCCESS": "[+]", "FAILED": "[-]", "WARNING": "[!]", "INFO": "[*]", "VULNERABLE": "[VULN]", "SECURE": "[SAFE]" } symbol = symbols.get(status, "[?]") msg = f"{symbol} {step}" if details: msg += f" - {details}" print(f" {msg}") def analyze_cookies(c_before: dict, c_after: dict) -> str: if not c_before: return "No session cookies received" analysis: List[str] = [] for ck in ("SVPNCOOKIE", "SVPNTMPCOOKIE"): if ck in c_before and ck not in c_after: analysis.append(f"{ck} properly invalidated") elif ck in c_before: analysis.append(f"{ck} persists after logout") return " | ".join(analysis) if analysis else "No session cookies found" def load_targets(singles: List[str], file_path: str | None) -> List[Tuple[str, int]]: """Return list of (host, port) tuples from CLI.""" targets: List[Tuple[str, int]] = [] # single -t entries for item in singles: host, *port = item.split(":") targets.append((host.strip(), int(port[0]) if port else 443)) # file entries if file_path: for line in Path(file_path).read_text().splitlines(): line = line.split("#", 1)[0].strip() # allow comments if not line: continue host, *port = line.split(":") targets.append((host.strip(), int(port[0]) if port else 443)) if not targets: raise SystemExit("[!] No targets supplied – use --target or --file") return targets # ───────────────────────── CORE TEST ───────────────────────────────────────── def test_target(host: str, port: int, username: str, password: str, realm: str, current: int, total: int) -> Tuple: base = f"https://{host}:{port}" print_target_header(host, port, current, total) sess = requests.Session() sess.verify = False try: # 1. Portal connect log_step("Connecting to SSL-VPN portal", "INFO") sess.get(f"{base}/remote/login", params={"lang": "en"}, timeout=TIMEOUT) log_step("Portal connection", "SUCCESS") # 2. Auth log_step("Authenticating", "INFO", f"user={username}") r_login = sess.post( f"{base}/remote/logincheck", data={"ajax": "1", "username": username, "realm": realm, "credential": password}, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=TIMEOUT ) body_login = r_login.text cookies_login = requests.utils.dict_from_cookiejar(r_login.cookies) ret_login = re.search(r"\bret=(\d+)", body_login) success = ret_login and ret_login.group(1) == "1" and "/remote/hostcheck_install" in body_login if not success: log_step("Authentication", "FAILED", "Invalid credentials or MFA") return host, port, False, "auth-failed", cookies_login, {}, {}, "UNTESTABLE" log_step("Authentication", "SUCCESS") # Cookie info if cookies_login: log_step("Session cookies received", "INFO", ", ".join(cookies_login.keys())) # 3. Logout log_step("Initiating logout", "INFO") r_logout = sess.get(f"{base}/remote/logout", timeout=TIMEOUT) cookies_logout = requests.utils.dict_from_cookiejar(r_logout.cookies) log_step("Logout completed", "SUCCESS") # Cookie invalidation log_step("Cookie invalidation", "INFO", analyze_cookies(cookies_login, cookies_logout)) # 4. Re-use cookies log_step("Testing cookie reuse", "INFO", "Creating new session with old cookies") reuse = requests.Session() reuse.verify = False reuse.cookies.update(cookies_login) r_reuse = reuse.get(f"{base}{PORTAL_PATH}", timeout=TIMEOUT) verdict = verdict_from_body(r_reuse.text) cookies_reuse = requests.utils.dict_from_cookiejar(reuse.cookies) # 5. Verdict if verdict == "REUSED": log_step("VULNERABILITY DETECTED", "VULNERABLE", "Session remains active after logout") log_step("CVE-2024-50562", "VULNERABLE", "System requires immediate patching") else: log_step("Session properly invalidated", "SECURE", "No vulnerability detected") log_step("CVE-2024-50562", "SECURE", "System appears patched or not vulnerable") return host, port, True, verdict, cookies_login, cookies_logout, cookies_reuse, verdict except requests.Timeout: log_step("Connection", "FAILED", "Timeout") return host, port, False, "timeout", {}, {}, {}, "ERROR" except requests.ConnectionError: log_step("Connection", "FAILED", "Connection refused") return host, port, False, "connection-error", {}, {}, {}, "ERROR" except Exception as e: log_step("Test execution", "FAILED", f"{e.__class__.__name__}: {str(e)[:50]}") return host, port, False, f"error:{e.__class__.__name__}", {}, {}, {}, "ERROR" # ─────────────────────────── CLI / MAIN ────────────────────────────────────── def main() -> None: parser = argparse.ArgumentParser( prog="fortinet_reuse_check.py", description="Scanner for CVE-2024-50562 – Fortinet SSL-VPN session " "management vulnerability." ) parser.add_argument("-u", "--username", required=True, help="VPN username") parser.add_argument("-p", "--password", required=True, help="VPN password") parser.add_argument("-r", "--realm", default="", help="Realm (optional)") parser.add_argument("-t", "--target", action="append", help="Target in HOST[:PORT] form (repeatable)") parser.add_argument("-f", "--file", help="File with HOST[:PORT] lines (blank & # comments ok)") parser.add_argument("-o", "--output", default="fortinet_reuse_results.csv", help="CSV output path (default: %(default)s)") args = parser.parse_args() targets: List[Tuple[str, int]] = load_targets(args.target or [], args.file) print_header() results: List[Tuple] = [] stats = {"vulnerable": 0, "secure": 0, "untestable": 0, "errors": 0} for idx, (host, port) in enumerate(targets, 1): try: res = test_target( host, port, username=args.username, password=args.password, realm=args.realm, current=idx, total=len(targets) ) results.append(res) # tally if res[3] == "REUSED": stats["vulnerable"] += 1 elif res[3] == "INVALIDATED": stats["secure"] += 1 elif res[3] == "auth-failed": stats["untestable"] += 1 else: stats["errors"] += 1 except KeyboardInterrupt: print("\n[!] Scan interrupted by user") break # Summary print("\n" + "=" * 80) print("SCAN SUMMARY") print("=" * 80) total = len(results) print(f"Targets scanned: {total}") print(f"Vulnerable to CVE-2024-50562: {stats['vulnerable']}") print(f"Secure/Patched: {stats['secure']}") print(f"Authentication failed: {stats['untestable']}") print(f"Connection/Other errors: {stats['errors']}") if stats["vulnerable"]: print(f"\n[CRITICAL] {stats['vulnerable']} system(s) vulnerable to session hijacking") print("[ACTION] Immediate patching required:") print(" - FortiOS 7.6.x: Upgrade to 7.6.1+") print(" - FortiOS 7.4.x: Upgrade to 7.4.8+") print(" - FortiOS 7.2.x: Upgrade to 7.2.11+") print(" - FortiOS 7.0.x/6.4.x: Migrate to supported version") print("\nVulnerable systems:") for r in results: if r[3] == "REUSED": print(f" - {r[0]}:{r[1]}") else: print("\n[GOOD] No vulnerable systems detected") if stats["secure"]: print(f"[INFO] {stats['secure']} system(s) properly invalidate sessions") # CSV Export try: with open(args.output, "w", newline="") as fh: wr = csv.writer(fh) wr.writerow(["ip", "port", "login_success", "vulnerability_status", "cookies_login", "cookies_logout", "cookies_reused", "summary"]) for row in results: wr.writerow([ row[0], row[1], row[2], row[3], pretty_json(row[4]), pretty_json(row[5]), pretty_json(row[6]), row[7] ]) print(f"\n[+] Detailed results exported to: {args.output}") except Exception as e: print(f"[!] Failed to export results: {e}") # ───────────────────────────────────────────────────────────────────────────── if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n[!] Scan terminated by user") sys.exit(1)