In early 2023, given some early success in auditing Fortinet appliances, I continued the effort and landed upon the Fortinet FortiSIEM. Several issues were discovered during this audit that ultimately lead to unauthenticated remote code execution in the context of the root user. The vulnerabilities were assigned CVE-2023-34992 with a CVSS3.0 score of 10.0 given that the access allowed reading of secrets for integrated systems, allowing for pivoting into those systems.
The FortiSIEM allows customers to do many of the expected functions of a typical SIEM solution such as log collection, correlation, automated response, and remediation. It also allows for simple and complex deployments ranging from a standalone appliance to scaled out solutions for enterprises and MSPs.
In a FortiSIEM deployment, there are four types of roles that a system can have:
● Supervisor – for smaller deployments this is all that’s needed, and supervises other roles
● Worker – handles all the data coming from Collectors in larger environments
● Collector – used to scale data collection from various geographically separated network
environments, potentially behind firewalls
● Manager – can be used to monitor and manage multiple FortiSIEM instances
For the purposes of this research, I deployed an all-in-one architecture where the appliance contains all of the functionality within the Supervisor role. For more information about FortiSIEM key concepts refer to the documentation.
One of the first things we do when auditing an appliance is to inspect the listening services given you have some time of shell access. Starting with the most obvious service, the web service, we see that it listens of tcp/443 and the proxy configuration routes traffic to an internal service listening on tcp/8080.
Figure 2. httpd.conf proxying traffic
We find that the backend web service is deployed via Glassfish, a Java framework similar to Tomcat in that it provides a simple way to deploy Java applications as WAR files. We find the WAR file that backs the service, unpack it, and decompile it. Inspecting some of the unauthenticated attack surface, we happen upon the LicenseUploadServlet.class
.
Figure 4. LicenseUploadServlet doPost method
We follow the code into this.notify()
, where we eventually observe it calling sendCommand()
, which interestingly sends a custom binary message with our input to the port tcp/7900.
We find that tcp/7900 hosts the phMonitor service, which listens on all interfaces, not just localhost.
Figure 6. phMonitor on tcp/7900
And it is also a compiled C++ binary.
Now that we’ve identified a pretty interesting attack surface, let’s build a client to interact with it in the same way the web service does. The message format is a pretty simple combination of:
Constructing the LicenseUpload message in little-endian format and sending it over an SSL wrapped socket will succeed in communicating with the service. Re-implementing the client messaging protocol in Python looks like the following:
Figure 7. phMonitor Python client
As a test that the client works, we send a command type of 29, mapped to handleProvisionServer
, and can observe in the logs located at /opt/phoenix/log/phoenix.log
that the message was delivered.
Figure 8. phMonitor client successful message sent
The phMonitor service marshals incoming requests to their appropriate function handlers based on the type of command sent in the API request. Each handler processes the sent payload data in their own ways, some expecting formatted strings, some expecting XML.
Inside phMonitor, at the function phMonitorProcess::initEventHandler()
, every command handler is mapped to an integer, which is passed in the command message. Security Issue #1 is that all of these handlers are exposed and available for any remote client to invoke without any authentication. There are several dozen handlers exposed in initEventHandler()
, exposing much of the administrative functionality of the appliance ranging from getting and setting Collector passwords, getting and setting service passwords, initiating reverse SSH tunnels with remote collectors, and much more.
Figure 9. Sampling of handlers exposed
Given the vast amount of attack surface available unauthenticated within the phMonitor service, we begin with the easiest vulnerability classes. Tracing the calls between these handlers and calls to system()
we land of the handler handleStorageRequest()
, mapped to command type 81. On line 201, the handler expects the payload to be XML data and parses it.
Figure 10. handleStorageRequest() expecting XML payload
Later, we see that it attempts to extract the server_ip
and mount_point
values from the XML payload.