T1589

Gather Victim Identity Information

Reconnaissance Last updated:

This detection identifies adversary attempts to enumerate victim identity information—credentials, email addresses, and employee names—through active probing of authentication services and monitoring of downstream indicators of OSINT-driven targeting. Since T1589 is a PRE-ATT&CK technique occurring largely outside victim infrastructure, detection focuses on second-order observable signals: anomalous username enumeration via Azure AD sign-in failures with differential error codes (e.g., UserNameDoesNotExist vs. InvalidPassword), Self-Service Password Reset (SSPR) flow abuse, high-volume authentication probing from single sources against multiple distinct accounts, and MFA method enumeration patterns. Groups such as LAPSUS$, Scattered Spider, and HEXANE have exploited these mechanisms to build target identity lists before launching phishing, credential stuffing, or social engineering campaigns.

What is T1589 Gather Victim Identity Information?

Gather Victim Identity Information (T1589) maps to the Reconnaissance tactic — the adversary is trying to gather information they can use to plan future operations in MITRE ATT&CK.

This page provides production-ready detection logic for Gather Victim Identity Information, covering the data sources and telemetry it touches: Azure Active Directory, Microsoft Entra ID. The queries below are rated high severity at medium confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.

MITRE ATT&CK

Tactic
Reconnaissance
Technique
T1589 Gather Victim Identity Information
Canonical reference
https://attack.mitre.org/techniques/T1589/
Microsoft Sentinel / Defender
kusto
let lookback = 1h;
let enumThreshold = 15;
// Primary: Azure AD username enumeration via differential auth error codes
let AzureADEnum = AADSignInLogs
| where TimeGenerated > ago(lookback)
| where ResultType in ("50034", "50053", "50055", "50057", "50072", "50076")
// 50034=UserNameDoesNotExist, 50053=AccountLocked, 50055=PasswordExpired, 50057=AccountDisabled
| extend 
    IsUsernameNotFound = iff(ResultType == "50034", 1, 0),
    IsLockedOrDisabled = iff(ResultType in ("50053", "50057"), 1, 0)
| summarize 
    TotalFailures = count(),
    UniqueUsernames = dcount(UserPrincipalName),
    UsernameNotFoundCount = sum(IsUsernameNotFound),
    LockedDisabledCount = sum(IsLockedOrDisabled),
    UserList = make_set(UserPrincipalName, 25),
    ErrorCodes = make_set(ResultType),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by IPAddress, AppDisplayName, ResultDescription
| where UniqueUsernames >= enumThreshold
| extend 
    DurationMinutes = datetime_diff('minute', LastSeen, FirstSeen),
    EnumRate = round(todouble(UniqueUsernames) / iff(datetime_diff('minute', LastSeen, FirstSeen) == 0, 1, todouble(datetime_diff('minute', LastSeen, FirstSeen))), 2),
    SuspicionScore = case(
        UniqueUsernames >= 100, "Critical",
        UniqueUsernames >= 50 or UsernameNotFoundCount >= 30, "High",
        UniqueUsernames >= 15, "Medium",
        "Low"
    )
| project 
    DetectionTime = LastSeen,
    IPAddress,
    AppDisplayName,
    UniqueUsernames,
    TotalFailures,
    UsernameNotFoundCount,
    EnumRate,
    SuspicionScore,
    UserList,
    ErrorCodes,
    DurationMinutes
| order by UniqueUsernames desc;
// Secondary: SSPR abuse for identity enumeration
let SSPREnum = AuditLogs
| where TimeGenerated > ago(lookback)
| where OperationName in ("Reset password (self-service)", "Self-service password reset flow activity", "Verify email address phone number")
| where ResultReason contains "blocked" or ResultReason contains "failed" or ActivityDisplayName contains "verify"
| extend IPAddress = tostring(InitiatedBy.user.ipAddress)
| where isnotempty(IPAddress)
| summarize
    SSPRAttempts = count(),
    UniqueTargets = dcount(tostring(TargetResources[0].userPrincipalName)),
    TargetList = make_set(tostring(TargetResources[0].userPrincipalName), 25),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by IPAddress
