Next, Next, SYSTEM: Exploiting NSIS installer bugs to escalate privileges in Zscaler Client Connector

Introduction

In this post we walk through multiple vulnerabilities in both NSIS (Nullsoft Scriptable Install System) and Zscaler Client Connector for Windows, demonstrating how these can be combined to escalate privileges from a standard user to SYSTEM. We also explore how these issues extend well beyond Zscaler - given NSIS’s ubiquity, there are likely many other affected products - and share guidance for identifying them, including a YARA rule for detecting vulnerable NSIS versions.

All vulnerabilities discussed here have since been patched by both Zscaler and NSIS.

When installer vulnerabilities are raised, a common pushback is: “so what, you need to be admin to run the installer anyway.” This overlooks a critical class of bugs - where a privileged service, auto-updater, or self-service software portal triggers an installer on behalf of a low-privileged user. In this post, we demonstrate two case studies in Zscaler where NSIS installer flaws lead to privilege escalation from a standard user to SYSTEM, with no admin pre-requisite. In future posts, we will expand on this with additional techniques and case studies.

Background

NSIS Vulnerabilities

NSIS is one of the most widely used open-source installer frameworks for Windows, relied upon by thousands of software packages. In this post we cover two NSIS vulnerabilities:

  • CVE-2023-37378 - NSIS before 3.09 mishandles access control for an uninstaller directory.
  • CVE-2025-43715 - NSIS before 3.11 contains a race condition in the temporary plugin directory creation logic, caused by incomplete checking of the CreateRestrictedDirectory return value.

The first vulnerability (CVE-2023-37378) was discovered by myself in 2023 whilst researching Zscaler Client Connector. A writeup of the NSIS bug can be found here. I actually found the Zscaler trigger first, before tracing the root cause upstream to NSIS and reporting it to both parties. Despite reporting this to Zscaler, the Network Adapter installer was never upgraded to a patched version of NSIS. As such, the weak ACLs on the temporary directory remained exploitable until Zscaler stopped shipping the Network Adapter entirely in version 4.8 (30th October 2025).

The second vulnerability (CVE-2025-43715) was discovered and responsibly disclosed by Sandro Poppi. It is a race condition in the installer’s plugin directory creation logic. As there does not appear to be a public writeup of this vulnerability beyond the NSIS changelog entry, we are sharing technical details here alongside our Zscaler case studies. We demonstrate this vulnerability with two different triggers in Zscaler, including the same TAP driver trigger.

CVE-2023-37378 - Insecure Temp Folder Usage

The full writeup for this vulnerability can be found in the original advisory. We cover the key points here as they are relevant to the Zscaler case studies later.

In summary, during uninstallation, the NSIS uninstaller needs to copy itself to a temporary directory before it can delete the original binary. When running as SYSTEM, this temporary directory (~nsuA.tmp) is created in C:\Windows\Temp - a location which is writable by all users. NSIS attempts to protect this directory by calling CreateRestrictedDirectory when it detects an admin context, but three problems combine to make this protection ineffective.

The Bugs

When the uninstaller detects it is running as admin, it calls CreateRestrictedDirectory to set up the ~nsuA.tmp temporary directory with a restrictive DACL. However, in Source/exehead/Main.c, the return value is completely ignored. The uninstaller carries on to SetCurrentDirectory, copies Un_A.exe into the folder, and launches it - regardless of whether the directory was secured or already contained attacker-controlled files:

admin ? CreateRestrictedDirectory(state_temp_dir) : CreateNormalDirectory(state_temp_dir);
SetCurrentDirectory(state_temp_dir);

The g_restrictedacl array in Source/exehead/util.c includes an ACE granting Everyone the DELETE and FILE_DELETE_CHILD rights on the directory. Any local user can remove it:

C:\Windows\System32>icacls C:\Windows\Temp\~nsuA.tmp
C:\Windows\Temp\~nsuA.tmp BUILTIN\Administrators:(OI)(CI)(F)
                          Everyone:(OI)(CI)(D,Rc,RD,DC)

