Earlier this year, soon after reproducing a remote code execution vulnerability for the Fortinet FortiNAC, I was on the hunt for a set of new research targets. Fortinet seemed like a decent place to start given the variety of lesser-known security appliances I had noticed while searching for the FortiNAC firmware. The first target I landed on was the Fortinet Wireless LAN Manager (WLM). The security audit of this appliance began what became the successful, but failed journey of what I dubbed the “Forti Forty” – a goal to find 40 CVE’s in Fortinet appliances. The journey ended in 16 mostly critical and high security issues identified across the FortiWLM, FortiSIEM, and another appliance before it was cut short when Fortinet’s download portal no longer provided access to download their appliances.
This blog details several of the issues discovered in the FortiWLM that have since been patched:
Additionally two vulnerabilities that have not received patches leading to appliance compromise:
Fortinet’s Wireless LAN Manager allows for ease of management of wireless devices throughout an enterprise. It remotely monitors the devices health, performance, and any RF interference all in a single pane of glass. It’s commonly deployed in large campus settings like universities, hospitals, and other large office footprints – making it a valuable target of interest in today’s threat landscape.
Figure 1. FortiWLM Dashboard
The FortiWLM web services are built atop the Apache2 and Django frameworks. The Apache configuration has defined several rewrite rules from the frontend on tcp/443 and passes them back to the Django framework middleware which listens on localhost tcp/8000.
The core of the Django middleware logic lies within the /opt/meru/etc/ws/wlmweb directory. Inspecting the file /opt/meru/etc/ws/wlmweb/wlmweb/wlmrbac.py, within the process_view() function, we see that several endpoints depending on the request path are validated against the current requests session authentication information.
On line 86, the two endpoints are explicitly allowed for any request without any authentication checks:
Figure 2. wlmrbac.py authentication checks
CVE-2024-34993: Fortinet FortiWLM Unauthenticated Command Injection Vulnerability
This vulnerability allows remote, unauthenticated attackers to inject a crafted malicious string in a request to the /ems/cgi-bin/ezrf_upgrade_images.cgi endpoint that will be executed in the context of root. The issue results from the lack of proper validation of results and calls to the potentially dangerous function system().
The first endpoint explicitly allowed without authentication, /ems/cgi-bin/ezrf_upgrade_images.cgi, can be found at /opt/meru/etc/ws/cgi-bin/ezrf_upgrade_images.cgi. All CGI endpoints are Perl scripts which, depending on the endpoint, parse different request parameters which are sometimes validated, but sometimes not.
This ezrf_upgrade_image.cgi endpoint parses the op_type request parameter, and depending on the value, passes control to different defined functions within the script.
Figure 3. ezrf_upgrade_images.cgi operation types
The deleteprogressfile() function extracts the progressfile request parameter and then directly passes it to a call to system() without any input validation.
Figure 4. ezrf_upgrade_images.cgi’s vulnerable deleteprogressfile()
Sending a request to this unauthenticated endpoint with op_type=deleteprogressfile and a malicious string in the progressfile parameter results in remote code execution in the context of the root user.
Figure 5. Unauthenticated RCE as root via ezrf_ugprade_image.cgi endpoint
CVE-2024-???? (0-day): Fortinet FortiWLM Unauthenticated Limited File Read Vulnerability
This vulnerability allows remote, unauthenticated attackers to access and abuse builtin functionality meant to read specific log files on the system via a crafted request to the /ems/cgi-bin/ezrf_lighttpd.cgi endpoint. This issue results from the lack of input validation on request parameters allowing an attacker to traverse directories and read any log file on the system.
The second endpoint explicitly allowed without authentication, /ems/cgi-bin/ezrf_lighttpd.cgi, can be found at /opt/meru/etc/ws/cgi-bin/ezrf_lighttpd.cgi. Again, this CGI endpoint is a Perl script in which different request parameters are not always validated.
When the op_type is upgradelogs, control is passed to the upgradelogs() function. This function will read the specific log file from the system and return its content in the response.
Figure 6. ezrf_lighttpd.cgi upgradelogs() function
Inspecting how the $filename variable is constructed we see that it is partially controlled by the imagename request parameter.
Figure 7. ezrf_lighttpd.cgi attacker controlled input unvalidated
Abusing the lack of input validation, an attacker can construct a request where the imagename parameter contains a path traversal, allowing the attacker to read any log file on the system.
Luckily for an attacker, the FortiWLM has very verbose logs – and logs the session ID of all authenticated users. Abusing the above arbitrary log file read, an attacker can now obtain the session ID of a user and login and also abuse authenticated endpoints.
Figure 8. Leak Session ID with Unauthenticated Arbitrary Log File Read
CVE-2023-48782: Fortinet FortiWLM Authenticated Command Injection Vulnerability
This vulnerability allows remote, authenticated attackers to inject a crafted malicious string in a request to the /ems/cgi-bin/ezrf_switches.cgi endpoint that will be executed in the context of root. The issue results from the lack of proper validation of results and insecure use of the dangerous system calls. This endpoint is accessible for both low privilege users and admins.
The ezrf_switches.cgi endpoint supports several op_type subfunctions related to adding a LAN switch for monitoring. This CGI script can be found at /opt/meru/etc/ws/cgi-bin/ezrf_switches.cgi.
The updateStatus() helper function, called in two different op_type functions, is vulnerable to command injection of both the $switche_table_k_Hostname and $switche_table_k_CommunityString variables which are derived from user input.
Figure 9. ezrf_switches.cgi vulnerable updateStatus()
Both addSwitche() and editSwitche() functions call the vulnerable updateStatus() helper function without validating any input.
Figure 10. ezrf_switches.cgi addSwitche() calls updateStatus()
Tracing those variables to where they are declared, they are derived straight from the request parameters Hostname and CommunityString, and are never validated along the way.
Figure 11. ezrf_switches.cgi request parameters unvalidated
Combining both the unauthenticated arbitrary log file read and this authenticated command injection, an unauthenticated attacker can obtain remote code execution in the context of root.
Figure 12. Abusing Unauth Log Read and Auth Command Injection to obtain root RCE
The initial report to Fortinet contained a total of 9 specific security issues, largely concentrated on the unauthenticated endpoints discovered in the Django middleware. The authenticated attack surface is large, and at the time contained numerous issues that were similar in nature to the ones detailed here stemming from a lack of input validation. Two additional security issues of note reported and patched, but unused in the attack paths were:
CVE-2023-42783: Fortinet FortiWLM Unauthenticated Arbitrary File Read Vulnerability
This vulnerability allows remote, unauthenticated attackers to access and abuse builtin functionality meant to read specific log files on the system via a crafted request to the /ems/cgi-bin/ezrf_upgrade_images.cgi endpoint in the uploadstatus function with the progressfile parameter. This issue results from the lack of input validation on request parameters allowing an attacker to traverse directories and read any file on the system.
CVE-2023-34991: Fortinet FortiWLM Unauthenticated SQL Injection Vulnerability
This vulnerability allows remote, unauthenticated attackers to access and abuse builtin functionality meant to list images on the system via a crafted request to the /ems/cgi-bin/ezrf_upgrade_images.cgi endpoint in the editimage function with the imageName and description parameters. This issue results from the lack of input validation on request parameters allowing an attacker to modify a SQL query string.
CVE-2024-???? (0-day): Fortinet FortiWLM Static Session ID Vulnerability
The web session ID token of authenticated users remains static, and unchanged, for users between sessions. Each time a user logs in, they receive the exact same session ID token. This token remains static for each boot of the device. An attacker that can obtain this token can abuse this behavior to hijack sessions and perform administrative actions. This session ID is retrievable with the unpatch limited log file read vulnerability above and can be user to gain administrative permissions to the appliance.
While we found it to be popular with State, Local, and Education (SLED) and healthcare focused customers, luckily the internet exposure is fairly limited to around 15 instances.
Figure 13. Shodan Exposure
The FortiWLM logs the majority of its application activities in the /data/apps/nms/logs directory. Specifically activity related to the exploitation these issues can be observed in the /data/apps/nms/logs/httpd_error_log.log file. Example entries of the log file included below show the exploitation of the unauthenticated remote code execution vulnerability, CVE-2023-34993. If defenders suspect that an appliance has been compromised, the logged request parameters should be analyzed to determine if they appear malicious.
Figure 14. httpd_error_log.log example entry
12 May 2023 – Submitted report to Fortinet PSIRT
15 May 2023 – PSIRT acknowledges receipt
10 July 2023 – PSIRT reproduces issues and indicates fix is in-progress
10 August 2023 – Ask for update, PSIRT responds fix is awaiting release
11 October 2023 – PSIRT releases fixes for 2 reported issues
14 November 2023 – PSIRT releases fixes for 2 more reported issues
21 November 2023 – Indicate to PSIRT of intent to publicly disclose all issues
22 November 2023 – PSIRT indicates remaining 3 vulnerabilities will be patched soon
7 December 2023 – PSIRT releases 1 fix for more reported issues
23 February 2024 – RingZer0 conference talk discussing some of these vulnerabilities
14 March 2024 – This public disclosure after 307 days with two unpatched vulnerabilities