| where UniqueTargets >= 10
| extend DetectionType = "SSPR_Enumeration", SuspicionScore = iff(UniqueTargets >= 25, "High", "Medium")
| project DetectionTime = LastSeen, IPAddress, UniqueTargets, SSPRAttempts, SuspicionScore, TargetList;
// Union both detection signals
AzureADEnum
| extend DetectionType = "Auth_Username_Enumeration"
| union (SSPREnum | extend UniqueUsernames = UniqueTargets, TotalFailures = SSPRAttempts, UserList = TargetList, UsernameNotFoundCount = 0, EnumRate = 0.0, ErrorCodes = dynamic(["SSPR"]), DurationMinutes = 0)
| order by SuspicionScore asc, UniqueUsernames desc

Detects adversary username enumeration via two vectors: (1) Azure AD sign-in attempts producing differential error codes (50034=UserNameDoesNotExist vs. password errors) from a single source IP across 15+ distinct usernames within an hour, and (2) Self-Service Password Reset flow abuse targeting multiple unique accounts. Combines both signals to surface identity reconnaissance activity consistent with LAPSUS$, Scattered Spider, and HEXANE TTPs.

high severity medium confidence

Data Sources

Azure Active Directory Microsoft Entra ID

Required Tables

AADSignInLogs AuditLogs

False Positives

  • Penetration testing engagements performing authorized username enumeration against Azure AD tenants
  • Misconfigured applications cycling through user lists for automated login (e.g., legacy SSO, misconfigured service accounts)
  • Employee self-service helpdesk tools that probe SSPR status for multiple users during bulk account operations
  • Password expiration notification systems contacting multiple accounts in rapid succession
  • IT onboarding scripts performing bulk account validation during directory synchronization

Sigma rule & cross-platform mapping

The detection logic for Gather Victim Identity Information (T1589) above is provided in a vendor-neutral form so you can deploy it on any SIEM. The same logic is shipped here as native KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the following logsource:

logsource:
  product: azure

Browse the community-maintained Sigma rules for this technique:


Testing Methodology

Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1Azure AD Username Enumeration via GetCredentialType API

    Expected signal: Azure AD Sign-in Logs (AADSignInLogs) will show ResultType=50034 (UserNameDoesNotExist) for non-existent accounts. Successful lookups may show ResultType=0 or MFA-related codes. Source IP will be the test machine's public IP. Check Azure AD portal under Monitoring > Sign-in Logs filtering by the test domain.

  2. Test 2On-Premises Active Directory Username Enumeration via Kerberos

    Expected signal: Windows Security Event ID 4625 with SubStatus 0xc0000064 (user does not exist) on domain controller for each non-existent username tested. Event ID 4625 with SubStatus 0xc000006a (wrong password) for valid usernames. Event ID 4771 with Status 0x6 on DCs running Kerberos logging. Check DC Security event logs filtering: EventID=4625 AND (SubStatus=0xc0000064 OR SubStatus=0xc0000072).

  3. Test 3SSPR Username Existence Probing via Azure AD Password Reset Flow

    Expected signal: Azure AD Audit Logs will contain SSPR-related entries under 'Self-service password reset flow activity' and 'Verify email address phone number'. Check Azure portal: Azure Active Directory > Monitoring > Audit Logs, filter Activity='Reset password (self-service)' or 'Self-service password management'. In Sentinel: AuditLogs | where OperationName contains 'password' | where TimeGenerated > ago(1h)


Response Playbook

Triage

  1. Step 1: Identify the source IP address triggering the alert. Run a geo-IP lookup (e.g., via Sentinel enrichment or ipinfo.io) — residential ISPs, VPN exit nodes, TOR exit nodes, and cloud hosting ASNs (AS14061=DigitalOcean, AS16509=AWS) are high-risk indicators of external reconnaissance.
  2. Step 2: Review the list of targeted usernames. Check whether they follow a naming convention pattern ([email protected], f.lastname, flastname) indicating the adversary has domain naming schema knowledge. Patterns suggest prior OSINT rather than random guessing.
  3. Step 3: Query AADSignInLogs for the source IP across a 7-day window: AADSignInLogs | where IPAddress == "<source_ip>" | summarize count(), dcount(UserPrincipalName) by bin(TimeGenerated, 1h). Determine if this is a one-time burst or sustained enumeration campaign.
  4. Step 4: Cross-reference targeted usernames against your HR/Active Directory roster. Determine the hit rate — what percentage of probed usernames actually exist in your directory? A high hit rate (>40%) suggests the adversary has prior identity intelligence (e.g., from LinkedIn, data breach data, or prior reconnaissance).
  5. Step 5: Check if any of the enumerated accounts have subsequently experienced successful logins, MFA push fatigue events, or password reset requests within 48 hours. This indicates progression from reconnaissance to access attempts.
  6. Step 6: Search OfficeActivity and CloudAppEvents for the same source IP: OfficeActivity | where ClientIP == "<source_ip>". Adversaries may pivot from auth probing to direct phishing or Business Email Compromise.
  7. Step 7: Assess whether the targeted accounts include privileged users (IT admins, executives, finance, HR). Prioritize investigation if VIP accounts were enumerated.
  8. Step 8: Check Azure AD Conditional Access logs for any bypass attempts — did the source IP attempt sign-ins from locations that would normally trigger policy enforcement?