Successfully processed 1 files; Failed processing 0 files

If the directory already exists, CreateRestrictedDirectory returns ERROR_ALREADY_EXISTS and just re-applies the DACL without touching the contents. Old Un_X.exe files are cleaned up, but anything else the attacker has placed there survives.

Exploitation via DotLocal Redirection

NSIS calls SetDefaultDllDirectories to harden against conventional DLL search-order hijacking, but this does not cover DotLocal redirection. Windows has a feature where, if a .local folder exists next to an executable, WinSxS assembly lookups are redirected to resolve from that folder first.

An attacker can exploit this by deleting ~nsuA.tmp (which the weak DACL permits), and recreate it under their control, building an Un_A.exe.local subdirectory with the expected SxS assembly path containing a malicious COMCTL32.dll:

~nsuA.tmp\Un_A.exe.local\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.19041.1110_none_60b5254171f9507e\COMCTL32.dll

When the uninstaller copies and launches Un_A.exe, Windows sees the .local folder and loads the attacker’s COMCTL32.dll instead of the system copy. Since Un_A.exe inherits the SYSTEM privileges of its parent, this gives the attacker code execution with SYSTEM privileges. This technique was confirmed working on Windows 10 22H2 at the time of reporting. The weak directory permissions were subsequently fixed in NSIS 3.09.

Procmon - COMCTL32.dll loaded from the attacker’s .local directory (Windows 10 22H2)

Procmon - COMCTL32.dll loaded from the attacker’s .local directory (Windows 10 22H2)

CVE-2025-43715 - NSIS 3.10 Race Condition

A separate but related issue, discovered by Sandro Poppi, existed in the installer’s plugin directory creation path (exec.c), where CreateRestrictedDirectory is also called, but the ERROR_ALREADY_EXISTS return value is not treated as a failure. This leads to a race condition. The NSIS changelog describes the issue as follows:

