|
| 1 | +[metadata] |
| 2 | +creation_date = "2026/06/06" |
| 3 | +integration = ["azure"] |
| 4 | +maturity = "production" |
| 5 | +min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above." |
| 6 | +min_stack_version = "8.17.0" |
| 7 | +updated_date = "2026/06/06" |
| 8 | + |
| 9 | +[rule] |
| 10 | +author = ["Elastic"] |
| 11 | +description = """ |
| 12 | +Identifies a high count of failed Microsoft Entra ID sign-in attempts as the result of the target user account being |
| 13 | +locked out. Adversaries may attempt to brute-force user accounts by repeatedly trying to authenticate with incorrect |
| 14 | +credentials, leading to account lockouts by Entra ID Smart Lockout policies. |
| 15 | +""" |
| 16 | +false_positives = [ |
| 17 | + """ |
| 18 | + Automated processes that attempt to authenticate using expired credentials or have misconfigured authentication |
| 19 | + settings may lead to false positives. |
| 20 | + """, |
| 21 | +] |
| 22 | +from = "now-60m" |
| 23 | +interval = "15m" |
| 24 | +language = "esql" |
| 25 | +license = "Elastic License v2" |
| 26 | +name = "Microsoft Entra ID Exccessive Account Lockouts Detected" |
| 27 | +note = """## Triage and analysis |
| 28 | +
|
| 29 | +### Investigating Microsoft Entra ID Exccessive Account Lockouts Detected |
| 30 | +
|
| 31 | +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. |
| 32 | +
|
| 33 | +### Possible investigation steps |
| 34 | +
|
| 35 | +- Review `user_id_list` and `user_principal_name`: Check if targeted users include high-value accounts such as administrators, service principals, or shared inboxes. |
| 36 | +- 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. |
| 37 | +- 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. |
| 38 | +- Inspect `device_detail_browser` and `user_agent`: Clients like `"Python Requests"` indicate scripted automation rather than legitimate login attempts. |
| 39 | +- Evaluate `unique_users` vs. `total_attempts`: A high ratio suggests distributed attacks across multiple accounts, characteristic of password spraying. |
| 40 | +- Correlate `client_app_display_name` and `incoming_token_type`: PowerShell or unattended sign-in clients may be targeted for automation or legacy auth bypass. |
| 41 | +- 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. |
| 42 | +- Validate time range (`first_seen`, `last_seen`): Determine whether the attack is a short burst or part of a longer campaign. |
| 43 | +
|
| 44 | +### False positive analysis |
| 45 | +
|
| 46 | +- Misconfigured clients, scripts, or services with outdated credentials may inadvertently cause lockouts. |
| 47 | +- Repeated lockouts from known internal IPs or during credential rotation windows could be benign. |
| 48 | +- Legacy applications without modern auth support may repeatedly fail and trigger Smart Lockout. |
| 49 | +- Specific known user agents (e.g., corporate service accounts). |
| 50 | +- Internal IPs or cloud-hosted automation with expected failure behavior. |
| 51 | +
|
| 52 | +### Response and remediation |
| 53 | +
|
| 54 | +- Investigate locked accounts immediately. Confirm if the account was successfully accessed prior to lockout. |
| 55 | +- Reset credentials for impacted users and enforce MFA before re-enabling accounts. |
| 56 | +- Block malicious IPs or ASN at the firewall, identity provider, or Conditional Access level. |
| 57 | +- Audit authentication methods in use, and enforce modern auth (OAuth, SAML) over legacy protocols. |
| 58 | +- Strengthen Conditional Access policies to reduce exposure from weak locations, apps, or clients. |
| 59 | +- Conduct credential hygiene audits to assess reuse and rotation for targeted accounts. |
| 60 | +""" |
| 61 | +references = [ |
| 62 | + "https://www.microsoft.com/en-us/security/blog/2025/05/27/new-russia-affiliated-actor-void-blizzard-targets-critical-sectors-for-espionage/", |
| 63 | + "https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying", |
| 64 | + "https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray", |
| 65 | + "https://www.sprocketsecurity.com/blog/exploring-modern-password-spraying", |
| 66 | + "https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties", |
| 67 | + "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes", |
| 68 | + "https://github.com/0xZDH/Omnispray", |
| 69 | + "https://github.com/0xZDH/o365spray", |
| 70 | +] |
| 71 | +risk_score = 73 |
| 72 | +rule_id = "2d6f5332-42ea-11f0-b09a-f661ea17fbcd" |
| 73 | +severity = "high" |
| 74 | +tags = [ |
| 75 | + "Domain: Cloud", |
| 76 | + "Domain: Identity", |
| 77 | + "Data Source: Azure", |
| 78 | + "Data Source: Entra ID", |
| 79 | + "Data Source: Entra ID Sign-in Logs", |
| 80 | + "Use Case: Identity and Access Audit", |
| 81 | + "Use Case: Threat Detection", |
| 82 | + "Tactic: Credential Access", |
| 83 | + "Resources: Investigation Guide", |
| 84 | +] |
| 85 | +timestamp_override = "event.ingested" |
| 86 | +type = "esql" |
| 87 | + |
| 88 | +query = ''' |
| 89 | +FROM logs-azure.signinlogs* |
| 90 | +
|
| 91 | +| EVAL |
| 92 | + time_window = DATE_TRUNC(30 minutes, @timestamp), |
| 93 | + user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name), |
| 94 | + ip = source.ip, |
| 95 | + login_error = azure.signinlogs.result_description, |
| 96 | + error_code = azure.signinlogs.properties.status.error_code, |
| 97 | + request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type), |
| 98 | + app_name = TO_LOWER(azure.signinlogs.properties.app_display_name), |
| 99 | + asn_org = source.`as`.organization.name, |
| 100 | + country = source.geo.country_name, |
| 101 | + user_agent = user_agent.original, |
| 102 | + event_time = @timestamp |
| 103 | +
|
| 104 | +| WHERE event.dataset == "azure.signinlogs" |
| 105 | + AND event.category == "authentication" |
| 106 | + AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs") |
| 107 | + AND event.outcome == "failure" |
| 108 | + AND azure.signinlogs.properties.authentication_requirement == "singleFactorAuthentication" |
| 109 | + AND error_code == 50053 |
| 110 | + AND user_id IS NOT NULL AND user_id != "" |
| 111 | + AND asn_org != "MICROSOFT-CORP-MSN-AS-BLOCK" |
| 112 | +
|
| 113 | +| STATS |
| 114 | + authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement), |
| 115 | + client_app_id = VALUES(azure.signinlogs.properties.app_id), |
| 116 | + client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name), |
| 117 | + target_resource_id = VALUES(azure.signinlogs.properties.resource_id), |
| 118 | + target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name), |
| 119 | + conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status), |
| 120 | + device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser), |
| 121 | + device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id), |
| 122 | + device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system), |
| 123 | + incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type), |
| 124 | + risk_state = VALUES(azure.signinlogs.properties.risk_state), |
| 125 | + session_id = VALUES(azure.signinlogs.properties.session_id), |
| 126 | + user_id = VALUES(azure.signinlogs.properties.user_id), |
| 127 | + user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name), |
| 128 | + result_description = VALUES(azure.signinlogs.result_description), |
| 129 | + result_signature = VALUES(azure.signinlogs.result_signature), |
| 130 | + result_type = VALUES(azure.signinlogs.result_type), |
| 131 | +
|
| 132 | + unique_users = COUNT_DISTINCT(user_id), |
| 133 | + user_id_list = VALUES(user_id), |
| 134 | + login_errors = VALUES(login_error), |
| 135 | + unique_login_errors = COUNT_DISTINCT(login_error), |
| 136 | + error_codes = VALUES(error_code), |
| 137 | + unique_error_codes = COUNT_DISTINCT(error_code), |
| 138 | + request_types = VALUES(request_type), |
| 139 | + app_names = VALUES(app_name), |
| 140 | + ip_list = VALUES(ip), |
| 141 | + unique_ips = COUNT_DISTINCT(ip), |
| 142 | + source_orgs = VALUES(asn_org), |
| 143 | + countries = VALUES(country), |
| 144 | + unique_country_count = COUNT_DISTINCT(country), |
| 145 | + unique_asn_orgs = COUNT_DISTINCT(asn_org), |
| 146 | + first_seen = MIN(event_time), |
| 147 | + last_seen = MAX(event_time), |
| 148 | + total_attempts = COUNT() |
| 149 | +BY time_window |
| 150 | +| WHERE unique_users >= 15 AND total_attempts >= 20 |
| 151 | +| KEEP |
| 152 | + time_window, total_attempts, first_seen, last_seen, |
| 153 | + unique_users, user_id_list, login_errors, unique_login_errors, |
| 154 | + unique_error_codes, error_codes, request_types, app_names, |
| 155 | + ip_list, unique_ips, source_orgs, countries, |
| 156 | + unique_country_count, unique_asn_orgs, |
| 157 | + authentication_requirement, client_app_id, client_app_display_name, |
| 158 | + target_resource_id, target_resource_display_name, conditional_access_status, |
| 159 | + device_detail_browser, device_detail_device_id, device_detail_operating_system, |
| 160 | + incoming_token_type, risk_state, session_id, user_id, |
| 161 | + user_principal_name, result_description, result_signature, result_type |
| 162 | +''' |
| 163 | + |
| 164 | + |
| 165 | +[[rule.threat]] |
| 166 | +framework = "MITRE ATT&CK" |
| 167 | +[[rule.threat.technique]] |
| 168 | +id = "T1110" |
| 169 | +name = "Brute Force" |
| 170 | +reference = "https://attack.mitre.org/techniques/T1110/" |
| 171 | +[[rule.threat.technique.subtechnique]] |
| 172 | +id = "T1110.001" |
| 173 | +name = "Password Guessing" |
| 174 | +reference = "https://attack.mitre.org/techniques/T1110/001/" |
| 175 | + |
| 176 | +[[rule.threat.technique.subtechnique]] |
| 177 | +id = "T1110.003" |
| 178 | +name = "Password Spraying" |
| 179 | +reference = "https://attack.mitre.org/techniques/T1110/003/" |
| 180 | + |
| 181 | +[[rule.threat.technique.subtechnique]] |
| 182 | +id = "T1110.004" |
| 183 | +name = "Credential Stuffing" |
| 184 | +reference = "https://attack.mitre.org/techniques/T1110/004/" |
| 185 | + |
| 186 | + |
| 187 | + |
| 188 | +[rule.threat.tactic] |
| 189 | +id = "TA0006" |
| 190 | +name = "Credential Access" |
| 191 | +reference = "https://attack.mitre.org/tactics/TA0006/" |
| 192 | + |
0 commit comments