Blog post

Caught in the FortiNet: How Attackers Can Exploit FortiClient to Compromise Organizations (3/3)

Yaniv Nizry photo

Yaniv Nizry

Vulnerability Researcher

Date

  • Code Security

Welcome back to our Caught in the FortiNet series. In these blog posts, we're uncovering multiple vulnerabilities in FortiClient and the Endpoint Management System (EMS). When chained together, these vulnerabilities could lead to the compromise of an entire organization. In previous posts, we detailed how an attacker could gain initial access within an organization by exploiting FortiClient, then spreading to other endpoints on the network using a vulnerability in the EMS.

In this last article of the series, we will showcase a vulnerability enabling the attacker to go the last mile. Despite compromising all endpoints, an attacker would still be executing code under the same low-privileged user as FortiClient's UI, as the vulnerability leverages weaknesses in the Electron framework of the app. However, during our research on FortiClient, we discovered a local privilege escalation affecting macOS machines running FortiClient.

Impact

Though each vulnerability's impact differs, when chained together, they form a severe threat capable of granting an attacker complete organizational control with minimal user interaction.
The vulnerabilities are tracked as:

  • CVE-2025-25251: fixed in FortiClientMac 7.4.3 and 7.2.9. Fix is also being backported to 7.0.
  • CVE-2025-31365: fixed in FortiClientMac 7.4.4 and 7.2.9
  • CVE-2025-22855: fixed in FortiClient EMS 7.4.3
  • CVE-2025-22859: fixed in FortiClient EMS 7.4.3; only EMS 7.4 (Linux-based) is affected by this issue. 
  • CVE-2025-31366: fixed in FortiOS and FortiProxy versions 7.6.3 and 7.4.8

In this last part of the series, we will focus on CVE-2025-25251, which affects FortiClient on macOS. This vulnerability allows an attacker who already have execute code capabilities on the victim’s machine to escalate their privileges to root.

Technical Details

As we've covered in previous posts, FortiClient is built upon the Electron framework, which enables convenient cross-platform development and provides a web-based graphical user interface (GUI). This Electron GUI runs as a process under the permission of the logged-in user.

When an attacker exploits CVE-2025-22855 (which we discussed in Part 1), the arbitrary code they execute inherits the same permissions as the exploited process, which means it runs under the current user's privileges. However, FortiClient is powerful software that is capable of enabling VPN connections, running system scans, installing certificates, and more. All of these operations require elevated (root) permissions. So, how does FortiClient achieve this when this process is only running with user privileges?

The Electron UI, while being the visible interface of the application, is only the tip of the iceberg. Beneath it, multiple processes and services run in the background, each with different responsibilities and permissions. This design adheres to the principle of least privilege, separating permission levels and granting only the necessary permissions for each function. The elevated processes, often referred to as "helper tools" and commonly registered as LaunchDaemons, facilitate specific actions that require root access. Since the UI itself doesn't require root, it can run with the current user's permissions.

But when separating components, developers must ensure they still work together seamlessly. This is achieved using XPC (macOS Interprocess Communication):

Apple provides developers with the option to create XPC services, which expose specific functionalities. A client process can initiate an XPC request to a service registered on the machine, thereby triggering particular application logic. Crucially, any process on the machine can act as a "client", initiating a request to any available service currently running. This means it is the sole responsibility of the listener service to authorize the client.

One common method developers use to authenticate and authorize the client process is by verifying its code signature. This is a default requirement on macOS for any executable to run. Within this signature, there's a value called the Team Identifier, which serves as a unique ID for the developer of the software. By using this, an application can ascertain which team developed a given executable and confirm that its code has not been tampered with.

However, when we examined FortiClient's privileged executables and their corresponding XPC verification mechanisms, we discovered a shared vulnerable practice that enables attackers to bypass this crucial security check.

PID reuse (CVE-2025-25251)

The main handler of XPC requests starts at the shouldAcceptNewConnection function. Here, Fortinet first retrieves the Process Identifier (PID) of the client's process and then passes it to the isValidPid function:

bool ServiceDelegate::listener:shouldAcceptNewConnection:
               (ID param_1,SEL param_2,ID param_3,ID param_4)
{
  //...
  auVar5 = _objc_msgSend$processIdentifier();
  bVar1 = {% mark yellow %}_objc_msgSend$isValidPid{% mark %}:(param_1,auVar5._8_8_,auVar5._0_8_);
  //...

Within isValidPid, the _proc_pidpath function is used to retrieve the executable path associated with the client's PID.

bool ServiceDelegate::isValidPid:(ID param_1,SEL param_2,int param_3)
{
 //...
 {% mark yellow %}_proc_pidpath{% mark %}((int)uVar3,local_439,0x401);
 Var1 = _verifySignature(local_439);
 //...

This path is then sent to the _verifySignature function, which extracts the executable's code signature and compares its Team ID against a hardcoded Fortinet Team ID.

ulong _verifySignature(ulong param_1)
{
  //...
  if ((param_1 != 0) && (param_1 = _CFStringCreateWithCString(0,param_1,0x8000100), param_1 != 0)) {
    local_38 = 0;
    lVar2 = _CFURLCreateWithFileSystemPath(0,param_1,0,0);
    if (lVar2 == 0) {
      _CFRelease(param_1);
      param_1 = 0;
    }
    else {
      iVar1 = _SecStaticCodeCreateWithPath(lVar2,0,&local_38);
      uVar4 = 0;
      if (iVar1 == 0) {
        local_40 = 0;
        iVar1 = _SecCodeCopySigningInformation(local_38,2,&local_40);
        uVar4 = 0;
        if ((iVar1 == 0) && (local_40 != 0)) {
          team_id = _CFDictionaryGetValue
                            (local_40,*(undefined8 *)PTR__kSecCodeInfoTeamIdentifier_10004c288);
          if ((team_id == 0) || (lVar3 = _CFStringCompare(team_id,&cf_AH4XFXJ7DK,0), lVar3 != 0)) {

While at a glance, comparing the client’s executable signature to Fortinet’s team ID appears to be robust, the way they have implemented it is susceptible to a race condition. An attacker can initiate an XPC request from their malicious client process. Immediately after sending the request, they can use posix_spawn to switch the executable associated with their client's PID to a legitimate Fortinet executable.

If this switch occurs before the listener service fetches the process path from the PID, then the executable that undergoes the signature check will be the legitimate Fortinet executable. Attackers can increase the reliability of this race condition by forking multiple processes and sending numerous XPC messages. This tactic enqueues the messages, slowing down the listener's verification process and extending the time window for the attacker to successfully perform the executable swap.

From vulnerability to impact

This vulnerability allows an attacker, who has already achieved code execution on a victim's machine, to execute arbitrary XPC requests on FortiClient's privileged services. By itself, this doesn't immediately imply any impact, as the attacker's capabilities are limited to the functionality exposed by the XPC services. To execute code with the XPC service's permissions (root), attackers must identify what functions they can invoke and determine if these functions can be leveraged for further exploitation.

In our search for such functions, we discovered the runTool function within the fctservctl2 service. This function offers multiple purposes, determined by the ID provided. Specifically, an interesting code block caught our attention under ID 11:

pFVar3 = _fopen(pcVar2,"r");
//...
pFVar5 = _fopen(local_520,"w");
//... some kind of magic ...
_fwrite(abStack_105a0,(long)iVar1,1,pFVar5);
//...
_fchmod(iVar1,uStack_e8._4_2_);
//...
_fchown(iVar1,local_f0._4_4_,(gid_t)uStack_e8);
_unlink(pcVar2);
  1. This code first reads a file from a path provided in the XPC request.
  2. Creates a new file.
  3. Performs some manipulation on the content of the original file.
  4. Writes the modified content to the new file. 
  5. Then updates the file permissions and owner. 
  6. Finally, it deletes the original file that was read.

While this sequence of operations might seem unusual at first glance, it makes perfect sense when we understand the function's purpose. FortiClient includes a feature that scans files for malware on the machine. If a malicious file is detected, FortiClient quarantines it by moving it to a restricted folder (/Library/Application Support/Fortinet/FortiClient/data/quarantine_sandbox/). It also modifies the file's content, permissions, and owner to prevent it from being accessed and executed. A common practice among antivirus software.

This specific runTool:11 XPC request is designed to unquarantine a file. It restores all metadata and content of a quarantined file and moves it to a destination defined in the XPC request. If an attacker can create a fake quarantined file and then exploit the PID reuse vulnerability to initiate the unquarantine process, they would effectively achieve an arbitrary file write with root privileges.

However, there's a small hurdle: legitimate quarantined files are stored within a folder that requires elevated permissions to access. We noticed that when sending a file name in the XPC message, attackers can traverse back and point to any file on the system.

int arg1 = 11;
NSDictionary *arg2 = @{
@"FileName":@"../../../../../../../../../../Users/user/Desktop/fake_quarantined.txt",
@"sandbox":@0,
@“DestDir”:@"/"
};
[xpcConnection.remoteObjectProxy runTool:arg1 arguments:arg2 withReply:^(int arg3){}];

From this root-level arbitrary file write, there are numerous options to achieve code execution. But first, we have to reverse engineer the quarantine file format:

  • 0xc0 (192) bytes, which consists of:
    • HEADER_BYTES: 0x3209
    • 40 bytes PADDING1
    • FILENAME length (max 0x400)
    • UNKNOWN length (max 0x80, we are not sure what this is used for)
    • OWNER (8 bytes, used for chown)
    • PERMISSION (8 bytes, used for chmod)
    • PADDING2 (to fit the 0xc0 size)
  • FILENAME
  • UNKNOWN
  • 0xab XOR-ed file content

Using this, attackers can create a simple script that generates a fake quarantined file. Then, one of the simplest methods an attacker could use is to overwrite a daily periodic script, located at /private/etc/periodic/daily/999.local, which is executed daily as root. In the following screenshot, we can see how the file has been changed

On a different terminal, attackers will set up a reverse shell listener and will wait for the daily script to run. After its execution, they will be granted root privileges:

Patch

The vulnerabilities we discovered are fixed in the following versions:

  • CVE-2025-25251: fixed in FortiClientMac 7.4.3 and 7.2.9. Fix is also being backported to 7.0.
  • CVE-2025-31365: fixed in FortiClientMac 7.4.4 and 7.2.9
  • CVE-2025-22855: fixed in FortiClient EMS 7.4.3
  • CVE-2025-22859: fixed in FortiClient EMS 7.4.3; only EMS 7.4 (Linux-based) is affected by this issue. 
  • CVE-2025-31366: fixed in FortiOS and FortiProxy versions 7.6.3 and 7.4.8

We urge customers to update their affected Fortinet products to the fixed versions.

Timeline

DateAction
2024-11-20We report all issues to Fortinet
2024-11-29Fortinet acknowledges the receipt of the report
2024-12-18Fortinet confirms the issues are being worked on
2025-01-28CVE-2025-22855 and CVE-2025-22859 are assigned
2025-03-05CVE-2025-25251 is assigned
2025-03-28CVE-2025-31366 and CVE-2025-31365 are assigned
2025-04-08CVE-2025-22855 is published
2025-04-08Fortinet shares the CVSS scoring with us
2025-04-08We request further clarification about the scoring
2025-04-10Fortinet shares further CVSS details with us
2025-04-11We provide our feedback regarding the CVSS scoring
2025-05-13CVE-2025-22859 and CVE-2025-25251 are published

Summary

This concludes our 3-part blog series covering a chain of vulnerabilities in FortiClient and Endpoint Management System (EMS) that could compromise an entire organization. Following discussions on initial access and lateral movement, this post details a local privilege escalation affecting macOS machines.

This research highlights the inherent "double-edged sword" nature of endpoint protection software. While designed as a primary defense against cyber threats, these powerful tools themselves can harbor vulnerabilities. When the very software intended to secure an organization becomes a gateway for attackers, it exposes a critical attack surface. Our findings demonstrate how adversaries could leverage flaws within Fortinet's product to bypass security mechanisms, escalate privileges, and ultimately gain control over an entire organization, underscoring the vital need for continuous security scrutiny of even trusted security solutions.

Finally, we would like to thank Fortinet PSIRT again for their collaboration and responsiveness in addressing these findings.

Related Blog Posts

Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles. 

I do not wish to receive promotional emails about upcoming SonarQube updates, new releases, news and events.

By submitting this form, you agree to the storing and processing of your personal data as described in the Privacy Policy and Cookie Policy. You can withdraw your consent by unsubscribing at any time.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

  • Follow SonarSource on Twitter
  • Follow SonarSource on Linkedin
language switcher
Español (Spanish)
  • Documentación jurídica
  • Centro de confianza

© 2008-2024 SonarSource SA. Todos los derechos reservados. SONAR, SONARSOURCE, SONARQUBE, y CLEAN AS YOU CODE son marcas comerciales de SonarSource SA.