Retry creating restricted $PLUGINSDIR (thanks Sandro Poppi for responsibly disclosing, bug #1315)
This stops a possible privilege escalation. A malicious actor can create the temporary plugins
directory ($PLUGINSDIR) at just the right time and fill it with malicious files that the installer
might use. Installers running as SYSTEM use C:\windows\temp which is accessible by all users on the
system. Any user can use this bug to gain SYSTEM privileges by winning the race condition with an
installer that starts as SYSTEM.
Root Cause

An NSIS installer is built around the exehead - a stub executable that interprets compiled NSIS bytecode. When an installer script uses a plugin (such as nsExec.dll), the bytecode instructs the exehead to extract the plugin DLL to a temporary directory ($PLUGINSDIR) and then load it via LoadLibraryEx. When running under an admin context, the exehead attempts to restrict access to this temporary directory via CreateRestrictedDirectory.

The bytecode for creating the plugin directory is generated in Source/build.cpp:

  // GetTempFileName $0
  ret=add_entry_direct(EW_GETTEMPFILENAME, var_zero, add_asciistring(_T("$TEMP")));
  if (ret != PS_OK) return ret;
  // Delete $0 [simple, nothing that could clash with special temp permissions]
  ret=add_entry_direct(EW_DELETEFILE, zero_offset, DEL_SIMPLE);
  if (ret != PS_OK) return ret;
  // CreateDirectory $0 - a dir instead of that temp file
  ret=add_entry_direct(EW_CREATEDIR, zero_offset, 0, 1);
  if (ret != PS_OK) return ret;

This translates to:

  1. Create a temporary FILE
  2. Delete the temporary FILE
  3. Recreate it as a DIRECTORY

The reason for this convoluted approach is that GetTempFileName both generates a unique name and creates the file on disk. The developers repurpose this API to obtain a unique directory name, but must first delete the file it creates before they can replace it with a directory. This creates a race window in between deleting and re-creating the directory.

The EW_CREATEDIR handler in Source/exehead/exec.c processes the directory creation:

    case EW_CREATEDIR: {
      TCHAR *buf1=GetStringFromParm(-0x10);
      log_printf3(_T("CreateDirectory: \"%s\" (%d)"),buf1,parm1);
      {
        TCHAR *p = skip_root(buf1), c = _T('c');
        if (p)
        {
          while (c)
          {
            DWORD ec;
            p = findchar(p, _T('\\'));
            c = *p, *p = 0;
            if (!c && parm2 && UserIsAdminGrpMember()) // Lock down the final directory?
              ec = CreateRestrictedDirectory(buf1);
            else
              ec = CreateNormalDirectory(buf1);
            if (ec)
            {
              if (ec != ERROR_ALREADY_EXISTS)
              {
                log_printf3(_T("CreateDirectory: can't create \"%s\" (err=%d)"),buf1,ec);
                exec_error++;
              }
              else if ((GetFileAttributes(buf1) & FILE_ATTRIBUTE_DIRECTORY) == 0)
              {
                log_printf2(_T("CreateDirectory: can't create \"%s\" - a file already exists"),buf1);
                exec_error++;
              }
            }
            else
            {
              log_printf2(_T("CreateDirectory: \"%s\" created"),buf1);
            }
            *p++ = c;
          }
        }
      }

The problem here is in how exec_error is handled. It is only incremented when CreateRestrictedDirectory fails and the path does not already exist as a directory. If the directory is already present, the result is ERROR_ALREADY_EXISTS with FILE_ATTRIBUTE_DIRECTORY set - and the installer silently proceeds without having applied the restrictive ACL.

An attacker who can slip a CreateDirectory call into the gap between the file deletion and the installer’s own CreateDirectory call wins the race. The attacker’s directory survives the check unmodified, and the installer goes on to extract and load plugin DLLs from it - giving the attacker a DLL hijacking opportunity.

Exploitation

To exploit this, we can monitor the C:\Windows\Temp directory, wait for the .tmp file to be deleted, and then immediately create a directory with the same name before the installer does. If we win the race, the installer sees ERROR_ALREADY_EXISTS with FILE_ATTRIBUTE_DIRECTORY and carries on - using our directory as the plugin folder. We can then populate it with malicious plugin DLLs (e.g. nsExec.dll) that execute our payload when loaded.

Many installer frameworks - not just NSIS - rely on randomly generated temporary file names as a form of security-through-obscurity, on the assumption that an attacker cannot predict or discover them. However, this assumption is flawed. As James Forshaw demonstrated, an attacker can call NtNotifyChangeDirectoryFile to receive notifications when files are created or deleted in a directory - even without enumerate permissions:

Handy trick from James Forshaw

Handy trick from James Forshaw

Case Studies - Zscaler Client Connector for Windows

The NSIS vulnerabilities described above are not specific to any single product; any software that ships a vulnerable NSIS installer and runs it with elevated privileges could be affected. In the following case studies, we demonstrate exploitation through Zscaler Client Connector’s IPC mechanism. First, we need to cover how the Zscaler IPC architecture works, as it is central to both attacks.

Zscaler IPC Architecture

Zscaler Client Connector for Windows is composed of several co-operating processes that communicate over RPC via ALPC. Note that prior to version 3.1.0, ZSATrayManager did not exist and ZSATray communicated directly with ZSAService. The architecture described here applies to version 3.1.0 and later:

  • ZSATray.exe - The system tray GUI, running as the logged-on user. It is spawned by the privileged ZSATrayManager process in the current user’s session via CreateProcessAsUserW.
  • ZSATrayManager.exe - Runs as NT AUTHORITY\SYSTEM. Receives commands from the tray process and carries out privileged operations, including launching installers and uninstallers. It exposes an ALPC port named ZSATray_talk_to_me, whose security descriptor grants the Everyone group Connect access.
  • ZSAService.exe - Runs as SYSTEM. Handles service management and configuration.
  • ZSATunnel.exe - Runs as SYSTEM. Operates the ZPA/ZPN tunnel and handles packet captures.

The following screenshot shows the Zscaler process hierarchy, with ZSAService and ZSATrayManager running as SYSTEM and ZSATray running as the logged-on user:

Zscaler process hierarchy and ALPC port

Zscaler process hierarchy and ALPC port

When a user performs a troubleshooting action through the tray UI - such as clicking “Repair App” or “Start Packet Capture” - the ZSATray process ultimately calls into ZSATrayHelper.dll to dispatch an ALPC message to ZSATrayManager. Depending on the command, ZSATrayManager either handles the operation directly or relays it to a backend service (ZSAService or ZSATunnel).

Zscaler Client Connector - Troubleshoot menu with Repair App and Start Packet Capture

Zscaler Client Connector - Troubleshoot menu with Repair App and Start Packet Capture

To illustrate the flow: clicking “Repair App” in the GUI eventually results in a call to the sendZSATrayManagerCommand function exported by ZSATrayHelper.dll, with the PERFORM_APP_REPAIR (0x3b) command code:

Decompiled ZSATray repairApplication function

Decompiled ZSATray repairApplication function

The native side is implemented in ZSATrayHelper.dll, which exports sendZSATrayManagerCommand - a function that constructs and sends the RPC message to the ZSATrayManager endpoint.

Bypassing the RPC Security Callback

To protect against unauthorised callers, ZSATrayManager registers a security callback via RpcServerRegisterIf2:

Decompiled ListenerInterface::start - RpcServerRegisterIf2 with IfCallbackFn

Decompiled ListenerInterface::start - RpcServerRegisterIf2 with IfCallbackFn

When an RPC call arrives, this callback retrieves the caller’s PID, opens the process, queries the image path with QueryFullProcessImageNameW, and verifies the Authenticode signature. If verification fails, the connection is rejected.

Decompiled isZscalerProcess - OpenProcess and QueryFullProcessImageNameW

Decompiled isZscalerProcess - OpenProcess and QueryFullProcessImageNameW

Decompiled isZscalerProcess - verifyFileSignature check

Decompiled isZscalerProcess - verifyFileSignature check

The weakness in this scheme is that the check only validates the calling process, not the calling code. By injecting into the low-privileged ZSATray.exe process, an attacker’s code inherits the trust of the host process - the RPC server sees a legitimately signed Zscaler binary making the call and allows it through.

The simplest way to exploit this is to take advantage of the fact that ZSATrayHelper.dll exports the sendZSATrayManagerCommand function, which accepts a pointer to a command structure containing the command code and an optional JSON configuration string. Since ZSATray.exe already has this DLL loaded, the attacker can resolve the function’s address via LoadLibraryEx/GetProcAddress, allocate memory in the ZSATray process for the command structure using VirtualAllocEx, write the desired command code with WriteProcessMemory, and then call CreateRemoteThread with the function address and the allocated structure as the parameter. This avoids the need to compile a full RPC client from IDL. A single CreateRemoteThread call is enough to trigger any supported RPC command.

Depending on the command, ZSATrayManager either handles the operation directly (e.g. launching the Npcap installer) or relays it to ZSAService (e.g. for application repair).

This architecture - where a low-privileged tray process communicates with a privileged backend service via RPC, with the caller validated by process signature - is common across ZTNA products including Zscaler and Netskope. The diagram below (from our DEFCON 2025 talk) illustrates the general pattern and how process injection bypasses the signature check:

Case Study 1: Zscaler Network Adapter LPE

The Zscaler-Network-Adapter (TAP driver) is provisioned when ZTunnel 1.0 is configured with route-based forwarding. If the driver needs to be reinstalled - for instance during an application repair - Zscaler invokes an NSIS-based uninstaller (and subsequently calls installer to re-install it).

Triggering the Uninstaller

Using the IPC bypass described above, an attacker can inject code into the ZSATray.exe process that sends the PERFORM_APP_REPAIR (0x3b) command through the ALPC channel to ZSATrayManager, which then relays it to ZSAService.

On receipt, ZSAService.exe begins the repair sequence, which includes reinstalling the Zscaler-Network-Adapter driver. It launches the NSIS-based uninstaller at C:\Program Files\Zscaler-Network-Adapter\Uninstall.exe with the /S (silent) flag, running under the SYSTEM account.

Procmon - ZSAService launching the Zscaler-Network-Adapter uninstaller as SYSTEM

Procmon - ZSAService launching the Zscaler-Network-Adapter uninstaller as SYSTEM

Exploitation

Since the Network Adapter installer was never upgraded to a patched version of NSIS, the CVE-2023-37378 weak ACLs remained present across all Zscaler versions that shipped the Network Adapter - right up until it was removed in version 4.8. This gives us multiple exploitation routes.

Technique 1: DotLocal Redirection (CVE-2023-37378)

The DotLocal/WinSxS redirection technique described in the CVE-2023-37378 section above works against the Zscaler Network Adapter trigger. We confirmed this on versions up to 4.7.0.141:

CVE-2023-37378 exploitation against Zscaler 4.7.0.141 - SYSTEM shell via DotLocal redirection

CVE-2023-37378 exploitation against Zscaler 4.7.0.141 - SYSTEM shell via DotLocal redirection

However, this technique relies on DotLocal behaviour which has been hardened in later builds of Windows 11.

Technique 2: Junction Swap (CVE-2023-37378)

An alternative approach that does not depend on DotLocal redirection is to use NTFS junctions to control where the uninstaller writes and reads Un_A.exe. As noted in the CVE-2023-37378 section, the uninstaller uses CopyFile to write Un_A.exe and then CreateProcess to launch it. If we can redirect the path between the write and the launch, we can swap in a malicious Un_A.exe.

We use junctions here because deleting a junction just removes the reparse point without needing to touch the target directory or any files inside it. This means we can swap the redirection even if handles are still open on files in the target - something that would prevent deletion of a regular directory.

The trick requires two levels of junction. We cannot swap ~nsuA.tmp itself after the uninstaller runs, because SetFileSecurity applies a restrictive DACL to it that removes write permissions - and deleting a junction requires write access. So instead, we chain two junctions and swap the second one, which remains under our control.

Since the ~nsuA.tmp directory does not exist until the uninstaller runs, and because the return value of CreateRestrictedDirectory is discarded, the attacker can pre-create it as a junction before triggering the repair:

  1. Pre-create C:\Windows\Temp\~nsuA.tmp as a junction pointing to workspace1
  2. Create workspace1 as a second junction pointing to workspace2 (a writeable directory under the attacker’s %TEMP%)
  3. Prepare workspace3 with a malicious Un_A.exe payload
  4. Trigger the repair - the uninstaller follows the chain (~nsuA.tmp -> workspace1 -> workspace2) and writes the real Un_A.exe to workspace2
  5. Monitor for Un_A.exe appearing in workspace2 (indicating the write has completed)
  6. Delete the workspace1 junction and recreate it pointing to workspace3
  7. When the uninstaller calls CreateProcess on Un_A.exe, it follows the updated chain (~nsuA.tmp -> workspace1 -> workspace3) and launches the attacker’s payload as SYSTEM
Junction swap technique - redirecting Un_A.exe between write and launch

Junction swap technique - redirecting Un_A.exe between write and launch

This works on any version of Windows because it only relies on the weak DACL and standard NTFS junction behaviour.

Successful exploitation of Zscaler 4.7.0.232 via junctions

Successful exploitation of Zscaler 4.7.0.232 via junctions

Technique 3: Plugin Directory Race (CVE-2025-43715)

The Network Adapter installer is also vulnerable to CVE-2025-43715, which provides a third exploitation route that does not depend on the weak ACLs at all. We’ll go into further detail about this later, however in summary, the attacker monitors C:\Windows\Temp for the deletion of the .tmp file and races to create a directory with the same name before the installer does. If successful, the installer finds the directory already exists, skips the restrictive ACL, and goes on to extract and load plugin DLLs (such as nsExec.dll) from the attacker-controlled folder.

Demo

Case Study 2: Zscaler Packet Capture LPE

Zscaler Client Connector’s packet capture feature relies on a bundled Npcap installer - itself NSIS-based. When a packet capture is started and Npcap is either missing or outdated, ZSATrayManager launches this installer with SYSTEM privileges.

Prior Art: CVE-2020-11635

This attack scenario closely mirrors another vulnerability, CVE-2020-11635, which I reported in Zscaler versions prior to 3.1.0 in 2020 (see archived Zscaler advisory). In the original finding, the Npcap installer (Npcap 0.9985, packaged with NSIS v2.51) called SetOutPath to change its working directory to C:\Windows\Temp part way through the install. It then executed several system binaries without specifying the full path, including net.exe, certutil.exe, pnputil.exe, and netsh.exe - which Windows resolved via the current working directory. Since C:\Windows\Temp is writable by all users, a low-privileged attacker could plant a malicious executable with one of those names and have it run under SYSTEM privileges.

Procmon - certutil.exe launched with C:\Windows\Temp as the working directory

Procmon - certutil.exe launched with C:\Windows\Temp as the working directory

Zscaler addressed this by upgrading to a newer Npcap version that no longer searched the %TEMP% directory for these binaries. However, the underlying installer remained NSIS-based, and was subsequently affected by the CVE-2025-43715 race condition bug.

Triggering the Npcap Install

Using the same IPC bypass technique described in the Background section, the attacker injects code into ZSATray.exe that sends an INSTALL_NPCAP command to ZSATrayManager, which launches the Zscaler-signed Npcap installer as SYSTEM.

Procmon - ZSATrayManager launching the bundled Npcap installer as SYSTEM

Procmon - ZSATrayManager launching the bundled Npcap installer as SYSTEM

Winning the Race

Once the installer starts, it follows the previously described NSIS plugin directory creation pattern, first calling GetTempFileName to obtain a unique name in C:\Windows\Temp (e.g. nsXXXX.tmp), deleting the file, and then recreating it as a directory. To exploit this, the attacker simply needs to monitor the temp directory using NtNotifyChangeDirectoryFile, watching for file deletion events targeting files ending with .tmp.

When the deletion is detected, the exploit immediately creates a directory with the same name - beating the installer to the CreateDirectory call. Because the directory already exists when the installer tries to create it, NSIS sees ERROR_ALREADY_EXISTS with FILE_ATTRIBUTE_DIRECTORY and proceeds without applying the restrictive ACL. The attacker now controls the plugin directory.

Procmon - race condition during Npcap installer temp directory creation

Procmon - race condition during Npcap installer temp directory creation

Achieving Code Execution

With control of the plugin directory, the attacker can place arbitrary files inside it. There are two routes to code execution here:

  • First, the NSIS installer extracts plugin DLLs (such as nsExec.dll) into $PLUGINSDIR and loads them via LoadLibraryEx. If the attacker places a malicious plugin DLL in this directory, then it will be loaded instead.
  • Second, the Npcap NSIS script uses SetOutPath to change the working directory to $PLUGINSDIR before executing system binaries via nsExec, without specifying their full path. This is the same class of bug as CVE-2020-11635, but targeting a different writable directory. In our proof-of-concept, we place a malicious powershell.exe in the hijacked $PLUGINSDIR. When the installer script invokes powershell.exe without a full path, Windows resolves it from the current working directory first - executing our payload as SYSTEM.

Demo

Proof of Concept

Proof-of-concept code for both case studies is available on GitHub:

Zscaler’s Response

  • We reported the vulnerable NSIS installers to Zscaler in July 2025.
  • The Npcap installer vulnerability was fixed in version 4.7.0.168 (22nd December 2025), by upgrading the bundled Npcap installer to version 1.84, which ships with NSIS 3.11.
  • Zscaler addressed the TAP driver vulnerability in version 4.8 (30th October 2025), by switching to the packet-based filter driver for tunnel mode regardless of the Tunnel Driver Type configured in the forwarding profile:
Zscaler 4.8 release notes

Zscaler 4.8 release notes

  • At the time of writing, we are not aware of any security advisory from Zscaler acknowledging that these versions resolve exploitable security vulnerabilities. We encourage users to update to the latest versions and to adopt the modern packet filter driver.

Detection and Mitigation

Since these NSIS vulnerabilities affect far more than just Zscaler, the most useful detection approach is to identify which software in your environment ships with a vulnerable NSIS version, and baseline DLL loads/child process execution. The YARA rule below can help with this.

For the Zscaler-specific attack chain, the most practical detection opportunity is monitoring for process injection into ZSATray.exe, since this is required to bypass the RPC security callback.

Mitigations

The root cause of these temp directory issues is that GetTempPath returns C:\Windows\Temp for SYSTEM processes - a directory writable by all users. Starting with Windows 11 Build 22000, Microsoft introduced GetTempPath2, which returns C:\Windows\SystemTemp when called from a SYSTEM process. This directory is only accessible to SYSTEM and administrators. However, at the time of writing, this does not appear to be used by NSIS (potentially due to legacy support requirements).

Vendors launching installers from privileged services should consider setting the TEMP/TMP environment variables to a secure location (such as C:\Windows\SystemTemp) before spawning the installer process.

For end users, the preferred approach is to upgrade Zscaler Client Connector to a patched version (4.7.0.168+ for the Npcap issue, 4.8+ for the TAP driver issue). As a workaround on unpatched systems, administrators could override the TEMP and TMP environment variables for the affected service by adding them under HKLM\SYSTEM\CurrentControlSet\Services\<ServiceName>\Environment, pointing them to C:\Windows\SystemTemp instead of C:\Windows\Temp. Since GetTempPath checks TMP first then TEMP, this would cause the NSIS installer to use the restricted directory instead of the user-writable one.

Detecting Vulnerable NSIS Versions

The following YARA rule can be used to identify installers compiled with a vulnerable version of NSIS:

rule NSIS_vulnerable_version {
    meta:
        description = "Detects NSIS versions vulnerable to CVE-2025-43715 or CVE-2023-37378"
        author = "Rich Warren"
    strings:
        $exehead = "Nullsoft.NSIS.exehead"
        $version_mar_2022 = "<description>Nullsoft Install System v27-Jan-2022.cvs"
        $version_mar_2023 = "<description>Nullsoft Install System v30-Mar-2023.cvs"
        $version_3_10 = "<description>Nullsoft Install System v3.10"
        $version_3_0 = "<description>Nullsoft Install System v3.0"
        $version_2 = "<description>Nullsoft Install System v2."
    condition:
        $exehead and any of ($version_*)
}

A Note on Supply Chain Disclosure

The vulnerabilities in this post raise a broader question: when a security product ships a vulnerable third-party component, whose responsibility is it to tell customers?

NSIS is open source, widely used, and the CVEs are public. But the fact that a vulnerability exists in NSIS does not tell an organisation whether the copy of NSIS bundled inside their ZTNA product is actually exploitable. That requires understanding the specific context - whether the installer runs as SYSTEM, whether it can be triggered by an unprivileged user, and whether the temp directory lands somewhere writable. That analysis is not something end users should be expected to do themselves. It is the vendor’s job.

In both of the Zscaler case studies covered here, the vulnerabilities were reported directly to Zscaler and subsequently fixed. However, at the time of writing, Zscaler has not published security advisories for any of these fixes. Customers have no way of knowing, from Zscaler’s own communications, that these versions address exploitable privilege escalation bugs.

This is not a problem unique to Zscaler. Many vendors silently patch security issues in bundled components without issuing advisories, leaving customers unable to prioritise updates or assess their exposure. Security products in particular should be held to a higher standard - as we discussed in our DEFCON 2025 talk “Zero Trust, Total Bust”, the premise of Zero Trust requires you to first trust your vendor. Silent patching undermines that trust.

Acknowledgements

Thanks to Sandro Poppi for discovering and responsibly disclosing CVE-2025-43715, Jamie O’Hare for reviewing drafts of this post, and Amir Szekely for his responsiveness in addressing these issues in NSIS.

You May Also Like