← Back to SOC feed Coverage →

Sign-in from unseen IP within 60 minutes of MFA disabled for account

kql MEDIUM Azure-Sentinel
T1556.006T1078.004
AuditLogsSigninLogs
credential-thefthuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-28T11:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may be using stolen credentials to sign in from new IP addresses shortly after disabling MFA to avoid detection. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential post-compromise activity and mitigate lateral movement risks.

KQL Query

let timeframe = 1d;
let correlationWindow = 60m;
let lookback = 30d;
let DisabledMFA =
    AuditLogs
    | where TimeGenerated >= ago(timeframe)
    | where OperationName in~ (
          "Disable Strong Authentication",
          "User deleted security info"
      )
    | where Result =~ "success"
    | extend AffectedUser = tolower(tostring(TargetResources[0].userPrincipalName))
    | where isnotempty(AffectedUser)
    | project AffectedUser, MFADisabledTime = TimeGenerated;
let KnownIPs =
    SigninLogs
    | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
    | where ResultType == 0
    | project UserPrincipalName = tolower(UserPrincipalName), IPAddress
    | summarize KnownIPSet = make_set(IPAddress, 1000)
        by UserPrincipalName;
SigninLogs
| where TimeGenerated >= ago(timeframe)
| where ResultType == 0
| extend UserUpn = tolower(UserPrincipalName)
| join kind=inner DisabledMFA on $left.UserUpn == $right.AffectedUser
| where TimeGenerated >= MFADisabledTime and TimeGenerated <= MFADisabledTime + correlationWindow
| join kind=leftouter KnownIPs on $left.UserUpn == $right.UserPrincipalName
| where isnull(KnownIPSet) or not(set_has_element(KnownIPSet, IPAddress))
| extend AccountName      = tostring(split(UserUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(UserUpn, "@")[1])
| project
    TimeGenerated,
    UserUpn,
    AccountName,
    AccountUPNSuffix,
    MFADisabledTime,
    IPAddress,
    AppDisplayName,
    Location,
    AutonomousSystemNumber,
    CorrelationId
| sort by TimeGenerated desc

Analytic Rule Definition

id: 3140d3e9-f87c-48aa-8d81-1b78b6c5d7bf
name: Sign-in from unseen IP within 60 minutes of MFA disabled for account
description: Identifies successful sign-ins from IP addresses not seen in the prior 30 days occurring within 60 minutes of MFA being disabled for the same account, consistent with post-compromise credential use after weakening authentication.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
      - SigninLogs
tactics:
  - CredentialAccess
  - Persistence
relevantTechniques:
  - T1556.006
  - T1078.004
query: |
  let timeframe = 1d;
  let correlationWindow = 60m;
  let lookback = 30d;
  let DisabledMFA =
      AuditLogs
      | where TimeGenerated >= ago(timeframe)
      | where OperationName in~ (
            "Disable Strong Authentication",
            "User deleted security info"
        )
      | where Result =~ "success"
      | extend AffectedUser = tolower(tostring(TargetResources[0].userPrincipalName))
      | where isnotempty(AffectedUser)
      | project AffectedUser, MFADisabledTime = TimeGenerated;
  let KnownIPs =
      SigninLogs
      | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
      | where ResultType == 0
      | project UserPrincipalName = tolower(UserPrincipalName), IPAddress
      | summarize KnownIPSet = make_set(IPAddress, 1000)
          by UserPrincipalName;
  SigninLogs
  | where TimeGenerated >= ago(timeframe)
  | where ResultType == 0
  | extend UserUpn = tolower(UserPrincipalName)
  | join kind=inner DisabledMFA on $left.UserUpn == $right.AffectedUser
  | where TimeGenerated >= MFADisabledTime and TimeGenerated <= MFADisabledTime + correlationWindow
  | join kind=leftouter KnownIPs on $left.UserUpn == $right.UserPrincipalName
  | where isnull(KnownIPSet) or not(set_has_element(KnownIPSet, IPAddress))
  | extend AccountName      = tostring(split(UserUpn, "@")[0])
  | extend AccountUPNSuffix = tostring(split(UserUpn, "@")[1])
  | project
      TimeGenerated,
      UserUpn,
      AccountName,
      AccountUPNSuffix,
      MFADisabledTime,
      IPAddress,
      AppDisplayName,
      Location,
      AutonomousSystemNumber,
      CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserUpn
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.0
metadata:
    source:
        kind: Community
    author:
        name: descambiado
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection", "Identity" ]

Required Data Sources

Sentinel TableNotes
AuditLogsEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/MultipleDataSources/MFADisabledThenSignInFromUnseenIP.yaml