Break-glass account credentials or MFA modified


Description

Identifies password resets, security-info registrations, and MFA changes made to an account designated as an emergency break-glass account, which should otherwise remain untouched between scheduled tests.

Query · kql

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let BreakGlassAccounts = (
    _GetWatchlist('BreakGlassAccounts')
    | project AccountUPN = tolower(tostring(SearchKey))
);
let SecurityInfoOps = dynamic([
    "Admin registered security info",
    "Admin updated security info",
    "Admin deleted security info",
    "User registered security info",
    "User changed default security info",
    "User deleted security info",
    "User registered all required security info",
    "User started security info registration"
]);
// Password reset/change operation names are not consistent across tenants and Entra ID
// service versions (this repo alone has three different exact-string lists for the same
// event in other hunting queries), so credential resets are matched the same way the
// official "Multiple Password Reset by user" analytic rule does it: any OperationName
// that mentions a password/credential noun together with a change/reset verb, admin or
// self-service alike, rather than an exact-string list that silently misses variants.
let PasswordWords = dynamic(["password", "credential", "credentials"]);
let ChangeActionWords = dynamic(["change", "changed", "reset"]);
AuditLogs
| where TimeGenerated between (starttime .. endtime)
| where Result =~ "success"
| where OperationName in~ (SecurityInfoOps)
      or (OperationName has_any (PasswordWords) and OperationName has_any (ChangeActionWords))
// The target user is read from whichever TargetResources entry has type "User" rather
// than a fixed array index, since these operations do not consistently place the target
// first; this matches the extraction used elsewhere in this repository for password
// reset events specifically.
| mv-apply TargetResource = TargetResources on (
      where TargetResource.type =~ "User"
      | extend TargetUpn = tolower(tostring(TargetResource.userPrincipalName))
  )
| where TargetUpn in (BreakGlassAccounts)
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor    = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp  = iff(
      isnotempty(tostring(InitiatedBy.user.ipAddress)),
      tostring(InitiatedBy.user.ipAddress),
      tostring(InitiatedBy.app.ipAddress))
| extend AccountName      = tostring(split(TargetUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(TargetUpn, "@")[1])
| project
    TimeGenerated,
    OperationName,
    TargetUpn,
    AccountName,
    AccountUPNSuffix,
    Actor,
    ActorIp,
    CorrelationId
| sort by TimeGenerated desc
Raw source Break-glass account credentials or MFA modified · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: 67657f61-a999-4c64-9093-e988d8d98f63
name: Break-glass account credentials or MFA modified
description: |
  Identifies password resets, security-info registrations, and MFA changes made to an account
  designated as an emergency break-glass account, which should otherwise remain untouched
  between scheduled tests.
description-detailed: |
  Break-glass (emergency access) accounts exist to remain usable when every other authentication
  path in the tenant has failed, which only works if their credentials and registered security
  info stay exactly as documented between the periodic tests organizations run to validate them.
  A password reset or a change to registered MFA methods outside a documented test window is a
  strong signal that either the account is being repurposed by an attacker for persistence, or
  that its emergency-access properties have silently drifted out of the state the runbook expects.
  This query depends on the same watchlist as the other two queries in this pack: a watchlist
  named `BreakGlassAccounts` whose `SearchKey` column holds the user principal names of the
  tenant's designated emergency access accounts.
  Every match should be checked against the change calendar for a planned access-recovery test
  before escalating. A match with no corresponding test window, or one closely followed by a
  sign-in or a role and group membership change on the same account (see the two companion
  hunting queries in this pack), should be treated as high priority.
  References:
  - https://learn.microsoft.com/entra/identity/role-based-access-control/security-emergency-access
  - https://attack.mitre.org/techniques/T1098/
  - https://attack.mitre.org/techniques/T1556/006/
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - DefenseEvasion
relevantTechniques:
  - T1098
  - T1556.006
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  let BreakGlassAccounts = (
      _GetWatchlist('BreakGlassAccounts')
      | project AccountUPN = tolower(tostring(SearchKey))
  );
  let SecurityInfoOps = dynamic([
      "Admin registered security info",
      "Admin updated security info",
      "Admin deleted security info",
      "User registered security info",
      "User changed default security info",
      "User deleted security info",
      "User registered all required security info",
      "User started security info registration"
  ]);
  // Password reset/change operation names are not consistent across tenants and Entra ID
  // service versions (this repo alone has three different exact-string lists for the same
  // event in other hunting queries), so credential resets are matched the same way the
  // official "Multiple Password Reset by user" analytic rule does it: any OperationName
  // that mentions a password/credential noun together with a change/reset verb, admin or
  // self-service alike, rather than an exact-string list that silently misses variants.
  let PasswordWords = dynamic(["password", "credential", "credentials"]);
  let ChangeActionWords = dynamic(["change", "changed", "reset"]);
  AuditLogs
  | where TimeGenerated between (starttime .. endtime)
  | where Result =~ "success"
  | where OperationName in~ (SecurityInfoOps)
        or (OperationName has_any (PasswordWords) and OperationName has_any (ChangeActionWords))
  // The target user is read from whichever TargetResources entry has type "User" rather
  // than a fixed array index, since these operations do not consistently place the target
  // first; this matches the extraction used elsewhere in this repository for password
  // reset events specifically.
  | mv-apply TargetResource = TargetResources on (
        where TargetResource.type =~ "User"
        | extend TargetUpn = tolower(tostring(TargetResource.userPrincipalName))
    )
  | where TargetUpn in (BreakGlassAccounts)
  | extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
  | extend ActorApp = tostring(InitiatedBy.app.displayName)
  | extend Actor    = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
  | extend ActorIp  = iff(
        isnotempty(tostring(InitiatedBy.user.ipAddress)),
        tostring(InitiatedBy.user.ipAddress),
        tostring(InitiatedBy.app.ipAddress))
  | extend AccountName      = tostring(split(TargetUpn, "@")[0])
  | extend AccountUPNSuffix = tostring(split(TargetUpn, "@")[1])
  | project
      TimeGenerated,
      OperationName,
      TargetUpn,
      AccountName,
      AccountUPNSuffix,
      Actor,
      ActorIp,
      CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUpn
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ActorIp
customDetails:
  OperationName: OperationName
  Actor: Actor
version: 1.0.0
metadata:
    source:
        kind: Community
    author:
        name: descambiado
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection", "Identity" ]

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.