It’s not unusual to find firewall devices during a security assessment, which can make life harder for penetration testers. Modern firewall devices (Next Generation Firewalls aka NGFWs) are a far cry from simple traffic control systems.
A NGFW is an integrated network platform that combines a traditional firewall with other network device filtering functionalities, such as an application firewall using in-line deep packet inspection (DPI), antintrusion prevention system (IPS) and/or techniques such as SSL and SSH interception, website filtering, QoS/bandwidth management, antivirus inspection and third-party integration (i.e. Active Directory).
From an attacker's perspective, this increase in functionality also represents an extension of the attack surface exposed by these systems.
In this post I'm going to analyze a pre-authenticated remote code execution vulnerability which I found in one of the integration mechanisms used by FortiGate devices to authenticate to a Windows Domain Controller.
Core Security released an advisory for this vulnerability in coordination with Fortinet PSIRT Team.
Introduction to SSO
The FortiGate unit can authenticate users transparently and allow them network access based on their privileges in Windows AD. This means users who have logged on to the network are not asked to enter their credentials again before they can access network resources through the FortiGate unit.
- FortiGate is a network security platform that includes firewall, IPS, Antivirus/AntiMalware/AntiSpam, DLP, Vulnerability Management, Layer 2/3 routing, and VPN, among others.
- FortiOS is the Operating System for all FortiGate devices.
- Single Sign-On (SSO) allows logged in users to access resources through the FortiGate unit without being asked to enter their credentials again.
- Fortinet Single Sign-On (FSSO) provides single sign-on capabilities to Windows AD, Citrix, or Novell eDirectory users with the help of agent software installed on these networks. In a Windows AD network, FSSO can also provide NTLAN Manager (NTLM) authentication services to the FortiGate unit.
One way to provide SSO capabilities to Windows AD and Novell eDirectory users is to install an Agent Software on the network.
FSSO is a complex protocol that has many types of agents.
The “Collector Agent” is a type of agent installed as a service in the Windows AD network. This agent is able to poll information from domain controllers and share it with FortiGate units.
The official documentation recommends installing the Collector Agent with a Domain Administrator Account to ensure the installation goes smoothly and to avoid permissions issues.
Once installed, the Collector Service presents a configuration UI like the following:
If we connect to the TCP port 8000 we get the following hello message:
This service manages the communication between the FortiGate unit and the Collector Agent.
FSSO Service Protocol
In order to understand the protocol and messages format, the main agent binary “collectoragent.exe” was analyzed with IDA Pro. The goal was to find out how the service handles new incoming packets.
The first thing it does upon receiving a new packet is determine its total size by reading the first DWORD of data. In any case, the packet cannot be longer than 0x100000 bytes.
After this check, the program starts filling a buffer with the socket data and calls the message_dispatcher function passing the socket, the buffer packet and the packet_size as arguments.
The message_dispatcher in turn performs the following actions:
- Gets the packet_type by reading the first byte from the second DWORD of the packet
- Substract 0x80 to the packet_type
- Use the value as an index into a function table pointer
Among the several callable functions, the first one named “call_process_hello” is the more interesting as is the responsible for authenticating the client and setting some initial parameters. To use the other functions, the client must be authenticated.
Before proceeding with the disassembly, we're going to introduce the message format and a reversed structure that is used throughout the code:
A packet is composed by a packet size, a protocol message, and a series of chunks (subpackets) that are specific to each message.
Each chunk is defined as follows:
The protocol message dictates which processing function is going to be called (as seen above).
Inside each processing function, a special structure is initialized to keep track of the current chunk being processed. The structure was named _working_record and it’s defined as follows:
unsigned int packet_size;
This function is setting the first three members of the working_record. It’s important to highlight that ptr_current_chunk starts with the same value of buff_pckt.
After the initialization, each chunk is processed and its data is saved in some variable defined by the processing function.
The code responsible for doing chunk processing does not perform enough checks on the data it receives. The chunk sizes can be set to arbitrary values and then are copied to local variables without trimming the data. Moreover, depending on the chunk_data_type field, the chunk data will be treated differently. This condition can be abused to cause a stack overflow and gain control of the execution flow.
The PROCESS_HELLO defines several local variables that are going to be filled during chunk processing including client version, serial number, and password. Next, it passes these variables as parameters along with the packet and packet size to “process_hello_chunks” functions.
The process_hello_chunks is actually the one that initializes the working_record and processes each chunk of the hello packet.
get_next_chunk function starts by checking if there are more chunks in the data to be processed. It does this by comparing the ptr_current_chunk against the sum of the packet_base_addr and the packet_size:
There will be always at least one chunk to process the first time; the function continues by reading the chunk message, chunk size, and chunk data type:
The chunk size is converted to little endian and then is stored in the record. In addition, the 5th byte determines how the chunk data is going to be treated:
#define DATA_TYPE_DWORD_BIG_ENDIAN_1 0
#define DATA_TYPE_DWORD_LITTLE_ENDIAN 1
#define DATA_TYPE_DWORD_LITTLE_ENDIAN 2
#define DATA_TYPE_UNKNOWN 3
#define DATA_TYPE_DWORD_BIG_ENDIAN_2 4
#define DATA_TYPE_PTR 5
These functions set the working_record->chunk_data, update the ptr_current_chunk by adding it the value stored on working_record->chunk_size_le, and finally return the message_type.
Now that the working_record is complete, the processing function reads it and uses the message type to determine which setting must be set. This is done in a loop fashion for every chunk of the packet.
Despite the fact that the ptr_current_chunk can be set to arbitrary memory with these functions, if we take a look at how the working_record is then used by the processing function we will be able to see that no checks on the maximum size are performed before filling the local variables.
For instance, to set the password local buffer it uses an insecure memcpy that can be fully controlled by the current working_record values.
To sum up, a hello packet with a special chunk setting the password can lead to a pre-authenticated remote stack overflow allowing full control of the execution flow and the ability of running arbitrary code in the context of the user configured, which the documentation suggest to be a Domain Administrator.
The Novell eDirectory Agent shares this same message dispatcher, so triggering an overflow condition isn’t very different from what was described here.
Users of Core Impact 2014 R2 have access to a full working exploit supporting three versions of this software including the last one before the patch (4.3.0161).
- Install the latest version of FSSO.
- Even though the official documentation states that it is recommended to run FSSO with a Domain Account, there is a section dedicated to those who prefer to take the safe way and create a restricted user with the minimum required privileges. Refer to the documentation for more details.
- Configure a Windows firewall rule to allow traffic only from FortiGate(s) IPs.
Adding more features increases the attack surface on any system. In the case of NGFWs, there are many services and protocols that are valuable targets for attackers.