Microsoft is listening to feedback, I submitted an idea to adfeedback at microsoft.com back in May 2026 – that it would be nice to be able to view metadata in LDP.exe in a way similar to repadmin.exe /showobjmeta <DSA> <DN> where you get the attid’s translated to the real attribute ldapDisplayName instead, nope I don’t know all attids by heart – just a few 🙂
This was the only option before, and it still exists – for down-level combability and when you actually want the attid’s (Some links at the end of the post – if you want to get more familiar with attids)
By introducing a new option – View metadata (msDS-ReplAttributeMetaData) in the replication context menu
You can now get the ldapDisplayName of the attributes instead:
I hope you like this feature as much as I do, It’s available in Windows Insider Preview builds and Windows Server vNext Preview builds.
Isn’t the title of this blog post a bit strange? RID Masters and InfrastructureUpdate Objects? Shouldn’t it be Infrastructure Update Masters and InfrastructureUpdate Objects? – It could have been if this blog post was written in the year of 2000 – but it’s 2026 now and things change.
But let us go back to Windows 2000 and have a sort summary of what the IM does
A very detailed explanation (possibly the most detailed that exist) of what the IM or none-GC DCs (If Recycle-Bin is enabled) are responsible for is already available in if you start reading at “Let’s have a look at “Cross Naming Context (NC) references in NTDS.dit” in How the Active Directory–Data Store Really Works (Inside NTDS.dit)–Part 4 – Christoffer Andersson you will find those details.
Summary Windows 2000 – Windows Server 2008
The IM only have something to do in a multiple-domain forest, and only in domains that have at least one none-GC DC, if there is only one none-GC DC in a such forest – it has to hold the IM.
Summary Windows Server 2008 R2 – Windows Server vNext
Exactly the same as up until Windows Server 2008, after that still the same unless if the Recycle-Bin is enabled – If Recycle-Bin is enabled any none-GC DC will handle phantoms (Cross-DB/Domain reference integrity) – hence the IM as a FSMO has no work to do anymore and as each none-GC DC resolves the tasks them self, there is no need to create “carrier” objects in form of InfrastructureUpdates.
The RID Master
So in my session I mention that if the Recycle-Bin is enabled in the forest, the RID Master is the only FSMO that creates InfrastructureUpdate objects now days, but only certain conditions only – the most obvious role for the RID Master is to allocated and handle out RID pools to domain controllers within the domain – but let’s look at the documented description of the RID Master
So the the RID Master is responsible for cross-domain move (XDOMMOVE) aka interdomain moves – this means that a such move only can originate from a domains RID master, this is not a blog post about XDOMMOVE it self- trust me when I say that I’ve tried to write a blog article series about that very subject for several years, it’s one of the most complex working of Active Directory if you ask me – some day I might be able to finish it.
This is how an XDOMMOVE is performed in a multi-domain forest with both GCs and DCs (none-GCs)
1 – The user XDOM resides in D1
2- The user XDOM is moved from D1 (CN=XDOM,CN=Users,DC=D1) to D2 (CN=XDOM,CN=Users,DC=D2) – the move can only be initiated from the RID Master in D1 (D1-DC1)
2- RID Master creates an InfrastructureUpdate object with the attribute proxiedObjectName set to the objects new DN (CN=XDOM,CN=Users,DC=D2) and the binary portion to indicate it’s a moved object and 0 because it’s the first name the object is XDOMMOVE’ed
2 – The DC in the destination domain will create the object and in addition also set the attribute proxiedObjectName to DC=D1 to indicate the object was moved and last time it moved to instance the object it came from the domain D1
2+ – The infrastructureUpdate object replicates out to other DCs/GCs in D1, they will update the name of the object from CN=XDOM,CN=Users,DC=D1 to CN=XDOM,CN=Users,DC=D2 and phantomize the object (record in the database) to a phantom – the record in the database will keep it’s DNT
3 – The object CN=XDOM,CN=Users,DC=D2 now replicates out authoritatively in the forest belonging to D2, once this reaches GCs in D1, the phantomized object representing the moved object to D2 is now promoted back to an object and partial attributes are added – the row in the database will keep it’s DNT
This explain the usage of InfrastructureUpdate objects used by the RID FSMO Master for XDOMMOVE.
Moving the RID FSMO Master must also move the InfrastructureUpdate objects – but way?
So having the requirement of only being able to move objects off from the RID FSMO in the source domain during XDOMMOVE prevents several DCs to move the same object away to different destinations – cleaning that up would be very difficult once replicated out in the forest.
But if you move the RID FSMO from one DC to another DC (From D1-DC1 in Site to D1-DC2 in site2) as in the illustration move, this happens instant using the drsuapi RPC Interface and the GetNCChanges function when called with the extended operation to move the FSMO and the role is transferred instantly without any replication latency.
However we are now again in a dangerous state as the infrastructureUpdate objects that phantomize already XDOMMOVE’d objects takes the normal replication path and are there for subject to replication latency, the RID FSMO could potentially move, already moved objects again to other destinations (domains) – well thankfully this is engineered in such a way that if you move the RID FSMO – during the move all infrastructureUpdate objects having a ‘proxiedObjectName’ is moved within the same operation.
The condition of ‘proxiedObjectName’ is to not move InfrastructureUpdate objects eventually produced by the IM FSMO.
The 3rd undocumented producer of one InfrastructureUpdate object – the fix behind KB810089
So we have to go back in time to understand this one, or at least Pre-Windows 2000 Server SP4 or to be more specific KB810089
My understanding is that this was fixed up in several steps and places
Patched DC – first DC in new domain with a name that has previously existed in the enterprise (forest)
Patched ntdsutil.exe to fix up mangled nCName referances
Patched ntds.dll/ntdsai.dll – the core ds to prevent this from happen at all.
Fix (1.) is implemented in the install/boot code that is called by DCPROMO when you create a new domain within an existing forest, and here the ds devs made a smart move by creating and deleting an InfrastructureUpdate object in CN=LostAndFoundConfig,DC=X, named CN=CRUpdate,CN=LostAndFoundConfig,DC=X with the ‘dNReferenceUpdate’ attribute set to point to the newly created domain’s crossRef objects (via the IDL_DRSAddEntry API using the Directory Replication Service (DRS) Remote Protocol) value of nCName.
Since the InfrastructureUpdate is created/deleted in the same transaction and moved to the Deleted Objects container, it’s guaranteed to replicate in before any corssRef objects in the Partitions container and would reserve the name with a phantom that has name, guid and sid of the newly created domain at replicas in the enterprise (forest) or eventually improve an existing name to resolve a name conflict.
Here is a view of the DIT just pre-restart after promotion of the first DC in a new domain:
Here is the LDP.exe view post-restart after the promotion of the first DC in a new domain:
This is an illustration that might not be correct, but should give an idea:
It’s been very hard to get the correct picture of this, might some one who was part of designing KB810089 can reach out and inform us how it’s really working? How ever I’m sure this is the 3rd case that make use of InfrastructureUpdate objects at least.
This concludes this blog post and the is the final one in the series of blog posts covering the topics from my HiP 2026 Europe session “Inside Active Directory: Misconceptions, Myths and Bugs” – It’s been fun!
Note: When I did spend way to much time on this last 3:rd case I come across another bug in the DS, that isn’t accent – in fact it hasn’t been fixed yet, as I just reported it to Microsoft.
It’s been a while, sorry for the delay – reality and customer demands caught me with after the Hybrid Identity Protection Conference (HIP) Europe 2026 – but this is a follow up on DN references in the DIT and how they work, can logically corrupt the DIT and how a new RootDSE mod can fix those issues while the DS is online.
But let’s summary what it is and the different types, reference integrity is a concept on the Active Directory database layer, and how objects (represented) as rows reference each other by their primary key (DNT), on DCs that are none-GCs – references to objects in other domains are being made up by a phantom to satisfy reference integrity.
Simple references = None Linked attributes
Linked references = Easy to deal with from a DBLayer perspective (because they can be enumerated in the link_table)
Cross-NC references = Either simple or linked references between two objects in different instantiated NCs
Cross-DB references = Either simple or linked references between two objects in different NCs where one is un-instantiated.
Structural Phantoms are a different story (always GUID-less) and created to make up the full DN, in this case “Users” is a such – however if “Users” has been referenced directly earlier it might have a GUID, if “Users” is being referenced directly later (after being created as a structural phantom, it will continue to be GUID-less.)
The “CNT_col” contains the numbers of references that points to the row/object/phantom – it should always be 1 for live objects as the “obj-Dist-Name” attribute point’s to / references the object it self, it’s removed during logical deletion, the object is eventually demoted to a phantom if it’s up for garbage collection but there is still other rows/objects/phantoms in the DIT that points to it, physically removing that row/phantom would cause a logical corruption then DIT and break the reference integrity, once (or if ever) the “CNT_col” reaches “0” it would be physically removed (the row in the database table is removed)
As seen in the below example in “2.” the row representing “Elina Andersson” has been physically removed from the DIT, leaving the reference in “Lena Andersson’s” see-Also’s attribute dangling, this cause the object of “Lena Andersson” to become locally corrupted, trying to expand it in LDP.exe would generate an error -1601 (that is JetErrRecordNotFound) – aka DNT 5524 isn’t found in this case, but theoretically it can be any reference to another record in the DIT.
This post aims to demonstrate what happens when reference integrity fails and how to fix such issues, It has been significantly easier to do so with a new RootDSE mod called “CheckAndFixDNReference” that I did demo during my HIP talk, but let’s first have a look on how an object can become logically corrupted in the DIT due to reference integrity failing as well how that could be fixed before the introduction of the RootDSE mod “CheckAndFixDNReference”
Logically corrupt the DIT offline using ESE APIs How do you cause a corruption to happen? Well for my demo during the session I wrote a special tool to do that, I’m not going to share the exact code, but it looks something like this, to point the see-Also att to a bogus DNT (-2):
Code to o point see-Also to bogus DNT (-2)
// Special code to corrupt "See-Also" refEseHelper.JET_SETINFO jET_SETINFO =newEseHelper.JET_SETINFO();jET_SETINFO.itagSequence=1;jET_SETINFO.ibLongValue=0;jET_SETINFO.cbStruct=Marshal.SizeOf(jET_SETINFO);err =E.Check(EseHelper.JetBeginTransaction(sesid));err =E.Check(EseHelper.JetPrepareUpdate(sesid, tableid,2));int seealsoid =pAC.GetByDisplayName("seeAlso").ID;IntPtr pvBookmark =IntPtr.Zero;int pcbActual =0;// Write a bogus value that don't exist in the DBbyte[] pvDataBytes =BitConverter.GetBytes(-2);// Get the dbData of the tag converted to bytes. int cbData =pvDataBytes.Length;// Allocate our data handle.GCHandle pvDataHandle =GCHandle.Alloc(pvDataBytes,GCHandleType.Pinned);err =E.Check(EseHelper.JetSetColumn(sesid, tableid, seealsoid,pvDataHandle.AddrOfPinnedObject(), cbData,0,ref jET_SETINFO));err =E.Check(EseHelper.JetUpdate(sesid, tableid,out pvBookmark,1024*5,out pcbActual),true);err =E.Check(EseHelper.JetCommitTransaction(sesid,0));pvDataHandle.Free();
Logically corrupt the DIT online on debug/checked ntdsai.dll Okey, but is there any other way to do this without being able to write code on the ESE level and corrupt the DIT that way? Well yes sort of, but still nah – not really.
If you have a debug/checked version of ntdsai.dll – there is a lot of things you can do for troubleshooting and testing purpose, one of them happen to be to logically corrupt the DIT online. (Please not that this is NOT in anyway supported).
However there is another undocumented rootDSE mod that is only available in debug/checked versions of ntdsai.dll – named “genericControl” – this have several different categories of “fun” stuff, one of them being “CorruptDB” – that in it’s turn take 8 different ways of logically corrupt the DIT online (No ESE level database modifications required)
I’m going to describe one of them, because it’s the only one I know ntdsutil can reliability fix, “genericControl” with a value of “CorruptDB:corruptObjTblRef:<DN>” will logically corrupt the object of <DN> by writing (-2) into the object’s objectCategory attribute as shown in the illustration below.
As seen in the illustration above – one the rootDSE mod has been executed, we can’t expand the object anymore, we get the same error message and that (-1601 aka JetErrRecordNotFound) – cause we can’t follow the reference to our objectCategory anymore there is no (-2) DNT in the local DIT.
How can we get some diagnostics for this logically corrupted object?
Having a tool like ESEDump this is easy…
Is there any native tools to use? Yep – ntdsutil.exe and the semantic database analyser – but to get view a specific object using it, we need to know the DNT of the object – bah.
Okey so is there any native tools to do DN to DNT translation online?
Guess what? There is another rootDSE mod for this, but as the last one, only available under debug/checked version of ntdsai.dll
The rootDSE mod “getExtendedObjectData” takes a value of a DN.
Well that didn’t work did it? It’s exactly what it did, inside that NameErr some stuff are encoded.
This is as far as we get online in the absence of “CheckAndFixDNReference” – let’s head over to ntdsutil.exe and the semantic database analyser – this requires that we take the DS offline.
Okey we can get some basic stuff, let’s roll with a “go fixup” to see if ntdsutil finds some issues in the DIT and eventually fix them.
Having a look at the log:
dsdit.dmp
Removing Non-existent references:Object 6526 has reference (colid 1364) to non-existent DNT -2. Removed
Let’s bring the DS online again and look if we can access the object “GOOSE” again.
Yes, we can now successfully access the object “GOOSE” again, however something is missing – let’s leave it as that, but comment if you can figure what’s missing.
That was one way of dealing with the problem before
It’s 2026 so let’s take another approach with – the “dumpReferences”, “dumpDatabaseExtended” and “CheckAndFixDNReference” rootDSE mod
Well there is a lot of things that don’t get any easier, and that is to determine where on a object we have a dangling DN reference – the best we can do is to get all attributes with such syntax over the given object, or we could reduce that a bit to only the attributes possible for the objects given class (How-ever the logical corruption could still be on a attribute that was once valid for the given class, but is no longer)
Let’s have a look again on a logically corrupted object, we can’t expand the object anymore, we get the same error message again (-1601 aka JetErrRecordNotFound) – most likely some reference issue.
Now let’s try another approach to get the DNT of this object, that won’t require a debug/checked version of ntdsai.dll.
dumpReferences
dumpReferences is a rootDSE mod available since Windows Server 2012 that will dump all none-linked attributes and objects containing a references pointing to a given object’s DN but written as a text file to the DCs disk with the DNT instead of the DN. Since all objects have the obj-Dist-Name (distinguishedName) pointing towards them self, we gan convert from DN to DNT
So let’s get the DNT of “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”
Opening the “ntds.ref.dmp” file manually will look something like, depending on numbers of object pointing towards the object you dumped the DN for:
But in our case for “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com” – it happened to look like:
We do know now that “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com” is equal to the DNT “5228” without having a debug/checked version of ntdsai.dll. Knowing the DNT is required for the next step
dumpDatabaseExtended
So now when we got the DNT of the troublesome object that seems to have reference integrity issues caused by a logical corruption as the object “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com” can’t be read we’re going on a chase for that DN syntaxed attribute that have a dangling DNT reference to a none-existent object/phantom/row in our DIT.
This can be done in a few different ways using the rootDSE dumpDatabaseExtended mod that is a bit poorly documented at 3.1.1.3.3.41 dumpDatabaseExtended I did decode/reverse it so let’s document it a bit more complete here – the format documented is
LDIF sample
dn: changetype: modify add: dumpDatabaseExtended dumpDatabaseExtended: 01:01:*:description sn -
Mentioning “The effects of dumpDatabaseExtended are outside of the state model. An update of dumpDatabaseExtended causes part of the contents of the Active Directory DCs database to be written to a text file on the DCs disk.
The format is like this:
XX:YY:OO: XX = 00 = both obj och phantoms (all records) XX = 01 = IsDeleted XX = 02 = only phantoms YY = The index to be used, see YY list OO = Only “*” supported – but supporting a DNT or DN here would have been very nice
YY as following: 00 – “DNT_index” – “Index over DNT_col” 01 – “PDNT_index” – “Index over PDNT_col and ATTm589825 (RDN)” 02 – “Ancestors_index” – “Index over Ancestors_col” 03 – “deltime_not_recycled_index” – Index over “time_col” 04 – “DRA_USN_CRITICAL_index” – “Index over NCDNT_col, ATTq131192 (uSNChanged), ATTi590692 (isCriticalSystemObject) and DNT_col” 05 – “DRA_USN_index” – “Index over NCDNT_col and ATTq131192 (uSNChanged)” 06 – “exprocesslinks_index” – “Index over extended extendedprocesslinks_col used by delayed tasks on the object or links that the object are part of” 07 – “nc_guid_Index” – “Index over NCDNT_col and ATTk589826 (objectGUID)” 08 – “PhantomIndex22” – “Index over time_col” 09 – “recycletime_index” – “index over recycle_time_col” 10 – “INDEX_00000000” – “Index over ATTc0 (objectClass)” 11 – “INDEX_00090062” – “Index over ATTj589922 (primaryGroupID)” 12 – “INDEX_000904E1” – “Index over ATTh591073 (proxiedObjectName)” 13 – “INDEX_000200D2” – “Index over ATTm131282 (proxyAddresses)” 14 – “INDEX_00090001” – “Index over ATTm589825 (RDN)” 15 – “INDEX_00090092” – “Index over ATTr589970 (objectSID)”
Bonus if (08) – “PhantomIndex22” index and (03) – “deltime_not_recycled_index” looks the same, they are not – in their ESE index definition they have something called a conditional column so in addition:
PhantomIndex22 has two conditional columns on objectGUID being non-null and obj-Dist-Name being null – this filters out structural phantoms and ensure no objects are included, hence the null requirement for the obj-Dist-Name.
deltime_not_recycled_index has one conditional column on recycle_time_col being null.
Get all DN-syntaxed attributes for the user objectClass
$SchemaPath = (Get-ADRootDSE).schemaNamingContext# 1. Get all attributes that have a DN-related syntax (2.5.5.1, 2.5.5.7, 2.5.5.14)$DNSyntaxOIDs =@('2.5.5.1','2.5.5.7','2.5.5.14')$DNAttributes =Get-ADObject-SearchBase $SchemaPath -LDAPFilter "(&(objectClass=attributeSchema)(!linkid=*)(|(attributeSyntax=2.5.5.1)(attributeSyntax=2.5.5.7)(attributeSyntax=2.5.5.14)))"-Properties ldapDisplayName, attributeSyntax$DNAttrNames = $DNAttributes.ldapDisplayName# 2. Resolve User class inheritance tree and auxiliary classes$CurrentClassName ="user"$ValidClassNames = [System.Collections.ArrayList]@()while ($CurrentClassName) { $ClassObj =Get-ADObject-SearchBase $SchemaPath -Filter "lDAPDisplayName -eq '$CurrentClassName'"-Properties subClassOf, auxiliaryClass, systemAuxiliaryClass, mayContain, mustContain, systemMayContain, systemMustContainif (-not $ClassObj) { break } [void]$ValidClassNames.Add($ClassObj.lDAPDisplayName)# Collect container attributes for this level of the class hierarchyforeach ($prop in@('mayContain','mustContain','systemMayContain','systemMustContain')) {if ($ClassObj.$prop) { foreach ($val in $ClassObj.$prop) { [void]$ValidClassNames.Add($val) } } }# Check auxiliary classes if presentforeach ($aux in@('auxiliaryClass','systemAuxiliaryClass')) {if ($ClassObj.$aux) { foreach ($val in $ClassObj.$aux) { [void]$ValidClassNames.Add($val) } } }if ($ClassObj.subClassOf -eq $CurrentClassName) { $CurrentClassName =$null } else { $CurrentClassName = $ClassObj.subClassOf }}# 3. Intersect valid attributes for the user class and STORE in a variable$UserDNAttributes = $DNAttributes |Where-Object { $ValidClassNames -contains$_.ldapDisplayName } |Select-Object@{N="AttributeName";E={$_.ldapDisplayName}},@{N="SyntaxOID";E={$_.attributeSyntax}} |Sort-Object AttributeName# 4. (Optional) Display the contents$UserDNAttributes
Or you can choose to get all DN-syntaxed attributes in the schema
Get all DN-syntaxed attributes in the schema
# 1. Get all variations of DN syntax (2.5.5.1, 2.5.5.7, 2.5.5.14)$SchemaPath = (Get-ADRootDSE).schemaNamingContext$DNFilters ="(attributeSyntax=2.5.5.1)(attributeSyntax=2.5.5.7)(attributeSyntax=2.5.5.14)"$UserDNAttributes =Get-ADObject-SearchBase $SchemaPath `-LDAPFilter "(&(objectClass=attributeSchema)(!linkID=*)(|$DNFilters))"`-Properties ldapDisplayName, attributeSyntax |Select-Object@{N="AttributeName";E={$_.ldapDisplayName}},@{N="SyntaxOID";E={$_.attributeSyntax}} |Sort-Object AttributeName# 2. (Optional) Display the contents$UserDNAttributes
So now try to use the “dumpDatabaseExtended” to get all the DN-syntaxed attributes in the DIT and try to find DNT 5228 (“CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”)
Call the dumpDatabaseExtended rootDSE mod
# 1. Get all variations of DN syntax (2.5.5.1, 2.5.5.7, 2.5.5.14)$SchemaPath = (Get-ADRootDSE).schemaNamingContext$DNFilters ="(attributeSyntax=2.5.5.1)(attributeSyntax=2.5.5.7)(attributeSyntax=2.5.5.14)"$UserDNAttributes =Get-ADObject-SearchBase $SchemaPath `-LDAPFilter "(&(objectClass=attributeSchema)(!linkID=*)(!ldapDisplayName=*WellKnownObjects)(|$DNFilters))"`-Properties ldapDisplayName, attributeSyntax |Select-Object@{N="AttributeName";E={$_.ldapDisplayName}},@{N="SyntaxOID";E={$_.attributeSyntax}} |Sort-Object AttributeName# 2. Call the 'dumpDatabaseExtended' rootDSE mod$RootDSE = [ADSI]"LDAP://RootDSE"$RootDSE.Put("dumpDatabaseExtended", [String]::Format("00:15:*:{0}", [String]::Join(" ",$UserDNAttributes.AttributeName)))$RootDSE.SetInfo()
Now let’s trim the output to find DNT 5228 (“CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”)
So we do now have a a list of all DN-syntaxed none-linked attributes for DNT 5228 (“CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”) and we can start to investigate if we think we have any of them pointing towards a none-existent DNT in the DIT.
Let’s have a look here:
DNT 9696 looks suspicious – so why is that?
Because the highest DNT allocated in our DIT is 5307, we can get this online via the operational attribute approximateHighestInternalObjectID, but also just scroll to the end of the ntds.dmp file we dumped with the rootDSE mod ‘dumpDatabaseExtended’
But let’s search the dit.dmp for the DNT 9696 so be sure
CheckAndFixDNReference is a rootDSE mod available for Windows Server 2019, Windows Server 2022 and Windows Server 2025 depending on the patch level. It’s capable of fixing up reference integrity issues such as dangling DN references online – but is limited to syntaxes (2.5.5.1, 2.5.5.7, 2.5.5.14) on none linked attributes (cause linked-attributes rarely suffers from reference integrity issues).
CheckAndFixDNReference attribute is supported in Windows Server 2019 operating system with [MSKB-5078752], Windows Server 2022 operating system with [MSKB-5078766], Windows Server 2022, 23H2 operating system with [MSKB-5078734], Windows Server 2025 operating system with [MSKB-5068966] and later.
So now we have everything to use ‘CheckAndFixDNReference’ to fix up the CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com object that has a dangling DN reference in the ‘seeAlso’ attribute as we have discovered above.
However the ” support two operation modes
0 – Check verify that the dn syntaxed none-linked attribute reference cause a logical database corruption, if there is a reference to a none-existing row (object or phantom) it will be reported by an event logged in the Directory Services log.
1- Fix the logical database corruption by removing the value of the dn syntaxed none-linked attribute, the invalid reference from the database.
# Define the query parameters in a hashtable for optimized filtering$FilterHashtable =@{ LogName ='Directory Service' Id =3091}try {# Retrieve the single most recent event matching the ID $LatestEvent =Get-WinEvent-FilterHashtable $FilterHashtable -MaxEvents 1-ErrorAction Stop $LatestEvent.Message}catch [System.Exception] {Write-Warning"No event with ID 3091 was found in the Directory Service log, or the log could not be accessed."}
We can now see that this is indeed a logical corruption where the ‘seeAlso’ attribute points to a DNT that don’t exist in the database ‘9696’
Now let’s run ‘CheckAndFixDNReference’ in fix mode to have the invalid reference that cause the logical corruption removed.
Now let’s get the 3091 event again from the Directory Services log:
We can see that the ‘Operation Mode’ this time is ‘Fix’ – and we can now also verify that our object is accessible and no longer causing a logical corruption in the database (DIT)
Thank you for reading and if you did follow the entire post to the end, it turned out to be long and might with to much details.
The Hybrid Identity Protection Conference (HIP) Europe 2026 in Frankfurt took place last week and I presented my session “Inside Active Directory: Misconceptions, Myths and Bugs”
I would like to start to summarize the first subject I covered in the session – the Security Descriptor Propagation Daemon (SDP) also known as SDPROP. It’s been a misconception for a long time that SDPROP would have been a process/task or in anyway related to AdminSDHolder – it’s NOT other than AdminSDHolder eventually would be one of many consumer of SDPROP e.g. making a change to an objects ntSecurityDescriptor attribute (Modify an objects securityDescriptor)
There are 100+ articles, blog posts and presentations that have got this wrong over the years, but I want to highlight a write up that describes AdminSDHolder very well: AdminSDHolder: Misconceptions, Misconfigurations, and Myths – SpecterOps – all credits to Jim Sykora for this write up.
Fix up the Ancestry in the Active Directory database (DIT)
ResetRDN (This is cause a index is changed used to enumerate children)
ResetDN
Lost parent in replication conflict
Conflict between reference phantom and structural phantom
Patches GUID-less objects if they have a SID
I did a demo in the session where I used SDPROP to patch up a GUID-less object, on a functional domain controller there should be no need to invoke SDPROP manually, but it can be trigged using a RootDSE Modify Operation – fixupInheritance.
Warning 1: If you need to make a GUID-less object for testing purposes, you can make one in a dedicated test forest, DO NOT ATTEMPT to create a GUID-less object in a production forest.
Warning 2: SDPROP can only patch GUID-less object if the object has a SID so this can only be done against a security principal – this is because the GUID is calculated based on the SID – Why is that? Because SDPROP runs independently on each domain controller and they must be able to set the very same GUID.
If you want to enqueue a propagation for the entire tree, this can be done by setting ‘fixupInheritance’ to ‘1’, don’t do this in a production forest unless you know what you’re doing.
To find an objects DNT without the need to dump the entire DIT, there is since Windows Server 2012 another RootDSE Modify Operation – ‘dumpReferences’ that can be used to translate between DN and DNT for objects. (This is due to the fact that all objects (not phantoms) are keeping a reference to them self by the DN attribute)
On the targeted domain controller you will get a text file within the same location as your Active Directory database (NTDS.dit) file, for example C:\Windows\NTDS\ named ntds.ref.dmp – the content should look like.
ntds.ref.dmp
Non-linked references to CN=ULF,OU=GUIDLess,DC=dstest,DC=chrisse,DC=comDNT Attribute(s)5559 distinguishedName
Ones we have obtained the DNT of the object using the self-reference from the DN attribute, we can proceed and trigger SDPROP up-on that specific object using it’s DNT.
if the object was GUID less you should now see that it has a GUID assigned to it again, also the domain controller should have logged this:
Get GUID fix event
Get-WinEvent-LogName 'Directory Service'-MaxEvents 10|Where-Object {$_.id-eq'2084'} | ft -Property Message -Wrap
This pretty much covers what I did show in the presentation, not let’s deep dive into SDPROP and look at how it works (something you can’t do in a 40 min session)
Implementation in the Active Directory database (NTDS.DIT) – SDPROP implements the following table: sdproptable
Column Name
Description
Introduced
order_col
This is the primary key
Windows 2000
begindnt_col
This is the DNT the propagation begins at
Windows 2000
trimmable_col
Indicates if this propagation can be merged with another propagation
Windows 2000
clientid_col
The thread that has enqueued the propagation, if enqueued by the SDPROP thread can the client id would be 0xFFFFFFFF, in all other cases it would be the thread state client id.
Windows 2000
flags_col
This column holds a set of flags, or no flags at all
Windows Server 2003
checkpoint_col
This column contains list of DNT’s (containers) and children to process, as well a list of deadends if the propagation encountered any issues.
Note: the duration of the propagation must be long enough for a checkpoint to be saved.
Windows Server 2003 Note: The format changes in SP1
unsafe_ancestors_col
if the propagation hasn’t finished and one or more objects have changed their parent- child relation, this hasn’t yet been reflected in the ancestors_col and therefor a list of “unsafe” ancestors have been saved, that would result in an eventual false-positive parent-child relation.
Note: This column is multi-valued and contains each DNT as it’s own value, reading all iTagSequence values is require to get the full list of DNTs
Note: The last DNT in the DNT’s list must match the DNT of the ‘begindnt_col’
Windows Server 2008
Here is a screen of how the “sdproptable” looks like when dumped with ESEDump
So is there a way to read the data out of the “sdproptable” from the “outside” using LDAP or any other APIs? Yes, sort of.
However this can only retrieve any pending propagations that was caused or enqueued by the same thread as the current LDAP connection, that would match the threads id in the “clientid_col”
The “dSCorePropagationData” none-replicated attribute and it’s relation to SDPROP
The “dSCorePropagationData” get’s updated by SDPROP with timestamps when SDPROP is working on the object and a set of flags associated with the stamps or the object/phantom.
Since Windows Server 2008 a propagation is always enqueued even if the SD hasn’t changed (is bitwise equal to the already existing SD) unless the 14th bit of the dSHeuristics is set aka fDontPropagateOnNoChangeUpdate – as documented here – 6.1.1.2.4.1.2 dSHeuristics -as this is FALSE by default on Windows Server 2008 and later a timestamp is written into “dSCorePropagationData” even if the SD didn’t change.
This attribute has the syntax String(Generalized-Time) – and is multi-valued, where the first value holds the flags and the other values holds a maximum of 4 timestamps, the following flags can be associated with each timestamp or object: SDP_NEW_SD = 1 SDP_NEW_ANCESTORS = 2 SDP_TO_LEAVES = 4 SDP_ANCESTRY_INCONSISTENT_IN_SUBTREE = 0x08 SDP_ANCESTRY_BEING_UPDATED_IN_SUBTREE = 0x10
if there is a need to write a 5th timestamp into the “dSCorePropagationData” attribute the 2th value is overwritten together with it’s corresponding flags.
You can view this attribute using LDP, it decodes the timestamps and the flags, but do not pair the flags with the time stamps
Looking at the same object using ESEDump:
The flags: SDP_ANCESTRY_INCONSISTENT_IN_SUBTREE | SDP_ANCESTRY_BEING_UPDATED_IN_SUBTREE signal to Active Directory query optimization code that the ancestors index isn’t safe to use for subtree searches as the SDPROP is working on brining it to a consistent state and that it might would generate false positives.
Security Descriptor (SD) – Single Instance Storage (SIS) in the Active Directory aatabase (NTDS.dit)
It’s time to go into the full possible parameters of the RootDSE modify operation ‘fixupInheritance’ – but before we can do that, we need to understand – single instance storage of security descriptors in the Active Directory database (NTDS.dit) and how it was implemented and introduced with Windows Server 2003.
Before the introduction of the “sd_table” every SD was stored on each object’s row within the “datatable” – directly in the “ATTp131353 / nTSecurityDescriptor” column.
Once the “sd_table” was introduced in Windows Server 2003, SDs was MD5-hased and placed in the “sd_table” instead of the “datatable” – leaving only a reference in the “datatable” pointing to the row storing the SD in the “sd_table”, if a SD to be inserted into the “sd_table” already existed as an existing – already stored SD, that row was referenced instead.
In Windows Server 2003 operating system and later, setting the fixupInheritance attribute to the special values “forceupdate” and “downgrade” has effects outside the state model.
The “downgrade” have the effect that all SDs are moved off the “sd_table” and put back into the “datatable” onto each object or phantoms row, as it was in a Windows 2000 style DIT 🙂
So in a fairly modern Active Directory database (NTDS.dit) aka post-Windows 2000 Server – it will look like this for the “ATTp131353 / nTSecurityDescriptor” in the “datatable”
If we look up the SDID reference in the “sd_table” we will find the actual SD
Warning: Do not test or perform this operation is a production environment
Let’s try the RootDSE modify operation ‘fixupInheritance’ with the parameter set to “downgrade”
Let’s have a look again at the same object in the “datatable”
Same SD but now stored directly in the “datatable” – I don’t expect you to compare the SDs but they do match 🙂
I don’t think I need to go into what the “forceupdate” parameter is doing.
Some old but good real-word scenarios related to SDPROP and the Active Directory database
I presented this at my first HiP Conf in 2017 in my session – “Inside the Active Directory database (NTDS.DIT)” – That is almost 10 years ago.
Case 1
The funny thing here is a added the -GetRecordSize into ESEDump to troubleshoot this very issue, it calls into the JetGetRecordSize function of the ESENT API.
Now in Windows Server 2025 this is available as an attribute natively – msDS-JetGetRecordSize3 that calls into a later version of JetGetRecordSize than me, JetRecordSize3 don’t seem to be publicly documented yet.
This is how it looks like in ESEDump
Case 2
The SDID reference between the “datatable” and the “sd_table” broke, the SDID stored for that object in the “datatable” pointed to a none-existent row in the “sd_table”
By the way, the database semantic checker would have patched up the GUID-less object as well, but that would have required to take the DC offline.
For some time LDP.exe just crash for me on my Windows 11 laptop, I can connect and bind successfully, but as soon as I want to view an object or tree, ldp.exe just crash.
So I decided to capture and analyze the crash dump
So what is going on here? The function takes the following parameters: BerEncode(CtrlInfo *ci, PBERVAL pBerVal)
This lead me to check my controls loaded in ldp.exe – what is that?
Something I’ve never entered at least, that is for sure? But how did it end up there?
It turned out that if, “HKEY_CURRENT_USER\Software\Microsoft\Ldp\Controls\ControlCount” get’s out of sync and contains a value that’s above the numbers of controls saved, it adds this kind of garbage data and will cause ldp.exe to crash!
I tried to dump a NTDS.dit from a RODC with ESEDump – something I haven’t done in years and just stumbled up on the fact that i got NDNC’s (None-Domain Naming Contexts) appearing twice in ‘msDS-HasInstantiatedNCs’ – ESEDump did work as expected but I started to question my self if my code walking the range in the “link_table” worked correctly.
I then thought that this must have something to do with the fact that this RODC is promoted from IFM. Let’s have a look on that attribute with repadmin.exe
Yep, I but those are my two ‘duplicated’ NDNCs – but why? I wrote two articles in the past about how IFM is working and did almost cover this but missed it in the first part as it solidly was focusing on Windows Server 2003 – it says:
Sourcing NDNCs with Windows Server 2003 is only supported by Windows Server 2003 SP1 or later under the following conditions:
Both the DC your souring the IFM from must be running at least Windows Server 2003 SP1 or later and as well the machine intending to become a DC using the source IFM.
The forest functional level (FFL) has to be: Windows Server 2003 (Pre-Windows Server 2003 FFL adding replicas to NCs has to be done on the Domain Naming Master – FSMO) Note: The promotion completes with the sourced IFM even if the forest functional level (FFL) is less than Windows Server 2003 but NDNCs aren’t sourced from the IFM and the following will happen:
The DomainDNSZones and ForestDNSZones are begin replicated in again over the wire using normal replication, as the promoted DC (Sourced from IFM) hosts the DNS Service
I forgot to mention that it’s not supported to keep any NDNCs in the DIT for the Red-Only Domain Controller IFM case – those get wacked and replicated back in again.
The solution here if I really only want to get PRESENT links would be to change incides over the “link_table” to the “present” ones depending on fwd o back links, the Recycle-Bin was not enabled in this environment.
All DIT’s on Windows Server 2025 isn’t 32k page size (Yes I know – I did say that in my last blog post), there is two exceptions, 1: the IPU case aka In-place-upgrade of a pre-Windows Server 2025 DC, in that case the DB will remain as-is in terms of page size.
You can verify the actual page size using a new attribute msDS-JetDBPageSize to determine the page size of a Windows Server 2025 DSA.
This clearly show us that the page size of the one and only DSA running Windows Server 2025 is 8k – this is because the DIT comes from a an IPU’ed aka in-place-upgraded Windows Server 2022 DC.
Let’s have a look with esentutil /m C:\Windows\NTDS\NTDS.dit
For sure the DIT is 8k page size, we can also validate by it’s DB Signature that it was created back in 2017, we can compare with the Distribution DIT that is 32k page size and created in 2023 – more on the Distribution DIT can be found here.
So how do we get to 32k page size on this DC? The answer is you can’t unless you demote and re-promote it. You can get to Windows Server 2025 DFL and FFL – but you can’t enable the “Database 32k pages feature”
There is is a 2:nd way to get up additional DCs running Windows Server 2025 with 8k page size DIT’s.
If you produce IFM media of a Windows Server 2025 DC with 8k page size – the Windows Server 2025 DCs you promote using that IFM media also going to have 8k page size of their DITs.
IPU-DC-02 is promoted using a IFM produced off IPU-DC-01 that once was IPU:ed (In-place-upgraded) from Windows Server 2022.
Extensible Storage Engine – ESENT and the Engine Format Version
Some where after Windows 19H1 version 1903 – released in May 2019 and within Windows 20H1 version 2004 – ESENT seem to have got support for something called “EngineFormatVersion” that enables specific features depending on the ESE Engine version – by default if a ESE DB is attached on a more recent engine than the ESE DB was initially created on, it and it’s logs are upgraded to the current ESE Engine version, that might break backwards compatibility to be able to attach the ESE DB by a older version of the ESE Engine (older version of the operating system with an ESE Engine only supporting certain EngineFormatVersions) – this is by default aka:
ESENT.h
#defineJET_efvUseEngineDefault (0x40000001) // Instructs the engine to use the maximal default supported Engine Format Version. (default)
How ever the Active Directory DSA (ntdsai.dll) have decided to support databases (DIT’s) that don’t even support the efv (Engine Format Version) by setting a hard version of:
ESENT.h
#defineJET_efvWindows19H1Rtm8920 // Last pre-efv version, shipped in Windows 10 until 19H1 release.
This sort of came up and got awareness to the outside world by Michael Grafnetter when someone used his incredible tool dsinternals to attach to a DIT and the default – JET_efvUseEngineDefault was used, it upgraded the DIT and when the Active Directory DSA (ntsdsai.dll) would try to attach to it again it would have a efv (Engine Format Version) that was way head of it’s hardcoded JET_efvWindows19H1Rtm
Ops the DIT can no longer be used by the Active Directory DSA (ntsdsai.dll).
ESENT error -623 is JET_errEngineFormatVersionSpecifiedTooLowForDatabaseVersion
This was resolved by setting the JetSetSystemParameter JET_paramEngineFormatVersion to JET_efvUsePersistedFormat before attaching the database – Michael figured out that this was how the esentutil would do it. DsIntetrnals got updated and I updated my ESEDump tool pretty much the same way:
ESEDump source
if (DbInfoMisc.dwBuildNumber>=20348) err =E.Check(EseHelper.JetSetSystemParameter(ref instance,EseHelper.JET_sesidNil,newIntPtr(EseHelper.JET_paramEngineFormatVersion),newIntPtr(EseHelper.JET_efvUsePersistedFormat),null));
What happened to NTDSUTIL.exe and 8k size DITs?
For some reason if you’re using ntdsutil.exe against a 8k size DIT on Windows Server 2025 and it attaches your DIT – It’s going to destroy your dit and your DC will never boot again – guess what? It upgrades your DB beyond efv (Engine Fromat Version) 8920 (JET_efvWindows19H1Rtm) – This is NOT good and is another bug in Windows Server 2025.
If you’re on Windows Server 2025 with 8k page size DITs – stay away from ntdsutil.exe and especially the file context, it will destroy your DIT. Or to be honest stay away from 8k page size DITs on Windows Server 2025 to being with.
Update 2025-11-07: Microsoft responded quickly and seriously to this issue and a fix is already on it’s way, also note that the DIT is not corrupted in anyway so that data is NOT lost, it’s just upgraded to a later version and the DSA is refusing to attach the DIT as Active Directory don’t support that ESE evf (Engine Fromat Version)
So what is going on here – a bad exchange schema upgrade that would cause replication to fail? At first you can think that would be the case – it’s NOT according to me, but I would hold on that the exchange schema upgrade do something bad [1] – let’s get back to that later.
So when can this disaster strike – if you’re running Exchange adprep to extend your Active Directory schema for the first time, aka the forest had no previous versions of Exchange schema extensions – nope nothing happens regardless of the versions of your DCs, Including the Schema FSMO – running Windows Server 2025.
If you run adprep and ALREADY have extended your schema for Exchange previously and the Schema FSMO would be running Windows Server 2025 – then you would run into issues – But what really happens?
Attributes with the syntax 2.5.5.2 aka String(Object-Identifier) suddenly takes duplicates – here it’s time to tell what exchange is doing bad [1] – Exchange adprep never check if a value is already present in those attributes, instead on each update (CU/Release) it’s trying to add the same value again, relaying that AD throw’s them out with ATT_OR_VALUE_EXISTS – below we can see how that works when the Schema – FSMO is held by a Windows Server 2008 R2
But this will be allowed on Windows Server 2025?
That is not good – and it will break Active Directory replication on the receiving end if it’s a down-level DC (e.g. not Windows Server 2025 [2]) the update will only apply one of the duplicate e.g. the duplicate detection will work, so if we’re looking up our example class here ‘Address-Book-Container’ between the schema FSMO and another DC.
Why will it not break replication to other Windows Server 2025 DCs? Well they suffer from the same duplication bug – or do they really? [2]
This is how it would look like on a down-level DC (e.g. not Windows Server 2025)
And of course it’s a schema mismatch when the definition of a class is different between two replocas, one with two values for the attribute auxiliaryClass (2) country;country while the other only has auxiliaryClass (1) country;
Well let’s go a step beyond this and leave Exchange for a while, let’s add our own attr (chDsObjid) with the syntax 2.5.5.2 aka String(Object-Identifier)
Windows Server 2025 DC
Windows Server 2022 DC
So the same behavior – however this will not cause a schema mismatch and break replication, only bring inconsistency.
So what is wrong here, something most be wrong with Active Directory’s duplication logic on Windows Server 2025 [2] or?
Let’s again look at how it should work – we should be thrown out if where trying to add a value that already exists on a attribute with syntax 2.5.5.2 aka String(Object-Identifier)
We are and it works as expected on down-level DC (e.g. not Windows Server 2025) – We get a DSID – that can give us a pointer to where this is blocked in the ds source. I happen to know that AD has it’s own detection for duplicates values for certain syntaxes, but not for 2.5.5.2 aka String(Object-Identifier) – here Active Directory solidly relays on Extensible Storage Engine – ESENT / Jet for value duplication detection.
JetSetColumn is called with grbit JET_bitSetUniqueMultiValues = 0x00000080 – this seems to fail on Windows Server 2025? [2]
I felt I had to try out this my self on the Extensible Storage Engine – ESE level so what the heck how hard can it be to modify ESEDump to do writes to the DIT:
So let’s try this and see if we’re thrown out by ESE – if we first try with a Windows Server 2022 DIT (8k)
Yes that works as expected, let’s try the very same code now on a Windows Server 2025 DIT (32k)
Ops – Something must be wrong within Extensible Storage Engine -ESE (ESENT.dll) – we’re getting through here even if we’re calling JetSetColumn with grbit JET_bitSetUniqueMultiValues and a value that is already present in the column.
But let’s try a DIT from Windows Server 2022 (8k) on Windows Server 2025 🙂 We’re thrown out with ESENT error: A duplicate value was detected on a unique multi-valued column.
But wait wasn’t Windows Server 2025 broken or had some defect in ESENT.dll?
My understanding is that this has to do with the page size of the NTDS.dit database and that JET_bitSetUniqueMultiValues don’t work correctly on 32k pages DBs and has never done, no matter of the underlaying operating system, it just happen to be that on Windows Server 2025 all NTDS.dit databases are 32k pages.
But wait shouldn’t the database be 32k pages first when the “Database 32k pages optional feature” has been enabled? No, again all NTDS.dit’s on Windows Server 2025 is 32k page by default, the “Database 32k pages optional feature” only let go of restrictions enforced to be able to co-exist with downl-level replicas (e.g. none Windows Server 2025 DCs)
[2] The issue has nothing to do with Active Directory – it seems to be a bug in Extensible Storage Engine – ESENT (ESENT.dll)
Summary
It took me a day to figure out and test this, including writing a version of ESEDump that could prove this. Exchange should look for the values they are trying to add and not relay on Active Directory throwing an error that the value already exists.
How can you find effected attributes, run the following AD query against your Schema NC:
It is possible to fix this if you run into it – without during a forest recovery. Please contact Microsoft Support and they will help you to get back in a supported manner, I know how to get out of this as well – but I’m not Microsoft nor do I work for Microsoft, I’m just a Active Directory geek that like to figure out how things work, or not work.
So the security updates for October has arrived and “AllowNtAuthPolicyBypass” registry key is now gone from kdcsvc.dll – All CAs that issue certificates to be used for PKINIT against Active Directory must now be trusted in NTAuth.
Please do not add CA’s to NTAuth that you don’t trust, as any one who can issue a certificate with subject of choice from those still can impersonate any user account within your forest e.g. a DA/EA and this is regardless of StrongCertificateBindingEnforcement and NTAuthEnforcement.
A good solution to keep NTAuth safe is NTAuthGuard by Carl Sörqvist.
But as I use to say, there is always a secret key – as with “StrongCertificateBindingEnforcement” another key instead of “AllowNtAuthPolicyBypass” can be used to “unsupported so far I know” turn off the NTAuthEnforcement requirement. You will find it by using:
Hybrid Identity Protection (HIP) Conference 2025 is over and I presented on the Active Directory and PKI subject again: “Enterprise PKI Today: Friend or Foe”
StrongCertificateBindingEnforcement vs NTAuthEnforcement
StrongCertificateBindingEnforcement has been mandatory since 10th of September 2025 with no supported way of doing a optout to Compatibility Mode. Enforcing this took over 3 years – and where still not done – while the ‘StrongCertificateBindingEnforcement’ registry key is gone from “kdcsvc.dll” with the September updates. However there is a new key available to still optout but that key is only intended for special cases and should NOT be used, but you can find it by string dumping the “kdcsvc.dll” at a specific offset.
Please be aware that the StrongCertificateBindingEnforcement only protect you from what it was designed to – the following:
dNSHostName/servicePrincipalName computer owner abuse, Remove DNS SPNs from servicePrincipalName, steal DNS hostname of a DC, put it in your computer accounts dNSHostName attr and request a cert, auth (PKINIT) with the cert and you’re a DC.
Overwrite userPrincipalName of user to be of target to hijack user account since the missing domain part does not violate an existing UPN
Overwrite userPrincipalName of user to be @ of target to hijack machine account since machine accounts don’t have a UPN
Delete userPrincipalName of user and overwrite sAMAccountName to be without a trailing $ to hijack a machine account
Note: 2-4 would require permissions to write to the ‘userPrincipalName’ attribute
It will NOT protect you from:
CAs trusted in your forest where you don’t have a good security hygiene for issuance of certificates
If someone can issue a certificate with subject + sid they own that security principal in your Active Directory Forest.
Subject + SID in AltSubject is sadly enough – tag:microsoft.com,2022-09-14:sid:<value>
•If you’re using Authentication Mechanism Assurance (AMA) – you must control/prevent issuance with specific issuance policies.
Bad certificate template hygiene
Supply in the request (SITR) should never be published on a CA trusted in NTAuth
Write access to certificate templates outside Tier 0 allows for SITR to be enabled.
3rd party/standalone CAs or RA’s/EA’s – you’re on your own to block the above.
NTAuthEnforcement
Since July the NTAuthEnforcement has been enabled by default, meaning that all CAs that issue certificates to be used for PKINIT must be trusted in NTAuth – this changes the picture.
Before this new requirement it was possible to be trusted for PKINIT even if the issuing CA was not trusted in NTAuth – if a strong mapping method was used using AltSecID (altSecurityIdentities). This is no longer possible after CVE-2025-26647 as X509SKI (Subject Key Identifier) for example was considered a strong mapping, but it is possible to create a certificate with a designated SKI (Subject Key Identifier) from any trusted CA – this becomes problematic as you could create a SKI (Subject Key Identifier) of an existing mapped user – a T0 administrator for example and become that security principal within the forest.
In my past post “When your Enterprise PKI becomes one of your enemies (Part 6)” i demonstrate how to – Create, Distribute and Force-Trust your own Fake CA within a forest to perform a T1 to T0 privilege escalation – at that time leverage Authentication Mechanism Assurance (AMA).
But let’s using CVE-2025-26647 instead, let’s say we found a T0 – “strongly” mapped with SKI (Subject Key Identifier) within the Active Directory forest.
You can for now until the October patch wave arrive opt-out from the NTAuthEnforcement but then you would be vulnerable to the above “HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc\AllowNtAuthPolicyBypass=1”
Summary
Same mitigation as presented before applies – make sure you have two enterprise issuing CAs where one of them isn’t trusted in NTAuth – this one can publish – Supply in the request (SITR) templates, while the other CA that is in NTAuth – Never should have any – Supply in the request (SITR) templates published. All and both Enterprise CAs must be managed from T0 this is very important, however they can issue certificates to lower tiers.
Strong Certificate Binding Enforcement protects against CVE-2022-34691, CVE-2022-26931 and CVE-2022-26923
It will NOT protect against bad security hygiene on our CAs, Templates or information within your certificates.
NTAuth requirement will protect against CVE-2025-26647 and eliminate all other paths to PKINIT that didn’t required NTAuth
Fake CA Scenario
AMA Abuse using altSecID from non-NTAuth CA
Note all my demos uses ‘CertRequestTools‘ from Carl Sörqvist and in this case also Rubeus from Will Schroeder.