Containment

  1. Block the source IP at the perimeter firewall and Azure AD Conditional Access Named Locations if confirmed malicious. Create a Named Location blocking policy scoped to the organization's tenant.
  2. Enable Azure AD Identity Protection risk policies if not already active: set 'Sign-in risk policy' to block HIGH risk sign-ins, and 'User risk policy' to require password change for HIGH risk users.
  3. If any enumerated accounts were successfully authenticated post-enumeration, immediately revoke all active sessions: Revoke-AzureADUserAllRefreshToken -ObjectId <userId> and force MFA re-registration.
  4. Temporarily restrict SSPR self-service to known networks via Conditional Access if SSPR enumeration was the attack vector. Alternatively, enable SSPR with CAPTCHA or rate limiting if available in your Entra configuration.
  5. Alert the accounts that were most aggressively targeted (top 10 by probe count) to be vigilant for follow-on phishing or vishing attempts.

Evidence Collection

  1. Export AADSignInLogs for the source IP and all probed UserPrincipalNames for the past 30 days. Include fields: TimeGenerated, IPAddress, UserPrincipalName, ResultType, ResultDescription, AppDisplayName, Location, DeviceDetail.
  2. Pull AuditLogs for SSPR activity if SSPR enumeration occurred: AuditLogs | where OperationName contains 'password' | where InitiatedBy.user.ipAddress == '<source_ip>'
  3. Capture the UserAgent strings from AADSignInLogs for the source IP — tools like o365enum, TrevorSpray, and Spray365 leave characteristic UserAgent fingerprints.
  4. Run threat intelligence lookups against the source IP using Microsoft Sentinel TI integration or external feeds (VirusTotal, Shodan). Document ASN, prior malicious activity history, and hosting provider.
  5. Preserve the full list of enumerated usernames in the incident ticket — this is intelligence about which identity data the adversary may already possess.
  6. If on-premises DCs were targeted, collect Security event logs (Event ID 4625, 4771, 4776) from all domain controllers for the attack window using wevtutil: wevtutil epl Security C:\evidence\security_dc01.evtx /q:"*[System[(EventID=4625) and TimeCreated[@SystemTime>='2026-03-19T00:00:00']]]"
  7. Document Conditional Access policy state at time of attack — screenshot Azure AD CA policy configuration to establish whether enumeration bypassed expected controls.

Escalation Criteria

  • ! Escalate immediately if any enumerated account subsequently shows a successful authentication from the same or geographically proximate IP — enumeration has progressed to access.
  • ! Escalate if privileged accounts (Global Admins, Security Admins, Exchange Admins, finance executives) appear in the enumerated username list.
  • ! Escalate if the enumeration hit rate exceeds 50% of probed usernames — this indicates the adversary possesses a pre-built identity list from a data breach or insider source.
  • ! Escalate if the attack pattern matches known threat actor TTPs: LAPSUS$ (telecom/tech targeting + SIM-swap correlation), Scattered Spider (helpdesk social engineering follow-up), or HEXANE (energy/telecom sector focus).
  • ! Escalate if SSPR enumeration is followed by MFA push notifications to targeted employees within 24 hours — this indicates active MFA fatigue attack in progress.
  • ! Escalate if the source IP or associated infrastructure appears in threat intelligence feeds as known C2, phishing infrastructure, or prior credential harvesting operations.

Investigation Guide

