Potential Microsoft 365 User Account Brute Force

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Potential Microsoft 365 User Account Brute Force

edit

Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.

Rule type: esql

Rule indices: None

Severity: medium

Risk score: 47

Runs every: 10m

Searches indices from: now-60m (Date Math format, see also Additional look-back time)

Maximum alerts per execution: 100

References:

Tags:

  • Domain: Cloud
  • Domain: SaaS
  • Data Source: Microsoft 365
  • Data Source: Microsoft 365 Audit Logs
  • Use Case: Identity and Access Audit
  • Use Case: Threat Detection
  • Tactic: Credential Access
  • Resources: Investigation Guide

Version: 413

Rule authors:

  • Elastic
  • Willem D’Haese
  • Austin Songer

Rule license: Elastic License v2

Investigation guide

edit

Triage and Analysis

Investigating Potential Microsoft 365 User Account Brute Force

Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.

Possible investigation steps

  • Review user_id_list: Enumerates the user accounts targeted. Look for naming patterns or privilege levels (e.g., admins).
  • Check login_errors: A consistent error such as "InvalidUserNameOrPassword" confirms a spray-style attack using one or a few passwords.
  • Examine ip_list and source_orgs: Determine if the traffic originates from a known corporate VPN, datacenter, or suspicious ASN like hosting providers or anonymizers.
  • Review countries and unique_country_count: Geographic anomalies (e.g., login attempts from unexpected regions) may indicate malicious automation.
  • Validate total_attempts vs duration_seconds: A high frequency of login attempts over a short period may suggest automation rather than manual logins.
  • Cross-reference with successful logins: Pivot to surrounding sign-in logs (azure.signinlogs) or risk detections (identityprotection) for any account that eventually succeeded.
  • Check for multi-factor challenges or bypasses: Determine if any of the accounts were protected or if the attack bypassed MFA.

False positive analysis

  • IT administrators using automation tools (e.g., PowerShell) during account provisioning may trigger false positives if login attempts cluster.
  • Penetration testing or red team simulations may resemble spray activity.
  • Infrequent, low-volume login testing tools like ADFS testing scripts can exhibit similar patterns.

Response and remediation

  • Initiate an internal incident ticket and inform the affected identity/IT team.
  • Temporarily disable impacted user accounts if compromise is suspected.
  • Investigate whether any login attempts succeeded after the spray window.
  • Block the offending IPs or ASN temporarily via firewall or conditional access policies.
  • Rotate passwords for all targeted accounts and audit for password reuse.
  • Enforce or verify MFA is enabled for all user accounts.
  • Consider deploying account lockout or progressive delay mechanisms if not already enabled.

Rule query

edit
FROM logs-o365.audit-*

| MV_EXPAND event.category
| EVAL
    time_window = DATE_TRUNC(5 minutes, @timestamp),
    user_id = TO_LOWER(o365.audit.UserId),
    ip = source.ip,
    login_error = o365.audit.LogonError,
    request_type = TO_LOWER(o365.audit.ExtendedProperties.RequestType),
    asn_org = source.`as`.organization.name,
    country = source.geo.country_name,
    event_time = @timestamp

| WHERE event.dataset == "o365.audit"
  AND event.category == "authentication"
  AND event.provider IN ("AzureActiveDirectory", "Exchange")
  AND event.action IN ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword")
  AND request_type RLIKE "(oauth.*||.*login.*)"
  AND login_error != "IdsLocked"
  AND login_error NOT IN (
    "EntitlementGrantsNotFound", "UserStrongAuthEnrollmentRequired", "UserStrongAuthClientAuthNRequired",
    "InvalidReplyTo", "SsoArtifactExpiredDueToConditionalAccess", "PasswordResetRegistrationRequiredInterrupt",
    "SsoUserAccountNotFoundInResourceTenant", "UserStrongAuthExpired", "CmsiInterrupt"
  )
  AND user_id != "not available"
  AND o365.audit.Target.Type IN ("0", "2", "6", "10")

| STATS
    unique_users = COUNT_DISTINCT(user_id),
    user_id_list = VALUES(user_id),
    login_errors = VALUES(login_error),
    unique_login_errors = COUNT_DISTINCT(login_error),
    request_types = VALUES(request_type),
    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

| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    bf_type = CASE(
        unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 10 AND duration_seconds <= 1800, "password_spraying",
        unique_users >= 8 AND total_attempts >= 15 AND unique_login_errors <= 3 AND unique_ips <= 5 AND duration_seconds <= 600, "credential_stuffing",
        unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 20 AND duration_seconds <= 300, "password_guessing",
        "other"
    )

| KEEP
    time_window, unique_users, user_id_list, login_errors, unique_login_errors,
    request_types, ip_list, unique_ips, source_orgs, countries,
    unique_country_count, unique_asn_orgs, first_seen, last_seen,
    duration_seconds, total_attempts, bf_type

| WHERE
    bf_type != "other"

Framework: MITRE ATT&CKTM