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
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" ]
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
SigninLogs | Ensure this data connector is enabled |
Scenario: A system administrator disables MFA for an account as part of a routine access review, and then logs in from a new IP address (e.g., using a personal device) shortly after.
Filter/Exclusion: Exclude sign-ins from IPs associated with admin tasks or known admin devices (e.g., ip.src == 192.168.1.100 or user_agent contains "Windows 10 Admin") or use a custom field for admin accounts.
Scenario: A scheduled job or automation tool (e.g., Ansible, Jenkins) runs a script that disables MFA for an account and then connects to a remote server using a new IP address.
Filter/Exclusion: Exclude sign-ins with user_agent contains "Ansible", user_agent contains "Jenkins", or client_ip in scheduled_jobs_ip_list.
Scenario: A user disables MFA on a mobile app and then signs in from a new IP address using the same account, possibly due to a network change or location-based access.
Filter/Exclusion: Exclude sign-ins where the user has a known IP range or use a field like user.device_type == "mobile" with a whitelist of trusted devices.
Scenario: A security tool or SIEM system (e.g., Splunk, ELK) runs a script that disables MFA for testing purposes and then authenticates from a new IP address.
Filter/Exclusion: Exclude sign-ins with user_agent contains "Splunk", user_agent contains "ELK", or source == "security_tool".
Scenario: A user disables MFA for an account and then uses a temporary or dynamic IP address (e.g., from a cloud provider like AWS EC2) to sign in for a short-term task.
Filter/Exclusion: Exclude sign