Forensic Artifacts

  • > Azure AD Sign-in Logs (AADSignInLogs): ResultType 50034 indicates username enumeration; ResultType distribution analysis reveals probe patterns
  • > Azure AD Audit Logs: SSPR flow events, MFA registration changes, and Conditional Access policy evaluation logs
  • > On-premises Security Event Log 4625 SubStatus 0xc0000064: 'User name does not exist' — primary Windows username enumeration indicator
  • > Kerberos Event 4771 with Status 0x6 (KDC_ERR_C_PRINCIPAL_UNKNOWN): username not found in Kerberos authentication
  • > NTLM Event 4776 with error code 0xC0000064: username enumeration via NTLM challenge-response
  • > Web application firewall (WAF) logs: high-frequency requests to /login, /api/auth, /forgot-password endpoints from single IPs
  • > Azure AD Identity Protection risk event logs: 'Atypical travel', 'Anonymous IP address', 'Malware linked IP address' signals correlated with enumeration source
  • > Network flow data: sustained TCP sessions to port 443 on login.microsoftonline.com, outlook.office365.com from anomalous sources
  • > DNS query logs: adversary infrastructure resolving Microsoft authentication endpoints immediately prior to enumeration bursts

Tuning Guidance

Start with a threshold of 15 unique usernames per IP per hour and tune upward if legitimate applications trigger alerts — common culprits include legacy SSO gateways, bulk email validation tools, and misconfigured LDAP clients. Add exclusions for known-good service account source IPs and internal IT management subnets. For SSPR-based detection, exclude SSPR traffic originating from the corporate SSPR management portal IP. Increase confidence to 'high' only after baselining — monitor 14 days of AADSignInLogs to establish normal failure rates per application and IP range before activating automated response. Consider separate tuning for internal (RFC1918) vs. external source IPs — internal sources producing 50034 errors may indicate misconfigured internal tooling rather than active reconnaissance. For on-premises AD environments, whitelist domain controller IPs in Windows Security event-based rules to suppress intra-domain replication and authentication traffic that produces benign 4625 events.


Hunting Queries

Hunts for MFA method enumeration (KQL) and explicit credential use across multiple targets post-enumeration (SPL), finding patterns distinct from the primary username-not-found detection.

Hunting — KQL
kql
// Hunt: MFA method enumeration - adversaries probing which MFA methods are registered
AADSignInLogs
| where TimeGenerated > ago(7d)
| where AuthenticationRequirement == "multiFactorAuthentication"
| where ResultType !in ("0", "50076")  // Exclude successful MFA and expected MFA challenges
| where ResultType in ("50074", "500121", "50097", "50131", "50155")
// 50074=StrongAuthRequired, 500121=AuthRequired, 50097=DeviceNotCompliant
| summarize
    MFAProbeCount = count(),
    UniqueUsers = dcount(UserPrincipalName),
    MFAErrorTypes = make_set(ResultType),
    UserList = make_set(UserPrincipalName, 20)
    by IPAddress, bin(TimeGenerated, 1h)
| where UniqueUsers >= 5
| extend HuntNote = "Potential MFA method enumeration — adversary mapping which accounts have MFA and what type"
| project TimeGenerated, IPAddress, UniqueUsers, MFAProbeCount, MFAErrorTypes, UserList, HuntNote
| order by UniqueUsers desc
Hunting — SPL
spl
index=* sourcetype="WinEventLog:Security" EventCode=4648
| eval TargetServer=coalesce(TargetServerName,"unknown")
| where TargetServer!="localhost" AND TargetServer!="127.0.0.1"
| stats
    count as ExplicitCredCount,
    dc(TargetUserName) as UniqueTargetUsers,
    dc(TargetServerName) as UniqueTargetServers,
    values(TargetUserName) as TargetUserList,
    values(TargetServerName) as TargetServerList
    by SubjectUserName, IpAddress
| where UniqueTargetUsers >= 5 OR UniqueTargetServers >= 5
| eval HuntNote="Explicit credential use (4648) across multiple targets - potential credential stuffing with enumerated identities"
| table SubjectUserName, IpAddress, ExplicitCredCount, UniqueTargetUsers, UniqueTargetServers, TargetUserList, TargetServerList, HuntNote
| sort - UniqueTargetUsers

Hunts for pattern-derived username probing that suggests the adversary reverse-engineered the corporate email naming convention (e.g., from LinkedIn data), measuring hit rates to assess prior OSINT depth.

