Fortinet Series 1 — Analysis of CVE-2022–40684

INTfinity Consulting
8 min readApr 4, 2023

--

Introduction

Given the recent surge in news coverage regarding exploits associated with Fortinet products, we think it will be a good learning experience for us to delve into the relevant vulnerabilities and exploits.

The first CVE to be analyzed in our Fortinet series is CVE-2022–40684.

As always, there are numerous informative resources available on this particular CVE (see reference section). Thus we strongly recommend reading the article from [1] and [2] before proceeding further. Since our objective is to gain a deeper understanding of the vulnerability of FortiOS (the main OS behind most Fortinet solution offerings), our article will be focusing on complementing the referenced articles with alternative methods to challenge our understanding.

Overview

CVE-2022–40684 is a critical authentication bypass vulnerability that receives a CVSSv3 score of 9.6. The vulnerability allows adversaries to circumvent authentication through an alternate pathway and execute commands with administrative privileges in the system. This can be achieved through the use of carefully crafted HTTP/HTTPS requests that deceive the system into believing that the request is originating from a local IP address and a trusted user-agent. As a result, the attacker sent HTTP/HTTPS request is processed without any form of authentication.

The firmware to be analyzed is FortiOS 7.2.1 of FortiGate-VM.

Analyzing the Vulnerability

The typical starting point is to perform firmware extraction to analyse the contents of the files. However, we will not be covering the firmware extraction as it is already very well documented in the reference article from [1].

From the examination of the contents of the firmware, it was discovered that the underlying file system was Linux with numerous files. Of particular interest to us was the init binary, which was responsible for invoking various services within the FortiOS, as evidenced by the symlinks to the init binary.

Directory listing in the FortiOS /bin directory

This could also be corroborated through reverse engineering. Starting from the main function, it became evident that if /bin/init was run from a symbolic link, the program flow would divert to sub_448D40 (which we renamed to fn_call_fortinet_other_binaries), where there is a function handlers table that mapped functions to their respective service, e.g. httpsd, was located. However, despite our best efforts with static analysis, we were unable to trace any functions that is related to the vulnerability from the /bin/httpd function. At this juncture, the referenced articles [1] and [2] provided much help.

init logic in determining whether the called binary is /bin/init
Function handler table

An alternative way of identifying the API handlers table

The team from Horizon3.ai [1] used HTTPSd debug messages while the team from hakaioffsec [2] used Burp and debug messages to identify the API handlers table and api_cmdb_v2-handler function. We asked ourselves if the same outcome could be achieved through static analysis and dynamic debugging (just for the fun of it?!?). With the (self-imposed) constraints of not using Burp and debug messages, we decided to look at the PSIRT advisory to help us narrow down the functions to analyze in the init binary. The PSIRT advisory mentioned “Local_Process_Access” as one of the indicators of compromise. Using this string, we were able to narrow down to 9 functions in IDA that referenced this string.

Snippet of the PSIRT advisory
Locating the functions with “Local Process Token”

By examining the cross references in the 9 functions, we are able to locate the API handlers table that is mentioned in the referenced article.

API handler table

There were many functions in the API handler table. We knew from [1] and [2] that api_cmdb_v2-handler was the function that we should focus on. Even without this prior knowledge, we could deduce from the advisory that attackers would be able to perform operations through the administrative interface. From the three links below, we were able to derive that api_cmdb_v2_handler as the function responsible for administrative operations.

- https://docs.fortinet.com/document/fortigate/7.2.4/administration-guide/940602/using-apis
- https://pypi.org/project/fortigate-api/0.1.1/
- https://community.fortinet.com/t5/Support-Forum/Fortigate-Rest-API-cmdb-vs-monitor/td-p/201235

To confirm that api_cmdb_v2-handler will be executed when any management API endpoint is called, we can make a POST request to an API handler from the management web GUI. By using one of the links used by the management web GUI, we can craft different API calls to confirm the APIs handler by setting breakpoints on the address of the corresponding functions which are 4 bytes after the pointer to respective APIs string.

In our example, we fired the following API and set a breakpoint at 0xc917b0 which is the cmdb_v2 handler.

https://<target>/api/v2/cmdb/firewall/address

Breakpoint at 0xc917b0 hit when https://172.16.204.53/api/v2/cmdb/firewall/address is triggered
By continuing the execution flow, the information of the firewall is displayed on the browser

Diving deeper into the api_cmdbv2_handler, as mentioned in Horizon3.ai article [1], the function handler will call sub_C53510 (which we renamed as fn_api_access_check). This function performs check for valid authentication e.g. session key, api key or trusted access. Depending on the return value of this function, different error codes will be generated as seen from the switch case statements. The aim is to get to the else condition in the default case where access check is passed and the program flow continues.

Other functions also referencing to the fn_api_access_check to check whether API call is authorised
Switch statement block to generate different HTTP response code

