diff --git a/rules/integrations/azure/credential_access_entra_id_excessive_account_lockouts.toml b/rules/integrations/azure/credential_access_entra_id_excessive_account_lockouts.toml new file mode 100644 index 000000000..c79731b94 --- /dev/null +++ b/rules/integrations/azure/credential_access_entra_id_excessive_account_lockouts.toml @@ -0,0 +1,192 @@ +[metadata] +creation_date = "2026/06/06" +integration = ["azure"] +maturity = "production" +min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above." +min_stack_version = "8.17.0" +updated_date = "2026/06/06" + +[rule] +author = ["Elastic"] +description = """ +Identifies a high count of failed Microsoft Entra ID sign-in attempts as the result of the target user account being +locked out. Adversaries may attempt to brute-force user accounts by repeatedly trying to authenticate with incorrect +credentials, leading to account lockouts by Entra ID Smart Lockout policies. +""" +false_positives = [ + """ + Automated processes that attempt to authenticate using expired credentials or have misconfigured authentication + settings may lead to false positives. + """, +] +from = "now-60m" +interval = "15m" +language = "esql" +license = "Elastic License v2" +name = "Microsoft Entra ID Exccessive Account Lockouts Detected" +note = """## Triage and analysis + +### Investigating Microsoft Entra ID Exccessive Account Lockouts Detected + +This rule detects a high number of sign-in failures due to account lockouts (error code `50053`) in Microsoft Entra ID sign-in logs. These lockouts are typically caused by repeated authentication failures, often as a result of brute-force tactics such as password spraying, credential stuffing, or automated guessing. This detection is time-bucketed and aggregates attempts to identify bursts or coordinated campaigns targeting multiple users. + +### Possible investigation steps + +- Review `user_id_list` and `user_principal_name`: Check if targeted users include high-value accounts such as administrators, service principals, or shared inboxes. +- Check `error_codes` and `result_description`: Validate that `50053` (account locked) is the consistent failure type. Messages indicating "malicious IP" activity suggest Microsoft’s backend flagged the source. +- Analyze `ip_list` and `source_orgs`: Identify whether the activity originated from known malicious infrastructure (e.g., VPNs, botnets, or public cloud providers). In the example, traffic originates from `MASSCOM`, which should be validated. +- Inspect `device_detail_browser` and `user_agent`: Clients like `"Python Requests"` indicate scripted automation rather than legitimate login attempts. +- Evaluate `unique_users` vs. `total_attempts`: A high ratio suggests distributed attacks across multiple accounts, characteristic of password spraying. +- Correlate `client_app_display_name` and `incoming_token_type`: PowerShell or unattended sign-in clients may be targeted for automation or legacy auth bypass. +- Review `conditional_access_status` and `risk_state`: If Conditional Access was not applied and risk was not flagged, policy scope or coverage should be reviewed. +- Validate time range (`first_seen`, `last_seen`): Determine whether the attack is a short burst or part of a longer campaign. + +### False positive analysis + +- Misconfigured clients, scripts, or services with outdated credentials may inadvertently cause lockouts. +- Repeated lockouts from known internal IPs or during credential rotation windows could be benign. +- Legacy applications without modern auth support may repeatedly fail and trigger Smart Lockout. +- Specific known user agents (e.g., corporate service accounts). +- Internal IPs or cloud-hosted automation with expected failure behavior. + +### Response and remediation + +- Investigate locked accounts immediately. Confirm if the account was successfully accessed prior to lockout. +- Reset credentials for impacted users and enforce MFA before re-enabling accounts. +- Block malicious IPs or ASN at the firewall, identity provider, or Conditional Access level. +- Audit authentication methods in use, and enforce modern auth (OAuth, SAML) over legacy protocols. +- Strengthen Conditional Access policies to reduce exposure from weak locations, apps, or clients. +- Conduct credential hygiene audits to assess reuse and rotation for targeted accounts. +""" +references = [ + "https://www.microsoft.com/en-us/security/blog/2025/05/27/new-russia-affiliated-actor-void-blizzard-targets-critical-sectors-for-espionage/", + "https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying", + "https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray", + "https://www.sprocketsecurity.com/blog/exploring-modern-password-spraying", + "https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties", + "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes", + "https://github.com/0xZDH/Omnispray", + "https://github.com/0xZDH/o365spray", +] +risk_score = 73 +rule_id = "2d6f5332-42ea-11f0-b09a-f661ea17fbcd" +severity = "high" +tags = [ + "Domain: Cloud", + "Domain: Identity", + "Data Source: Azure", + "Data Source: Entra ID", + "Data Source: Entra ID Sign-in Logs", + "Use Case: Identity and Access Audit", + "Use Case: Threat Detection", + "Tactic: Credential Access", + "Resources: Investigation Guide", +] +timestamp_override = "event.ingested" +type = "esql" + +query = ''' +FROM logs-azure.signinlogs* + +| EVAL + time_window = DATE_TRUNC(30 minutes, @timestamp), + user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name), + ip = source.ip, + login_error = azure.signinlogs.result_description, + error_code = azure.signinlogs.properties.status.error_code, + request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type), + app_name = TO_LOWER(azure.signinlogs.properties.app_display_name), + asn_org = source.`as`.organization.name, + country = source.geo.country_name, + user_agent = user_agent.original, + event_time = @timestamp + +| WHERE event.dataset == "azure.signinlogs" + AND event.category == "authentication" + AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs") + AND event.outcome == "failure" + AND azure.signinlogs.properties.authentication_requirement == "singleFactorAuthentication" + AND error_code == 50053 + AND user_id IS NOT NULL AND user_id != "" + AND asn_org != "MICROSOFT-CORP-MSN-AS-BLOCK" + +| STATS + authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement), + client_app_id = VALUES(azure.signinlogs.properties.app_id), + client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name), + target_resource_id = VALUES(azure.signinlogs.properties.resource_id), + target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name), + conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status), + device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser), + device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id), + device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system), + incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type), + risk_state = VALUES(azure.signinlogs.properties.risk_state), + session_id = VALUES(azure.signinlogs.properties.session_id), + user_id = VALUES(azure.signinlogs.properties.user_id), + user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name), + result_description = VALUES(azure.signinlogs.result_description), + result_signature = VALUES(azure.signinlogs.result_signature), + result_type = VALUES(azure.signinlogs.result_type), + + unique_users = COUNT_DISTINCT(user_id), + user_id_list = VALUES(user_id), + login_errors = VALUES(login_error), + unique_login_errors = COUNT_DISTINCT(login_error), + error_codes = VALUES(error_code), + unique_error_codes = COUNT_DISTINCT(error_code), + request_types = VALUES(request_type), + app_names = VALUES(app_name), + ip_list = VALUES(ip), + unique_ips = COUNT_DISTINCT(ip), + source_orgs = VALUES(asn_org), + countries = VALUES(country), + unique_country_count = COUNT_DISTINCT(country), + unique_asn_orgs = COUNT_DISTINCT(asn_org), + first_seen = MIN(event_time), + last_seen = MAX(event_time), + total_attempts = COUNT() +BY time_window +| WHERE unique_users >= 15 AND total_attempts >= 20 +| KEEP + time_window, total_attempts, first_seen, last_seen, + unique_users, user_id_list, login_errors, unique_login_errors, + unique_error_codes, error_codes, request_types, app_names, + ip_list, unique_ips, source_orgs, countries, + unique_country_count, unique_asn_orgs, + authentication_requirement, client_app_id, client_app_display_name, + target_resource_id, target_resource_display_name, conditional_access_status, + device_detail_browser, device_detail_device_id, device_detail_operating_system, + incoming_token_type, risk_state, session_id, user_id, + user_principal_name, result_description, result_signature, result_type +''' + + +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1110" +name = "Brute Force" +reference = "https://attack.mitre.org/techniques/T1110/" +[[rule.threat.technique.subtechnique]] +id = "T1110.001" +name = "Password Guessing" +reference = "https://attack.mitre.org/techniques/T1110/001/" + +[[rule.threat.technique.subtechnique]] +id = "T1110.003" +name = "Password Spraying" +reference = "https://attack.mitre.org/techniques/T1110/003/" + +[[rule.threat.technique.subtechnique]] +id = "T1110.004" +name = "Credential Stuffing" +reference = "https://attack.mitre.org/techniques/T1110/004/" + + + +[rule.threat.tactic] +id = "TA0006" +name = "Credential Access" +reference = "https://attack.mitre.org/tactics/TA0006/" +