Hunting — KQL
kql
// Hunt: Sudden spike in authentication attempts against accounts matching company naming convention
// Detects bulk testing of pattern-derived usernames (e.g., [email protected])
let domain = "@yourdomain.com";  // Parameterize for your tenant
let namingPatterns = dynamic([".", "-"]);  // Common separators in corporate email naming
AADSignInLogs
| where TimeGenerated > ago(24h)
| where UserPrincipalName endswith domain
| where ResultType != "0"  // Exclude successful logins
| extend Username = tostring(split(UserPrincipalName, "@")[0])
| extend HasSeparator = iff(Username has_any (namingPatterns), 1, 0)
| extend UsernameLength = strlen(Username)
| where HasSeparator == 1 and UsernameLength between (4 .. 30)
| summarize
    TotalAttempts = count(),
    UniqueUsernames = dcount(UserPrincipalName),
    SuccessCount = countif(ResultType == "0"),
    NotFoundCount = countif(ResultType == "50034"),
    IPList = make_set(IPAddress, 10)
    by bin(TimeGenerated, 1h)
| extend HitRate = round(todouble(UniqueUsernames - NotFoundCount) / todouble(UniqueUsernames) * 100, 1)
| where UniqueUsernames >= 20
| extend HuntNote = strcat("Pattern-based username probe: ", tostring(HitRate), "% of probed names exist — high hit rate indicates prior OSINT")
| project TimeGenerated, UniqueUsernames, TotalAttempts, NotFoundCount, HitRate, IPList, HuntNote
| order by HitRate desc
Hunting — SPL
spl
index=* sourcetype="WinEventLog:Security" EventCode=4625
| eval SubStatus=coalesce(SubStatus,"unknown")
| stats
    count as TotalFails,
    dc(TargetUserName) as UniqueUsers,
    values(TargetUserName) as UserList,
    dc(IpAddress) as UniqueSourceIPs
    by WorkstationName, _time span=1h
| where UniqueUsers >= 20 AND UniqueSourceIPs <= 3
| eval HuntNote="High unique username failures from few source IPs against single workstation - potential local enum or pass-the-hash pivot"
| table _time, WorkstationName, UniqueUsers, TotalFails, UniqueSourceIPs, UserList, HuntNote
| sort - UniqueUsers

Hunts for enumeration from cloud hosting/VPN ASNs (KQL) and statistically anomalous username-not-found spikes using z-score deviation from baseline (SPL), catching stealthy low-and-slow enumeration campaigns.

Hunting — KQL
kql
// Hunt: Geographically anomalous authentication probing correlated with known data breach timing
// Identifies IPs from high-risk ASNs (VPN/TOR/Proxy) probing accounts
let riskyASNKeywords = dynamic(["digitalocean", "linode", "vultr", "choopa", "ovh", "tor-exit", "mullvad", "nordvpn", "proton"]);
AADSignInLogs
| where TimeGenerated > ago(48h)
| where ResultType in ("50034", "50126", "50053")
| extend ASNLower = tolower(tostring(NetworkLocationDetails))
| where ASNLower has_any (riskyASNKeywords) or IPAddressFromResourceProvider == "true"
| summarize
    ProbeCount = count(),
    UniqueUsers = dcount(UserPrincipalName),
    UserSample = make_set(UserPrincipalName, 15),
    Countries = make_set(LocationDetails.countryOrRegion),
    CityList = make_set(LocationDetails.city)
    by IPAddress, ASNLower
| where UniqueUsers >= 5
| extend RiskFlag = "Cloud/VPN ASN conducting identity enumeration"
| project IPAddress, ASNLower, UniqueUsers, ProbeCount, Countries, CityList, UserSample, RiskFlag
| order by UniqueUsers desc
Hunting — SPL
spl
index=* sourcetype="WinEventLog:Security" EventCode IN (4625, 4771)
| eval AuthType=case(EventCode=4625, "NTLM_or_Kerberos", EventCode=4771, "Kerberos", "Other")
| eval FailReason=case(
    EventCode=4625 AND SubStatus="0xc0000064", "UserNotFound",
    EventCode=4771 AND Status="0x6", "KerberosUserUnknown",
    "OtherFailure"
  )
| where FailReason IN ("UserNotFound", "KerberosUserUnknown")
| bucket _time span=15m
| stats
    count as FailCount,
    dc(TargetUserName) as UniqueUsers
    by _time, IpAddress
| eventstats avg(FailCount) as AvgFails, stdev(FailCount) as StddevFails by IpAddress
| where FailCount > AvgFails + (3 * StddevFails) AND FailCount > 5
| eval HuntNote="Statistical anomaly: username-not-found failures 3+ standard deviations above baseline for this source IP"
| table _time, IpAddress, FailCount, UniqueUsers, AvgFails, StddevFails, HuntNote
| sort - FailCount

