Shadow Credentials: no bug, no patch, still domain admin
Kerberos will log you in with a key stapled to your account, and any delegation leftover with write access can do the stapling. Found it again at big clients, one of them on the US top-100 hospitals list. Find the rogue keys, fix who can write the attribute, alarm the lot.
01 The Big PictureFor leadership · no jargon
Okay, so here's one I keep finding on engagements, including at some very large clients lately. One of them was a hospital group on the US top-100 list, and I got domain admin there without cracking a password, exploiting a bug, or phishing anyone. I asked Active Directory to accept a brand new logon key for a privileged account, and it said yes. This guide walks through what Shadow Credentials actually is, why resetting passwords doesn't fix it, and the tidy-up: finding rogue keys, removing them, and fixing who was allowed to add them in the first place.
The thing that makes this work is legitimate: Kerberos, the authentication system every Windows domain runs on, can log you in with a registered public key instead of a password. That's how Windows Hello for Business signs you in. The list of keys an account trusts lives on the account's own record in the directory, and if someone has permission to edit that list, they can add their own key and become that account. Think of the server room with a key-card reader: the attack isn't picking the lock, it's having write access to the security office's spreadsheet of authorised cards and adding your own. No vulnerability, no CVE, nothing to patch. It's been working exactly as designed since 2016.
Two properties make it nasty on real engagements. It survives password changes (the key is not the password, so rotating credentials does nothing), and it's quiet: no failed logons, no lockouts, no reset events for the helpdesk to trip over. The permissions that enable it are usually ancient delegation leftovers, which is why it keeps turning up at big companies. The fix is three jobs: find any rogue keys and remove them surgically (legitimate Windows Hello keys must survive), correct the delegation so only the right hands can write that list, and put an alarm on the attribute so you hear about the next attempt. Hours, not weeks, no downtime, and the only real care point is not breaking Windows Hello for the people legitimately using it.
02 Technical BreakdownFor engineers
The technique was published by Elad Shamir of SpecterOps in June 2021 (credit where due: Michael Grafnetter demoed the same primitive at Black Hat Europe 2019 and built it into DSInternals). Everything hangs off one attribute on user and computer objects: msDS-KeyCredentialLink. It holds serialised Key Credential blobs: a public key, a device ID, timestamps. Its day job is the Key Trust model for passwordless sign-in, where Windows Hello or a FIDO2 key proves possession of a private key and the KDC checks the matching public key straight off the account, no certificate authority involved.
Normal Kerberos pre-authentication is symmetric: you encrypt a timestamp with a key derived from your password, and the KDC decrypts it with the same secret. PKINIT is the asymmetric path: you sign the pre-authentication data with your private key and the KDC verifies it against your public key. For Key Trust, "your public key" means whatever is sitting in msDS-KeyCredentialLink. So the attack chain is brutally simple: hold write access to that attribute on a target (GenericAll, GenericWrite, WriteProperty scoped to the attribute, WriteDACL or WriteOwner chains, or membership of Key Admins / Enterprise Key Admins), generate a key pair, LDAP-write your Key Credential onto the victim, then request a Kerberos ticket as them with your private key. Whisker plus Rubeus, pyWhisker plus PKINITtools, or a one-shot certipy shadow auto all automate this. A bonus most people miss: when PKINIT is used, the ticket's PAC carries the account's NT hash in an NTLM_SUPPLEMENTAL_CREDENTIAL field, and a User-to-User ticket to yourself hands it over, so you walk away with a reusable hash too.
Computer accounts are the juiciest targets, DC$ most of all: control the DC's account and you're one silver ticket away from the whole domain. There's a quirk worth knowing: users cannot write their own key list, but computers can write their own, only while it's empty. That single rule is what makes the NTLM relay version work: coerce a server to authenticate, relay it to LDAP on another DC, and it staples a key to itself. And unlike its cousin RBCD (resource-based constrained delegation), Shadow Credentials needs no new machine account and no SPN, leaves nothing obvious behind, and the planted key outlives every password reset you throw at it. Resetting the victim's password after a compromise is the classic mistake: it feels like remediation, and the attacker doesn't even notice.
Prerequisites, per the original research: at least one Server 2016 DC, 2016 domain functional level, a server-authentication certificate on the DC (free by default if AD CS or any PKI exists; without one PKINIT refuses with KDC_ERR_PADATA_TYPE_NOSUPP), and the write access above. Not exploitable out of the box, mind you: someone has to have misdelegated first. But as we'll see, someone always has.
03 Affected
Any Active Directory at Server 2016 functional level or later with a certificate on its DCs, which is effectively every maintained domain. The attribute itself arrived with the Server 2016 schema. The exposure is not the attribute, it's the delegation: any principal holding GenericAll, GenericWrite, or attribute-scoped WriteProperty on a juicy account's key list can take that account over.
How common is that in the wild? CrowdStrike's red team named Shadow Credentials and RBCD as the two most common abuses of misconfigured permissions on computer objects across their 2024 operations, and Tenable ships it as a High-severity standing exposure check, so it's common enough to warrant one. Practitioners also treat it as the default move when MachineAccountQuota is 0, since the usual RBCD fallback is blocked. There's no public dataset for how often it appears per sector, so I won't invent one, but anecdotally: big, old, heavily delegated estates, healthcare very much included.
04 Detection
Before we change anything, let's see what we're dealing with. First, who has any key credentials at all:
Get-ADObject -LDAPFilter '(msDS-KeyCredentialLink=*)' -Properties msDS-KeyCredentialLink, whenChanged |
Select-Object Name, ObjectClass, whenChangedEvery user and computer with the attribute populated. On a modern estate this won't be empty, so don't panic yet: the next step sorts real from rogue.
Get-ADObject -LDAPFilter '(msDS-KeyCredentialLink=*)' -Properties msDS-KeyCredentialLink |
Select-Object -ExpandProperty msDS-KeyCredentialLink | Get-ADKeyCredential Raw values are unreadable binary; DSInternals' Get-ADKeyCredential parses them into Usage, Source, DeviceId and creation time. Expect AV/EDR to side-eye DSInternals, it's dual-use.
Get-ADComputer -Filter 'msDS-KeyCredentialLink -like "*"' -Properties msDS-KeyCredentialLink |
Select-Object -ExpandProperty msDS-KeyCredentialLink | Get-ADKeyCredential |
Where-Object { $_.Usage -eq 'NGC' -and -not [string]::IsNullOrWhiteSpace($_.DeviceId) } The cirosec hunt: legitimate computer self-keys have no DeviceId, but attack tools (Whisker, pyWhisker, ntlmrelayx) generate a random one that maps to no real device. On user objects, a DeviceId that matches nothing in Entra (Get-AzureADDevice) is the same smell. A lead, not a law: tooling could be patched to blend in.
Set-AuditRule -AdObjectPath 'AD:\CN=Users,DC=contoso,DC=local' -WellKnownSidType WorldSid `
-Rights WriteProperty -InheritanceFlags Children `
-AttributeGUID 5b47d60f-6090-40b2-9f37-2a4de88f3063 -AuditFlags SuccessThe tripwire (OTRF's Set-AuditRule, via Elastic's prebuilt rule): audit writes to the attribute's schema GUID on user objects. Without a SACL there is no event 5136 at all, and the default audit policy doesn't cover user objects, so "we have auditing on" proves nothing.
Then in the SIEM: event 5136 (directory object modified) where the changed attribute is msDS-KeyCredentialLink and the writer is not the Entra Connect sync account (MSOL_*) or the ADFS service account, the two legitimate Key Provisioning Servers. And event 4768 (TGT requested) with non-blank certificate fields or pre-authentication type 15: PKINIT being used by accounts that never touch smartcards or Windows Hello is a flare. One honest caveat: Microsoft Defender for Identity does raise shadow credential alerts, but independent testing shows it mostly fires at use time and is evadable, so treat it as a bonus layer, not the alarm system.
MATCH p=(n)-[:AddKeyCredentialLink]->(m) RETURN p BloodHound maps this exact exposure as the AddKeyCredentialLink edge. Follow it up with shortest paths into Domain Admins, and remember GenericAll/GenericWrite/WriteDacl/WriteOwner edges imply the same write, so this edge alone undercounts.
05 Remediation
01 Remove the rogue keys, not the password
The injected key survives password resets, so resetting the victim's password is theatre. Remove the rogue value itself, surgically, because legitimate Windows Hello keys live in the same attribute and nuking them breaks sign-in for real users. Parse first, then delete by exact value:
Get-ADUser -Identity victim.user -Properties msDS-KeyCredentialLink |
Select-Object -ExpandProperty msDS-KeyCredentialLink | Get-ADKeyCredential |
Out-GridView -OutputMode Multiple -Title 'Select rogue credentials' |
ForEach-Object { Set-ADObject -Identity $PSItem.Owner -Remove @{ 'msDS-KeyCredentialLink' = $PSItem.ToDNWithBinary() } } Surgical removal (DSInternals, works for computers too): the -Remove hashtable needs the exact DN-with-binary value, which is why we parse first. Pick the rogue keys in the grid, leave the Hello enrolments alone.
Set-ADUser -Identity victim.user -Clear msDS-KeyCredentialLinkThe nuclear option, only for accounts with no legitimate keys. Whisker's own docs warn that clearing a Windows Hello user's attribute breaks their sign-in, and the same applies to you.
If this is incident response rather than hygiene, one more thing from Elastic's playbook: preserve the 5136 events and the attribute blob before you clean, and contain the account that did the writing, not just the account that got written to. The writer is your real intruder.
02 Fix who can write the attribute
Now the root cause. Pull the BloodHound edges from Detection, enumerate the actual ACEs on privileged and computer objects, and strip GenericAll, GenericWrite and attribute-scoped WriteProperty from any principal that has no business there. Replace blanket rights with least-privilege delegation. While you're in there, audit membership of Key Admins and Enterprise Key Admins: both hold write on this attribute by design, and certain Server 2016 adprep versions infamously granted Enterprise Key Admins Full Control over the entire domain naming context. Worth a look, that one.
One adjacent myth to squash: ForceChangePassword (the right to reset someone's password) does not enable Shadow Credentials. Different right, different attack, and the reset version is far noisier, which is exactly why attackers prefer the key.
03 Deny by default, then alarm it
Shamir's original preventive control: an explicit DENY for Everyone writing msDS-KeyCredentialLink on accounts that should never enrol in Key Trust, privileged accounts first. It comes with a caveat from the same source: anyone holding WriteDACL or WriteOwner can strip a deny ACE, so pair it with the SACL from Detection and let the alarm catch the locksmith.
Finally, close the relay path for computer accounts: the reason a server can staple a key to itself is NTLM relay to LDAP, and LDAP signing plus channel binding (and killing coercion where you can) takes that default-ish primitive off the table.
06 Verification
Fixed, or just feeling fixed? Let's prove it.
Get-ADObject -LDAPFilter '(msDS-KeyCredentialLink=*)' -Properties msDS-KeyCredentialLink |
Select-Object -ExpandProperty msDS-KeyCredentialLink | Get-ADKeyCredentialRe-run the full parse. Every remaining entry should be explainable: a documented Windows Hello or FIDO2 enrolment whose DeviceId resolves to a real device, or a computer self-key with an empty DeviceId.
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=5136} -MaxEvents 50 |
Where-Object { $_.Message -match 'msDS-KeyCredentialLink' }Confirm the tripwire is live: recent attribute writes should be generating 5136s from your new SACL. No events at all on a domain you know uses Windows Hello means the SACL didn't take, not that nobody writes keys.
Then refresh your BloodHound collection and re-run the AddKeyCredentialLink path queries: the paths into privileged groups should be gone, not just quieter. And as a last sanity check, have a look at 4768s for your Tier 0 accounts: any PKINIT logon from an account that doesn't do passwordless deserves a conversation.
07 Rollback
The realistic rollback here is having been too aggressive with the cleanup and breaking someone's Windows Hello. The fix is boring, thankfully: the user re-enrols, their device generates a fresh key pair in the TPM, and Entra Connect writes the new public key back to the attribute. Microsoft support forums are full of exactly this dance, and the lesson worth keeping is that msDS-KeyCredentialLink should have a single authoritative writer; two systems fighting over it (ADFS enrolment versus Entra Connect sync is the classic pairing) is how keys vanish mid-morning.
If you stripped a delegation that turns out to be load-bearing (a helpdesk group that legitimately provisions Hello keys, say), re-add the specific right, attribute-scoped WriteProperty, rather than handing GenericAll back because it's quick. That habit is how we got here :)
08 References
- Elad Shamir, Shadow Credentials: Abusing Key Trust Account Mapping for Takeover (SpecterOps, June 2021)
- Microsoft MS-ADA2: msDS-KeyCredentialLink
- Microsoft event reference: 4768 (TGT requested) and 5136 (directory object modified)
- DSInternals: Get-ADKeyCredential documentation
- cyberstoph: Detecting shadow credentials (rogue vs legit triage)
- cirosec: Microsoft Defender for Identity evasions, part II
- Elastic prebuilt rule: Potential Shadow Credentials added to AD Object
- BloodHound edge reference: AddKeyCredentialLink
- Tenable Identity Exposure: C-SHADOW-CREDENTIALS
- CrowdStrike: Top three red team exploitation paths
- Whisker, pyWhisker, Certipy shadow
If I've got anything wrong here, or you've hit an edge case I haven't covered, do say, corrections and questions are always welcome :)
exit(0);
Ship the fix. Prove it’s closed.
If this runbook doesn’t fit your stack, send the CVE — an operator writes a verified path, usually within a business day. Or contribute your own and get co-authored credit.