-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Description
Solutions/Microsoft Entra ID/Analytic Rules/AccountCreatedandDeletedinShortTimeframe.yaml
Describe the bug
TL:DR There is an inconsistency in how User Principal Names (UPNs) are extracted for 'Add user' versus 'Delete user' operations, particularly for guest accounts, which can lead to a failure in joining the creation and deletion events. That failure can be abused by an attacker to bypass detection.
The detection logic uses different methods to extract the TargetUserPrincipalName for 'Add user' and 'Delete user' operations, leading to potential mismatches that prevent the inner join from correlating events. Specifically, for 'Add user', it uses trim(@'\"', tostring(TargetResource.userPrincipalName)), which retains the full UPN string. For 'Delete user', it uses extract(@'([a-f0-9]{32})?(.*)', 2, tostring(TargetResource.userPrincipalName)), which is designed to strip an optional GUID prefix (common in guest account UPNs) and capture the remainder. If a guest account is created with a UPN like GUID#EXT#user@domain.com, the 'Add user' extraction will yield the full string including the GUID, while the 'Delete user' extraction will yield only #EXT#user@domain.com. This inconsistency causes the TargetUserPrincipalName values to differ, breaking the inner join and allowing the activity to go undetected.
Bypass type ADE1-01 Substring Manipulation.
To Reproduce
Steps to reproduce the behavior:
An attacker creates a guest user account, which in Azure AD Audit Logs might appear with a UPN format like 1234567890abcdef1234567890abcdef#EXT#malicioususer@external.com. The 'Add user' event will log this full UPN. When the attacker deletes this account, the 'Delete user' event's TargetUserPrincipalName will be extracted as #EXT#malicioususer@external.com by the extract function. Because 1234567890abcdef1234567890abcdef#EXT#malicioususer@external.com does not equal #EXT#malicioususer@external.com, the inner join on TargetUserPrincipalName will fail, and the detection will not trigger."
Expected behavior
The rule aims to detect the creation and subsequent deletion of a user account within a 24-hour timeframe in Azure Active Directory. This behavior is often associated with attackers creating temporary accounts for malicious activities (e.g., initial access, reconnaissance, data exfiltration) and then removing them to cover their tracks.
Screenshots
If applicable, add screenshots to help explain your problem.
Desktop (please complete the following information):
N.A
Smartphone (please complete the following information):
N.A
Additional context
The bug exists in the analytic rule provided by Microsoft.
Provided fix
id: bb616d82-108f-47d3-9dec-9652ea0d3bf6
name: Account Created and Deleted in Short Timeframe
description: |
'Search for user principal name (UPN) events. Look for accounts created and then deleted in under 7 days. Attackers may create an account for their use, and then remove the account when no longer needed.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account
ADE-FIXED: This version addresses ADE vulnerabilities related to UPN extraction consistency and timing-based bypasses by extending the correlation window to 7 days.'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 7d # ADE-FIX: Increased queryPeriod to 7 days to align with the extended internal queryperiod variable and mitigate timing-based bypasses (ADE3-03)
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let queryfrequency = 1h;
let queryperiod = 7d; // ADE-FIX: Increased queryperiod to 7 days to mitigate timing-based bypasses (ADE3-03)
AuditLogs
| where TimeGenerated > ago(queryfrequency)
| where OperationName =~ "Delete user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
// ADE-FIX: Standardized UPN extraction to handle GUID prefixes consistently with 'Add user' (ADE1-01)
| extend TargetUserPrincipalName = extract(@"^(?i)([0-9a-f]{32}).*?_(.*)$", 2, tostring(TargetResource.userPrincipalName))
)
| extend DeletedByApp = tostring(InitiatedBy.app.displayName),
DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
DeletedByAadUserId = tostring(InitiatedBy.user.id),
DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(queryperiod)
| where OperationName =~ "Add user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
// ADE-FIX: Standardized UPN extraction to handle GUID prefixes consistently with 'Delete user' (ADE1-01)
| extend TargetUserPrincipalName = extract(@'([a-f0-9]{32})?(.*)', 2, tostring(TargetResource.userPrincipalName))
)
| project-rename Creation_TimeGenerated = TimeGenerated
) on TargetUserPrincipalName
| extend TimeDelta = Deletion_TimeGenerated - Creation_TimeGenerated
| where TimeDelta between (time(0s) .. queryperiod)
| extend CreatedByApp = tostring(InitiatedBy.app.displayName),
CreatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
CreatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
CreatedByAadUserId = tostring(InitiatedBy.user.id),
CreatedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| project Creation_TimeGenerated, Deletion_TimeGenerated, TimeDelta, TargetUserPrincipalName, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
CreatedByApp, CreatedByAppServicePrincipalId, CreatedByUserPrincipalName, CreatedByAadUserId, CreatedByIPAddress, Creation_AdditionalDetails = AdditionalDetails, Creation_InitiatedBy = InitiatedBy, Creation_TargetResources = TargetResources, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend CreatedByName = tostring(split(CreatedByUserPrincipalName,'@',0)[0]), CreatedByUPNSuffix = tostring(split(CreatedByUserPrincipalName,'@',1)[0])
| extend DeletedByName = tostring(split(DeletedByUserPrincipalName,'@',0)[0]), DeletedByUPNSuffix = tostring(split(DeletedByUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: CreatedByUserPrincipalName
- identifier: Name
columnName: CreatedByName
- identifier: UPNSuffix
columnName: CreatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: CreatedByAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: DeletedByUserPrincipalName
- identifier: Name
columnName: DeletedByName
- identifier: UPNSuffix
columnName: DeletedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: DeletedByAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CreatedByIPAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DeletedByIPAddress
version: 1.1.0
kind: Scheduled