Query Details

HUNT 11 AAD Prov Dormant Re Enable 30d

Query

id: aa1f000b-200b-420b-920b-aadprov-hunt11
name: HUNT-11 Dormant Account Re-Enablement via Provisioning (30d)
description: |
  User accounts disabled for >= 90 days that were re-enabled via the
  provisioning channel in the last 30 days. Dormant-account re-enablement
  via provisioning is a documented persistence path: attacker forces a
  long-disabled service or shared identity back to life as a backdoor.
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADProvisioningLogs
      - AuditLogs
tactics:
  - Persistence
relevantTechniques:
  - T1098
  - T1078
query: |
  // Recent re-enables via provisioning
  let RecentReEnables =
      AADProvisioningLogs
      | where TimeGenerated > ago(30d)
      | where ResultType =~ "Success"
      | mv-expand Mod = todynamic(ModifiedProperties)
      | extend PropName = tostring(Mod.displayName),
               OldValue = tostring(Mod.oldValue),
               NewValue = tostring(Mod.newValue)
      | where PropName has_any ("accountEnabled","AccountEnabled")
      | where OldValue has "false" and NewValue has "true"
      | extend TargetUpn = tostring(parse_json(TargetIdentity).userPrincipalName),
               SPName    = tostring(parse_json(ServicePrincipal).Name)
      | project TimeGenerated, TargetUpn, SPName, OldValue, NewValue;
  // Find last disable for each
  let LastDisable =
      AuditLogs
      | where TimeGenerated > ago(180d)
      | where OperationName has_any ("Disable account","Update user")
      | mv-expand TargetResources
      | mv-expand Mod = todynamic(TargetResources.modifiedProperties)
      | where tostring(Mod.displayName) has_any ("AccountEnabled","accountEnabled")
      | where tostring(Mod.newValue) has "false"
      | extend TargetUpn = tostring(TargetResources.userPrincipalName)
      | summarize LastDisableTime = max(TimeGenerated) by TargetUpn;
  RecentReEnables
  | join kind=inner (LastDisable) on TargetUpn
  | extend DormantDays = datetime_diff('day', TimeGenerated, LastDisableTime)
  | where DormantDays >= 90
  | project ReEnableTime = TimeGenerated, TargetUpn, SPName,
            LastDisableTime, DormantDays
  | order by DormantDays desc

Explanation

This query is designed to identify user accounts that were disabled for 90 days or more and have recently been re-enabled through a provisioning process within the last 30 days. This situation can be a security concern because attackers might exploit these dormant accounts as backdoors. Here's a simplified breakdown of the query:

  1. Data Sources: The query uses data from Azure Active Directory, specifically from AADProvisioningLogs and AuditLogs.

  2. Recent Re-Enables: It first identifies accounts that have been re-enabled in the last 30 days. It checks for changes where the account status changed from "disabled" (false) to "enabled" (true).

  3. Last Disable: It then looks back over the last 180 days to find the last time these accounts were disabled.

  4. Join and Filter: The query joins these two datasets to find accounts that were disabled for 90 days or more before being re-enabled.

  5. Output: It outputs details such as the time of re-enablement, the user principal name, the service principal name, the last disable time, and the number of days the account was dormant.

  6. Order: The results are ordered by the number of dormant days in descending order, highlighting accounts that were dormant the longest.

Overall, this query helps identify potentially risky account re-enablement activities that could indicate a security threat.

Details

David Alonso profile picture

David Alonso

Released: June 1, 2026

Tables

AADProvisioningLogsAuditLogs

Keywords

UserAccountsProvisioningLogsAuditLogsServicePrincipal

Operators

let|where>ago()=~mv-expandtodynamic()extendtostring()parse_json()projecthas_anyhasandjoinkind=innerondatetime_diff()>=summarizemax()byorder bydesc

Actions