Query Details

HUNT-11 Dormant Account Re-Enablement via Provisioning (30d)

HUNT 11 AAD Prov Dormant Re Enable 30d

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

UserAccountsProvisioningLogsAuditServicePrincipal

Operators

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

Severity

Medium

Tactics

Persistence

MITRE Techniques

Actions

GitHub