Deciphering the api_access_check

We know from analyzing the api_cmdb_v2-handler that the 1st parameter is the apache request_rec structure. To help us better understand the api_access_check function, there is a need to construct some structs to visualise the context in which members of the structs are being used. We construct a local struct for request_rec struct in IDA (shown below) and insert it at the appropriate position. Note that some of the struct members in request_rec struct are also of the type struct and there is a need to construct them accordingly based on https://nightlies.apache.org/httpd/trunk/doxygen/structrequest__rec.html

Snippet of request_rec struct type declared in IDA

Using the strings from the functions within api_access_check, we can name the functions according to the strings. With the struct created and strings renamed, we had a clearer idea of what the functions are doing. In this case, we have to dive deeper on the sub_C5A340 (renamed as api_acess_check_for_trusted_access) which is one of the few checks when no session key has been found (since the bug is a pre-auth bug).

Convert first parameter to request_rec struct type and renaming the functions

Getting to the exploited function

Through referencing the request_rec object, it is observed that the sub_C5A340 (renamed as api_access_check_for_trusted_access) function contained multiple calls to sub_C50B80 (renamed as is_trusted_ip_and_get_useragent) which look for whether the client IP matched “127.0.0.1” and if the user agent matched “node.js” or “report runner”.

Authentication bypass pre-requisites

In order to fulfil these 2 conditions to obtain authentication bypass, we first need to trace when is the request_rec object populated with the client_ip and how we could manipulate it to be 127.0.0.1. Through cross-referencing, we were able to find out that the function sub_C4BF30 (renamed as fn_api_handler) took in the API handler type (e.g. api_cmdb_v2-handler) as the 2nd parameter. This fn_api_handler function would then match the requested API call with strings in the handler table and call the corresponding function.

API handler used in fn_api handler function to call
API handler table used as the second parameter
Matching the handler list name in order to call the correct function handler

Through further cross-referencing and reverse engineering, we were able to locate sub_C4AA60 (renamed as fn_extract_ip_port_check_local_ip) which extracted the “for” and “by” field from the header field. The “for” field allowed us to set the IP to the request record Client_IP field.

To give the audience a clearer idea, the general program flow look roughly like this where the relevant information is extracted before passing it to fn_api_handler to find the corresponding function to process the request further.

sub_C4A720 → sub_C4AA60 (fn_extract_ip_port_check_local_ip) → sub_C4A710 → sub_C4BF30 (fn_api_handler)

Extracting the “for” field from the “Forwarded” header

Moving on to the user agent portion, sub_C5A340 (api_access_check_for_trusted_access) is another long and complex function. As mentioned in the referenced articles, the check path for “Report Runner” as the user agent is less complex as compared to “NodeJs”. This is evidenced from the code snippets below. In essence, if the IP address is “127.0.0.1” and the user agent is “Report Runner”, then the return value will be 1.

Logic when the user agent is “Report Runner”

By crafting the exploit code with the IP address “127.0.0.1” and user agent matching “Report Runner”, the return value will be 1 and matches the default case in the select case statement. As (access_status — 1) is less than 1, the program flow will move to the else statement and at this point, authentication bypass is achieved.

Program flow to achieve authentication bypass
Successfully reached the logic to achieve authentication bypass after exploit has been launched

Successful authentication bypass will eventually lead the program flow to sub_C99A20 (renamed as fn_http_api_json_response) that process the API POST request.

Program flow to fn_http_api_json_response

During the execution of the POC, although we encountered HTTP server error 500, the exploit was able to work. Therefore we are expecting expecting an HTTP 500 error code which sub_c91963 (handle_cmdb_api_req) returned and this was confirmed in the our debugger where rax value is 0x1F4 when the sub_c91963 (handle_cmdb_api_req) function returned.

JSON response after POC has been executed with http_status of 500
sub_c91963 (handle_cmdb_api_req) returned 500 (0x1f4) in RAX register

As we continue the execution flow, the program will eventually call the function that will return the JSON response. At this stage, we have succeeded in performing authentication bypass with a valid server response.

Calling sub_C99A20 (fn_http_api_json_response)
JSON response string in memory

POC and Patching

We will not be covering the POC and the patching, do refer to the referenced articles for the details.

Conclusion

This was definitely not an exercise for the faint hearted due to the size and complexity in analysing the init binary. Furthermore, without the help of those articles, we might not be able to shortcut our way to locate some of the important functions. Through this analysis exercise, we have learnt some valuable skills in analysing and debugging FortiOS. We will continue to look into other known Fortinet CVEs and share our learning experiences through our articles.

References

[1] https://www.horizon3.ai/fortios-fortiproxy-and-fortiswitchmanager-authentication-bypass-technical-deep-dive-cve-2022-40684/

[2] https://labs.hakaioffsec.com/fortigate-authentication-bypass/

--

--

Responses (2)