{
  "technique_id": "T1110",
  "name": "Brute Force",
  "priority": "high",
  "status": "complete",
  "version": "0.1.0",
  "last_reviewed": "2026-07-23",
  "generated_by": "SOC Response Atlas by Basyrix",
  "tactics": [
    "Credential Access"
  ],
  "platforms": [
    "Windows",
    "Linux",
    "macOS",
    "Azure AD",
    "Office 365",
    "SaaS"
  ],
  "summary": "Adversaries may use brute force techniques — password guessing, cracking, spraying across many accounts, or credential stuffing from breached credential lists — to gain access to accounts, particularly where lockout policies are lenient, MFA is not enforced, or legacy authentication protocols are still reachable.\n",
  "soc_recommendation": "Correlate authentication failure volume and pattern (single account / many sources vs. many accounts / single or few sources) with any subsequent successful sign-in. Treat a success that follows a brute force or spray pattern as likely compromise until proven otherwise, regardless of whether the account itself looks privileged.\n",
  "d3fend_mappings": [
    {
      "id": "D3-CAA",
      "name": "Connection Attempt Analysis",
      "relationship": "detect",
      "practical_action": "Review failed sign-in bursts, password spray patterns across many accounts from a small set of sources, and any success immediately following a failure streak.\n",
      "tooling": [
        "Sentinel",
        "Entra ID",
        "Defender XDR"
      ]
    },
    {
      "id": "D3-SPP",
      "name": "Strong Password Policy",
      "relationship": "harden",
      "practical_action": "Confirm lockout thresholds, smart lockout, and banned-password lists are enforced; tighten where legacy or service accounts are exempt.\n",
      "tooling": [
        "Entra ID",
        "Group Policy"
      ]
    },
    {
      "id": "D3-MFA",
      "name": "Multi-factor Authentication",
      "relationship": "harden",
      "practical_action": "Confirm MFA is enforced for the affected account and that legacy authentication protocols (which bypass MFA) are blocked.\n",
      "tooling": [
        "Entra ID Conditional Access"
      ]
    },
    {
      "id": "D3-CR",
      "name": "Credential Revocation",
      "relationship": "evict",
      "practical_action": "Revoke sessions and refresh tokens if a spray attempt succeeded.",
      "tooling": [
        "Microsoft Graph",
        "Entra ID"
      ]
    }
  ],
  "investigation_steps": {
    "microsoft": [
      "Review Entra ID SigninLogs for the affected account(s) and source IPs over the last 24-48 hours.",
      "Check Identity Protection risky sign-ins and risky users for spray/brute-force risk detections.",
      "Review whether legacy authentication (IMAP/POP/SMTP AUTH) was used to bypass MFA.",
      "Check Conditional Access sign-in diagnostics for any policy that was not applied or was misconfigured.",
      "Confirm smart lockout / custom lockout thresholds are active for the tenant."
    ],
    "generic": [
      "Count failed authentication attempts per account and per source over a rolling window.",
      "Identify whether the pattern is single-account/many-source (brute force) or many-account/few-source (spray).",
      "Check whether any attempt succeeded and, if so, what happened immediately after.",
      "Review whether the source IP or ASN is associated with known scanning/attack infrastructure.",
      "Real detection reference: \"API - Account Takeover\" -- 42Crunch API protection against account takeover'"
    ]
  },
  "evidence_to_collect": [
    "Target username(s)",
    "Source IP(s) and ASN",
    "Authentication protocol used",
    "Failure count and time window",
    "Any successful sign-in following failures",
    "MFA challenge result, if any",
    "User agent / client app"
  ],
  "response_actions": [
    {
      "name": "Block attacking IPs in Conditional Access (Named Locations →",
      "category": "Response",
      "risk": "Medium",
      "automation_safe": false,
      "approval_required": true,
      "tool": "See investigation guide",
      "notes": "Block attacking IPs in Conditional Access (Named Locations → Block)"
    },
    {
      "name": "Reset passwords for all accounts successfully authenticated",
      "category": "Response",
      "risk": "Medium",
      "automation_safe": false,
      "approval_required": true,
      "tool": "See investigation guide",
      "notes": "Reset passwords for all accounts successfully authenticated from those IPs"
    },
    {
      "name": "Force MFA for targeted accounts if not already enforced",
      "category": "Response",
      "risk": "Low",
      "automation_safe": false,
      "approval_required": true,
      "tool": "See investigation guide"
    },
    {
      "name": "Alert customer — provide list of targeted and compromised ac",
      "category": "Response",
      "risk": "Low",
      "automation_safe": false,
      "approval_required": true,
      "tool": "See investigation guide",
      "notes": "Alert customer — provide list of targeted and compromised accounts"
    }
  ],
  "queries": {
    "kql": [
      {
        "name": "25c86f99-0a91-4b7f-88f3-599a008e5ab8 — API - Account Takeover",
        "description": "42Crunch API protection against account takeover' (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed).)",
        "query": "let firstRec = FortyTwoCrunchAPIProtection\n| where TimeGenerated >= ago(5m) \n| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid\n| where (UriPath has \"/api/register\" or UriPath has \"/api/reset\") and Status == 403;\nlet recCount = iff((toscalar(firstRec | count) >= 20), 1, 0);\nfirstRec | take recCount"
      },
      {
        "name": "d951d64d-0ecd-4675-8c79-6c870d5f72ac — API - Password Cracking",
        "description": "42Crunch API protection against password cracking' (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed).)",
        "query": "let loginRec = FortyTwoCrunchAPIProtection\n| where TimeGenerated >= ago(5m) \n| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid\n| where UriPath has \"/api/login?user=\" and Status == 403;\nlet recCount = iff((toscalar(loginRec | count) > 10), 1, 0);\nloginRec | take recCount"
      },
      {
        "name": "7bdc10d6-aa24-4ca9-9a93-802cd8761354 — API - Suspicious Login",
        "description": "42Crunch API protection against suspicious login' (Source: Microsoft's official Azure-Sentinel Detections (MIT licensed).)",
        "query": "let firstRec = FortyTwoCrunchAPIProtection\n| where TimeGenerated >= ago(5m) \n| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid\n| where UriPath has \"/api/login?user=\" and Status in (200, 403);\nlet ipAddressCount = toscalar(firstRec | summarize by SourceIp | count);\nlet secondRec = firstRec | summarize arg_max(TimeGenerated, *) by SourceIp;\nlet recCount = iff((toscalar(secondRec | count) > 3), ipAddressCount, 0);\nsecondRec | take recCount"
      }
    ],
    "spl": [
      {
        "name": "Authentication failure volume by source",
        "description": "Review authentication failure volume in Splunk.",
        "query": "index=authentication action=failure earliest=-24h\n| stats count by src_ip, user\n| where count > 20\n"
      }
    ],
    "esql": [
      {
        "name": "50742e15-c5ef-49c8-9a2d-31221d45af58 — Okta Successful Login After Credential Attack",
        "description": "(ESQL) Correlates Okta credential attack alerts with subsequent successful authentication for the same user account, identifying potential compromise following brute force, password spray, or credential stuffing attempts. (Source: Elastic's official detection-rules repository (Elastic License v2).)",
        "query": "FROM .alerts-security.*, logs-okta.system-* METADATA _id, _version, _index\n// Filter for credential attack alerts OR successful Okta authentications\n| WHERE\n    (\n        // Credential attack alerts from the five correlated rules\n        kibana.alert.rule.rule_id IN (\n            \"94e734c0-2cda-11ef-84e1-f661ea17fbce\",  // Credential Stuffing\n            \"42bf698b-4738-445b-8231-c834ddefd8a0\",  // Password Spraying\n            \"23f18264-2d6d-11ef-9413-f661ea17fbce\",  // DT Brute Force\n            \"5889760c-9858-4b4b-879c-e299df493295\",  // Distributed Brute Force\n            \"2d3c27d5-d133-4152-8102-8d051619ec4a\"   // Distributed Spray\n        )\n    )\n    OR (\n        // Successful Okta authentication events\n        data_stream.dataset == \"okta.system\"\n        AND (event.action LIKE \"user.authentication.*\" OR event.action == \"user.session.start\")\n        AND okta.outcome.result == \"SUCCESS\"\n        AND okta.actor.alternate_id IS NOT NULL\n    )\n// correlation - alerts may store user/IP in different fields than raw logs\n| EVAL\n    Esql.user = COALESCE(okta.actor.alternate_id, user.name, user.email),\n    Esql.source_ip = COALESCE(okta.client.ip, client.ip, source.ip)\n// Must have user identity to correlate\n| WHERE Esql.user IS NOT NULL\n// Classify events and capture timestamps/IPs by event type\n| EVAL\n    Esql.is_attack_alert = CASE(\n        kibana.alert.rule.rule_id IN (\n            \"94e734c0-2cda-11ef-84e1-f661ea17fbce\",\n            \"42bf698b-4738-445b-8231-c834ddefd8a0\",\n            \"23f18264-2d6d-11ef-9413-f661ea17fbce\",\n            \"5889760c-9858-4b4b-879c-e299df493295\",\n            \"2d3c27d5-d133-4152-8102-8d051619ec4a\"\n        ), 1, 0\n    ),\n    Esql.is_success_login = CASE(\n        data_stream.dataset == \"okta.system\"\n        AND okta.outcome.result == \"SUCCESS\", 1, 0\n    ),\n    Esql.attack_ip = CASE(\n        kibana.alert.rule.rule_id IN (\n            \"94e734c0-2cda-11ef-84e1-f661ea17fbce\",\n            \"42bf698b-4738-445b-8231-c834ddefd8a0\",\n            \"23f18264-2d6d-11ef-9413-f661ea17fbce\",\n            \"5889760c-9858-4b4b-879c-e299df493295\",\n            \"2d3c27d5-d133-4152-8102-8d051619ec4a\"\n        ), Esql.source_ip, null\n    ),\n    Esql.login_ip = CASE(\n        data_stream.dataset == \"okta.system\"\n        AND okta.outcome.result == \"SUCCESS\", Esql.source_ip, null\n    ),\n    Esql.attack_ts = CASE(\n        kibana.alert.rule.rule_id IN (\n            \"94e734c0-2cda-11ef-84e1-f661ea17fbce\",\n            \"42bf698b-4738-445b-8231-c834ddefd8a0\",\n            \"23f18264-2d6d-11ef-9413-f661ea17fbce\",\n            \"5889760c-9858-4b4b-879c-e299df493295\",\n            \"2d3c27d5-d133-4152-8102-8d051619ec4a\"\n        ), @timestamp, null\n    ),\n    Esql.login_ts = CASE(\n        data_stream.dataset == \"okta.system\"\n        AND okta.outcome.result == \"SUCCESS\", @timestamp, null\n    )\n// Aggregate by user (catches IP rotation: spray from IP A, login from IP B)\n| STATS\n    Esql.attack_count = SUM(Esql.is_attack_alert),\n    Esql.login_count = SUM(Esql.is_success_login),\n    Esql.earliest_attack = MIN(Esql.attack_ts),\n    Esql.latest_attack = MAX(Esql.attack_ts),\n    Esql.earliest_login = MIN(Esql.login_ts),\n    Esql.latest_login = MAX(Esql.login_ts),\n    Esql.attack_source_ips = VALUES(Esql.attack_ip),\n    Esql.login_source_ips = VALUES(Esql.login_ip),\n    Esql.all_source_ips = VALUES(Esql.source_ip),\n    Esql.alert_rule_ids = VALUES(kibana.alert.rule.rule_id),\n    Esql.alert_rule_names = VALUES(kibana.alert.rule.name),\n    Esql.event_action_values = VALUES(event.action),\n    Esql.geo_country_values = VALUES(client.geo.country_name),\n    Esql.geo_city_values = VALUES(client.geo.city_name),\n    Esql.source_asn_values = VALUES(source.as.number),\n    Esql.source_asn_org_values = VALUES(source.as.organization.name),\n    Esql.user_agent_values = VALUES(okta.client.user_agent.raw_user_agent),\n    Esql.device_values = VALUES(okta.client.device),\n    Esql.is_proxy_values = VALUES(okta.security_context.is_proxy)\n  BY Esql.user\n// Calculate time gap between latest attack and earliest subsequent login\n| EVAL Esql.attack_to_login_minutes = DATE_DIFF(\"minute\", Esql.latest_attack, Esql.earliest_login)\n// Correlation: attack BEFORE login + success within reasonable window (3 hours)\n| WHERE\n    Esql.attack_count > 0\n    AND Esql.login_count > 0\n    AND Esql.latest_attack < Esql.earliest_login\n    AND Esql.attack_to_login_minutes <= 180\n| SORT Esql.login_count DESC\n| KEEP Esql.*"
      },
      {
        "name": "cca64114-fb8b-11ef-86e2-f661ea17fbce — Entra ID User Sign-in Brute Force Attempted",
        "description": "(ESQL) Identifies potential brute-force attacks targeting user accounts by analyzing failed sign-in patterns in Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or password guessing... (Source: Elastic's official detection-rules repository (Elastic License v2).)",
        "query": "from logs-azure.signinlogs-*\n\n// Define a time window for grouping and maintain the original event timestamp\n| eval Esql.time_window_date_trunc = date_trunc(15 minutes, @timestamp)\n\n// Filter relevant failed authentication events with specific error codes\n| where data_stream.dataset == \"azure.signinlogs\"\n    and event.category == \"authentication\"\n    and azure.signinlogs.category in (\"NonInteractiveUserSignInLogs\", \"SignInLogs\")\n    and event.outcome == \"failure\"\n    and azure.signinlogs.properties.authentication_requirement == \"singleFactorAuthentication\"\n    and azure.signinlogs.properties.status.error_code in (\n        50034,  // UserAccountNotFound\n        50126,  // InvalidUsernameOrPassword\n        50055,  // PasswordExpired\n        50056,  // InvalidPassword\n        50057,  // UserDisabled\n        50064,  // CredentialValidationFailure\n        50076,  // MFARequiredButNotPassed\n        50079,  // MFARegistrationRequired\n        50105,  // EntitlementGrantsNotFound\n        70000,  // InvalidGrant\n        70008,  // ExpiredOrRevokedRefreshToken\n        70043,  // BadTokenDueToSignInFrequency\n        80002,  // OnPremisePasswordValidatorRequestTimedOut\n        80005,  // OnPremisePasswordValidatorUnpredictableWebException\n        50144,  // InvalidPasswordExpiredOnPremPassword\n        50135,  // PasswordChangeCompromisedPassword\n        50142,  // PasswordChangeRequiredConditionalAccess\n        120000, // PasswordChangeIncorrectCurrentPassword\n        120002, // PasswordChangeInvalidNewPasswordWeak\n        120020  // PasswordChangeFailure\n    )\n    and azure.signinlogs.properties.user_principal_name is not null and azure.signinlogs.properties.user_principal_name != \"\"\n    and user_agent.original != \"Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0\"\n    and source.`as`.organization.name != \"MICROSOFT-CORP-MSN-as-BLOCK\"\n\n| stats\n    Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),\n    Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),\n    Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name),\n    Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),\n    Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),\n    Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),\n    Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser),\n    Esql.azure_signinlogs_properties_device_detail_device_id_values = values(azure.signinlogs.properties.device_detail.device_id),\n    Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system),\n    Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type),\n    Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),\n    Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id),\n    Esql.azure_signinlogs_properties_user_id_values = values(azure.signinlogs.properties.user_id),\n    Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),\n    Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description),\n    Esql.azure_signinlogs_result_signature_values = values(azure.signinlogs.result_signature),\n    Esql.azure_signinlogs_result_type_values = values(azure.signinlogs.result_type),\n\n    Esql.azure_signinlogs_properties_user_id_count_distinct = count_distinct(azure.signinlogs.properties.user_id),\n    Esql.azure_signinlogs_properties_user_id_list = values(azure.signinlogs.properties.user_id),\n    Esql.azure_signinlogs_result_description_values_all = values(azure.signinlogs.result_description),\n    Esql.azure_signinlogs_result_description_count_distinct = count_distinct(azure.signinlogs.result_description),\n    Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),\n    Esql.azure_signinlogs_properties_status_error_code_count_distinct = count_distinct(azure.signinlogs.properties.status.error_code),\n    Esql.azure_signinlogs_properties_incoming_token_type_values_all = values(azure.signinlogs.properties.incoming_token_type),\n    Esql.azure_signinlogs_properties_app_display_name_values_all = values(azure.signinlogs.properties.app_display_name),\n    Esql.source_ip_values = values(source.ip),\n    Esql.source_ip_count_distinct = count_distinct(source.ip),\n    Esql.source_as_organization_name_values = values(source.`as`.organization.name),\n    Esql.source_geo_country_name_values = values(source.geo.country_name),\n    Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name),\n    Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name),\n    Esql.timestamp_first_seen = min(@timestamp),\n    Esql.timestamp_last_seen = max(@timestamp),\n    Esql.event_count = count()\nby Esql.time_window_date_trunc\n\n| eval\n    Esql.duration_seconds = date_diff(\"seconds\", Esql.timestamp_first_seen, Esql.timestamp_last_seen),\n    Esql.brute_force_type = case(\n        Esql.azure_signinlogs_properties_user_id_count_distinct >= 10 and Esql.event_count >= 30 and Esql.azure_signinlogs_result_description_count_distinct <= 3\n            and Esql.source_ip_count_distinct >= 5\n            and Esql.duration_seconds <= 600\n            and Esql.azure_signinlogs_properties_user_id_count_distinct > Esql.source_ip_count_distinct,\n        \"credential_stuffing\",\n\n        Esql.azure_signinlogs_properties_user_id_count_distinct >= 15 and Esql.azure_signinlogs_result_description_count_distinct == 1 and Esql.event_count >= 15 and Esql.duration_seconds <= 1800,\n        \"password_spraying\",\n\n        (Esql.azure_signinlogs_properties_user_id_count_distinct == 1 and Esql.azure_signinlogs_result_description_count_distinct == 1 and Esql.event_count >= 30 and Esql.duration_seconds <= 300)\n            or (Esql.azure_signinlogs_properties_user_id_count_distinct <= 3 and Esql.source_ip_count_distinct > 30 and Esql.event_count >= 100),\n        \"password_guessing\",\n\n        \"other\"\n    )\n\n| keep\n    Esql.time_window_date_trunc,\n    Esql.brute_force_type,\n    Esql.duration_seconds,\n    Esql.event_count,\n    Esql.timestamp_first_seen,\n    Esql.timestamp_last_seen,\n    Esql.azure_signinlogs_properties_user_id_count_distinct,\n    Esql.azure_signinlogs_properties_user_id_list,\n    Esql.azure_signinlogs_result_description_values_all,\n    Esql.azure_signinlogs_result_description_count_distinct,\n    Esql.azure_signinlogs_properties_status_error_code_count_distinct,\n    Esql.azure_signinlogs_properties_status_error_code_values,\n    Esql.azure_signinlogs_properties_incoming_token_type_values_all,\n    Esql.azure_signinlogs_properties_app_display_name_values_all,\n    Esql.source_ip_values,\n    Esql.source_ip_count_distinct,\n    Esql.source_as_organization_name_values,\n    Esql.source_geo_country_name_values,\n    Esql.source_geo_country_name_count_distinct,\n    Esql.source_as_organization_name_count_distinct,\n    Esql.azure_signinlogs_properties_authentication_requirement_values,\n    Esql.azure_signinlogs_properties_app_id_values,\n    Esql.azure_signinlogs_properties_app_display_name_values,\n    Esql.azure_signinlogs_properties_resource_id_values,\n    Esql.azure_signinlogs_properties_resource_display_name_values,\n    Esql.azure_signinlogs_properties_conditional_access_status_values,\n    Esql.azure_signinlogs_properties_device_detail_browser_values,\n    Esql.azure_signinlogs_properties_device_detail_device_id_values,\n    Esql.azure_signinlogs_properties_device_detail_operating_system_values,\n    Esql.azure_signinlogs_properties_incoming_token_type_values,\n    Esql.azure_signinlogs_properties_risk_state_values,\n    Esql.azure_signinlogs_properties_session_id_values,\n    Esql.azure_signinlogs_properties_user_id_values,\n    Esql_priv.azure_signinlogs_properties_user_principal_name_values,\n    Esql.azure_signinlogs_result_description_values,\n    Esql.azure_signinlogs_result_signature_values,\n    Esql.azure_signinlogs_result_type_values\n\n| where Esql.brute_force_type != \"other\""
      }
    ]
  },
  "automation": {
    "safe": [
      "Add recommendation as Sentinel incident comment.",
      "Create ServiceNow SecOps task.",
      "Run enrichment queries.",
      "Block known-malicious source IP on watchlist."
    ],
    "approval_required": [
      "Force password reset.",
      "Tenant-wide legacy authentication block.",
      "Disable account."
    ]
  },
  "escalation_criteria": [
    "A spray or brute-force attempt succeeded.",
    "Privileged or break-glass account targeted.",
    "Source IP associated with known threat infrastructure.",
    "Legacy authentication protocol was used to bypass MFA.",
    "Attempts continue after initial containment."
  ],
  "false_positive_considerations": [
    "Misconfigured service account with wrong password — Single account pattern — rule threshold of 5+ accounts won't trigger"
  ],
  "confluence": {
    "title": "T1110 - Brute Force Response Guidance",
    "labels": [
      "mitre",
      "attack",
      "d3fend",
      "secops",
      "basyrix"
    ],
    "sections": [
      "summary",
      "d3fend_mappings",
      "investigation_steps",
      "response_actions",
      "queries",
      "automation",
      "escalation_criteria",
      "false_positive_considerations"
    ]
  }
}