Atomic Red Team Tests

Test 1 Azure AD Username Enumeration via GetCredentialType API
linux

Simulates adversary username enumeration against Azure AD using the unauthenticated GetCredentialType endpoint, which returns differential responses indicating whether an account exists. This is the technique used by tools like o365enum and AADInternals.

Command

bash
# Requires: python3, requests library
# Replace TARGET_DOMAIN with your authorized test tenant domain
python3 << 'EOF'
import requests
import json
import time

target_domain = "TARGET_DOMAIN.onmicrosoft.com"  # Replace with authorized test domain
test_users = [
    f"validuser@{target_domain}",      # Should exist - replace with real test account
    f"definitelynotreal_xyz@{target_domain}",  # Should not exist
    f"anothertest@{target_domain}",    # Should not exist
    f"admin@{target_domain}"           # Test admin naming
]

url = "https://login.microsoftonline.com/common/GetCredentialType"
headers = {"Content-Type": "application/json"}

print(f"[*] Starting username enumeration against {target_domain}")
for username in test_users:
    payload = {"Username": username, "isOtherIdpSupported": True}
    try:
        resp = requests.post(url, json=payload, headers=headers, timeout=10)
        data = resp.json()
        # IfExistsResult: 0=Exists, 1=DoesNotExist, 5=ExistsWithDifferentIdP, 6=ExistsWithFederation
        exists_code = data.get("IfExistsResult", -1)
        exists_str = {0: "EXISTS", 1: "NOT_FOUND", 5: "EXISTS_DIFF_IDP", 6: "FEDERATED"}.get(exists_code, f"UNKNOWN({exists_code})")
        print(f"  [{exists_str}] {username}")
    except Exception as e:
        print(f"  [ERROR] {username}: {e}")
    time.sleep(0.5)  # Throttle to avoid rate limiting

print("[*] Enumeration complete")
EOF

Cleanup

bash
# No persistent changes made - API calls only
echo "Cleanup complete - no files to remove"

Expected Telemetry

Azure AD Sign-in Logs (AADSignInLogs) will show ResultType=50034 (UserNameDoesNotExist) for non-existent accounts. Successful lookups may show ResultType=0 or MFA-related codes. Source IP will be the test machine's public IP. Check Azure AD portal under Monitoring > Sign-in Logs filtering by the test domain.

Expected Detection

Alert should fire when 15+ distinct usernames are probed within 1 hour window. For this 4-account test, the alert may not trigger threshold — increase test_users list to 20+ accounts with mix of valid/invalid names to validate threshold detection.

Test 2 On-Premises Active Directory Username Enumeration via Kerberos
windows

Simulates Kerberos-based username enumeration against an on-premises domain controller using AS-REQ pre-authentication probing. Valid usernames return KDC_ERR_PREAUTH_REQUIRED (18), while invalid usernames return KDC_ERR_C_PRINCIPAL_UNKNOWN (6). Requires kerbrute or Rubeus on an authorized test machine.

Command

powershell
# Option A: Using kerbrute (Go binary, run from authorized attack machine)
# Download: https://github.com/ropnop/kerbrute/releases
# Replace DC_IP, DOMAIN, and userlist.txt with authorized test values

# Create test username list (mix of valid and invalid)
$testUsers = @(
    "validuser",
    "administrator",
    "jsmith",
    "notarealuserfake123",
    "anothernotreal456",
    "helpdesk",
    "svcaccount",
    "ghost789"
)
$testUsers | Out-File -FilePath C:\Temp\test_users.txt -Encoding ascii

# Run kerbrute enumeration (requires kerbrute.exe in current directory)
# .\kerbrute.exe userenum --dc DC_IP -d DOMAIN.LOCAL C:\Temp\test_users.txt

# Alternative: Native PowerShell Kerberos probe (no external tools)
$domain = $env:USERDOMAIN
$dc = (Resolve-DnsName "_kerberos._tcp.$domain" -Type SRV -ErrorAction SilentlyContinue).NameTarget | Select-Object -First 1
Write-Host "[*] Testing username enumeration against domain: $domain, DC: $dc"
foreach ($user in $testUsers) {
    try {
        $null = [System.DirectoryServices.DirectoryEntry]::new("LDAP://$dc", "$user@$domain", "WrongPassword123!")
    } catch [System.Runtime.InteropServices.COMException] {
        $errCode = $_.Exception.HResult
        # 0x8007052e = Invalid credentials (user exists), 0x80072030 = No such object (user not found)
        $status = if ($errCode -eq -2147023570) { "EXISTS (wrong password)" } elseif ($errCode -eq -2147016656) { "NOT_FOUND" } else { "OTHER: $errCode" }
        Write-Host "  [$status] $user"
    }
}

