← Back to SOC feed Coverage →

MFA method registered from an IP address not seen in user sign-in history

kql MEDIUM Azure-Sentinel
T1556.006
AuditLogsSigninLogs
backdoorcredential-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

An adversary may register an MFA method from a suspicious IP address to bypass multi-factor authentication after compromising credentials. SOC teams should proactively hunt for this behavior in Azure Sentinel to detect potential credential compromise and unauthorized access attempts.

KQL Query

let timeframe = 1d;
let lookback = 30d;
// Build per-user baseline of IPs from successful sign-ins over the past 30 days
let BaselineIPs =
    SigninLogs
    | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
    | where ResultType == 0
    | where isnotempty(IPAddress)
    | extend UserUpn = tolower(UserPrincipalName)
    | summarize KnownIPs = make_set(IPAddress) by UserUpn;
// MFA registration events in the query window
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName =~ "User registered security info"
| where Result =~ "success"
| extend UserUpn    = tolower(tostring(TargetResources[0].userPrincipalName))
| extend UserId     = tostring(TargetResources[0].id)
| extend MethodType = tostring(TargetResources[0].displayName)
| extend RegIp      = tostring(InitiatedBy.user.ipAddress)
| where isnotempty(RegIp) and isnotempty(UserUpn)
// Join with IP baseline; hint.strategy=broadcast pushes the smaller AuditLogs
// stream to all nodes holding BaselineIPs, avoiding an expensive shuffle join
// when the 30-day baseline is large relative to the 1-day event stream.
| join kind=leftouter hint.strategy=broadcast BaselineIPs on $left.UserUpn == $right.UserUpn
// Flag registrations from IPs not present in the baseline, or users with no baseline at all
| where isnull(KnownIPs) or not(set_has_element(KnownIPs, RegIp))
| extend AccountName      = tostring(split(UserUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(UserUpn, "@")[1])
| project
    TimeGenerated,
    UserUpn,
    AccountName,
    AccountUPNSuffix,
    UserId,
    MethodType,
    RegIp,
    KnownIPCount = array_length(KnownIPs),
    CorrelationId
| sort by TimeGenerated desc

Analytic Rule Definition

id: 3d36b19f-cd62-4522-8869-23cdd9cc0c9f
name: MFA method registered from an IP address not seen in user sign-in history
description: |
  Identifies MFA method registration events where the source IP address has not
  appeared in the registering user's 30-day sign-in history. An attacker who obtains
  credentials may register a new MFA method from an attacker-controlled IP to maintain
  access after a password reset. Does not require Entra ID P2 licensing.
  References:
  - https://learn.microsoft.com/azure/active-directory/authentication/concept-mfa-howitworks
  - https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities
  - https://attack.mitre.org/techniques/T1556/006/
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
      - SigninLogs
tactics:
  - Persistence
  - DefenseEvasion
relevantTechniques:
  - T1556.006
query: |
  let timeframe = 1d;
  let lookback = 30d;
  // Build per-user baseline of IPs from successful sign-ins over the past 30 days
  let BaselineIPs =
      SigninLogs
      | where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
      | where ResultType == 0
      | where isnotempty(IPAddress)
      | extend UserUpn = tolower(UserPrincipalName)
      | summarize KnownIPs = make_set(IPAddress) by UserUpn;
  // MFA registration events in the query window
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where OperationName =~ "User registered security info"
  | where Result =~ "success"
  | extend UserUpn    = tolower(tostring(TargetResources[0].userPrincipalName))
  | extend UserId     = tostring(TargetResources[0].id)
  | extend MethodType = tostring(TargetResources[0].displayName)
  | extend RegIp      = tostring(InitiatedBy.user.ipAddress)
  | where isnotempty(RegIp) and isnotempty(UserUpn)
  // Join with IP baseline; hint.strategy=broadcast pushes the smaller AuditLogs
  // stream to all nodes holding BaselineIPs, avoiding an expensive shuffle join
  // when the 30-day baseline is large relative to the 1-day event stream.
  | join kind=leftouter hint.strategy=broadcast BaselineIPs on $left.UserUpn == $right.UserUpn
  // Flag registrations from IPs not present in the baseline, or users with no baseline at all
  | where isnull(KnownIPs) or not(set_has_element(KnownIPs, RegIp))
  | extend AccountName      = tostring(split(UserUpn, "@")[0])
  | extend AccountUPNSuffix = tostring(split(UserUpn, "@")[1])
  | project
      TimeGenerated,
      UserUpn,
      AccountName,
      AccountUPNSuffix,
      UserId,
      MethodType,
      RegIp,
      KnownIPCount = array_length(KnownIPs),
      CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserUpn
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      -

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/MFARegistrationFromUnseenIP.yaml