# Cleanup temp file
Remove-Item C:\Temp\test_users.txt -ErrorAction SilentlyContinue

Cleanup

powershell
Remove-Item C:\Temp\test_users.txt -ErrorAction SilentlyContinue
Write-Host "Cleanup complete"

Expected Telemetry

Windows Security Event ID 4625 with SubStatus 0xc0000064 (user does not exist) on domain controller for each non-existent username tested. Event ID 4625 with SubStatus 0xc000006a (wrong password) for valid usernames. Event ID 4771 with Status 0x6 on DCs running Kerberos logging. Check DC Security event logs filtering: EventID=4625 AND (SubStatus=0xc0000064 OR SubStatus=0xc0000072).

Expected Detection

Splunk detection should aggregate 4625/4771 failures by source IP and flag when UniqueUsers threshold (10) is exceeded. Validate by checking Splunk search: index=* sourcetype=WinEventLog:Security EventCode=4625 SubStatus=0xc0000064 | stats dc(TargetUserName) by IpAddress

Test 3 SSPR Username Existence Probing via Azure AD Password Reset Flow
linux

Simulates adversary use of the Self-Service Password Reset (SSPR) flow to enumerate whether accounts exist in a tenant without triggering standard authentication failures. The SSPR endpoint returns differential responses for valid vs. invalid accounts. This technique was used by Obsidian Security to document SSPR abuse for identity enumeration (2023).

Command

bash
# Simulates SSPR-based username enumeration
# IMPORTANT: Only run against authorized test tenant with written permission
# This contacts Microsoft's SSPR endpoint - requires internet connectivity

python3 << 'EOF'
import requests
import json
import time
import re

# Replace with your authorized test tenant domain
target_domain = "TARGET_DOMAIN.onmicrosoft.com"

# Test accounts - mix valid and invalid
test_accounts = [
    f"validtestuser@{target_domain}",   # Should exist
    f"ghostaccount_xyz123@{target_domain}",  # Should not exist
    f"anotherghost_abc456@{target_domain}",   # Should not exist
]

print(f"[*] SSPR username existence probe against {target_domain}")
print("[*] Note: Generating Azure AD Audit Log entries for each probe")

for account in test_accounts:
    # Step 1: Get tenant info to construct SSPR request
    tenant_url = f"https://login.microsoftonline.com/{target_domain}/.well-known/openid-configuration"
    tenant_resp = requests.get(tenant_url, timeout=10)
    
    if tenant_resp.status_code == 200:
        # Step 2: Probe SSPR endpoint
        sspr_url = "https://passwordreset.microsoftonline.com/usernamevalidation"
        headers = {
            "Content-Type": "application/json",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        }
        payload = {"Username": account}
        
        try:
            resp = requests.post(sspr_url, json=payload, headers=headers, timeout=10)
            # Differential response indicates user existence
            result = resp.json() if resp.content else {}
            print(f"  [HTTP {resp.status_code}] {account}: {json.dumps(result)}")
        except Exception as e:
            print(f"  [ERROR] {account}: {e}")
    
    time.sleep(1)  # Throttle between requests

print("[*] SSPR probe complete - check Azure AD Audit Logs for generated events")
EOF

Cleanup

bash
# API calls only - no persistent changes
echo "No cleanup required - only HTTP requests were made"

Expected Telemetry

Azure AD Audit Logs will contain SSPR-related entries under 'Self-service password reset flow activity' and 'Verify email address phone number'. Check Azure portal: Azure Active Directory > Monitoring > Audit Logs, filter Activity='Reset password (self-service)' or 'Self-service password management'. In Sentinel: AuditLogs | where OperationName contains 'password' | where TimeGenerated > ago(1h)

Expected Detection

KQL SSPR detection branch should fire if 10+ unique SSPR targets are probed. Single-account test validates telemetry generation. Expand to 15+ accounts to validate the SSPR detection threshold. Confirm that InitiatedBy.user.ipAddress in AuditLogs matches test machine's IP.

Related Detections