Detection rules › Kusto
Mail.Read Permissions Granted to Application
This query look for applications that have been granted (Delegated or App/Role) permissions to Read Mail (Permissions field has Mail.Read) and subsequently has been consented to. This can help identify applications that have been abused to gain access to mailboxes.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule body
id: 2560515c-07d1-434e-87fb-ebe3af267760
name: Mail.Read Permissions Granted to Application
description: |
'This query look for applications that have been granted (Delegated or App/Role) permissions to Read Mail (Permissions field has Mail.Read) and subsequently has been consented to. This can help identify applications that have been abused to gain access to mailboxes.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Persistence
relevantTechniques:
- T1098
tags:
- Solorigate
- NOBELIUM
query: |
AuditLogs
| where Category =~ "ApplicationManagement"
| where ActivityDisplayName has_any ("Add delegated permission grant","Add app role assignment to service principal")
| where Result =~ "success"
| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
| extend props = TargetResource.modifiedProperties,
Type = tostring(TargetResource.type),
PermissionsAddedTo = tostring(TargetResource.displayName)
)
| mv-apply Property = props on
(
where Property.displayName =~ "DelegatedPermissionGrant.Scope"
| extend DisplayName = tostring(Property.displayName), Permissions = trim('"',tostring(Property.newValue))
)
| where Permissions has_any ("Mail.Read", "Mail.ReadWrite")
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend InitiatingUserAgent = tostring(AdditionalDetail.value)
)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| project-away props, TargetResource, AdditionalDetail, Property
| join kind=leftouter(
AuditLogs
| where ActivityDisplayName has "Consent to application"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend AppName = tostring(TargetResource.displayName),
AppId = tostring(TargetResource.id)
)
| project AppName, AppId, CorrelationId) on CorrelationId
| project-away CorrelationId1
| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserAgent, PermissionsAddedTo, Permissions, AppName, AppId, CorrelationId
| extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "ApplicationManagement"
Stage 3: where
| where ActivityDisplayName has_any ("Add delegated permission grant","Add app role assignment to service principal")
Stage 4: where
| where Result =~ "success"
Stage 5: where
| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
Stage 6: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
| extend props = TargetResource.modifiedProperties,
Type = tostring(TargetResource.type),
PermissionsAddedTo = tostring(TargetResource.displayName)
)
Stage 7: kusto:mv-apply
| mv-apply Property = props on
(
where Property.displayName =~ "DelegatedPermissionGrant.Scope"
| extend DisplayName = tostring(Property.displayName), Permissions = trim('"',tostring(Property.newValue))
)
Stage 8: where
| where Permissions has_any ("Mail.Read", "Mail.ReadWrite")
Stage 9: kusto:mv-apply
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend InitiatingUserAgent = tostring(AdditionalDetail.value)
)
Stage 10: extend (5 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
Stage 11: project-away
| project-away props, TargetResource, AdditionalDetail, Property
Stage 12: join
| join kind=leftouter(
AuditLogs
| where ActivityDisplayName has "Consent to application"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend AppName = tostring(TargetResource.displayName),
AppId = tostring(TargetResource.id)
)
| project AppName, AppId, CorrelationId) on CorrelationId
Stage 13: project-away
| project-away CorrelationId1
Stage 14: project-reorder
| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserAgent, PermissionsAddedTo, Permissions, AppName, AppId, CorrelationId
Stage 15: extend
| extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match |
Category | eq |
| field:"Category" kind:eq value:"ApplicationManagement" |
Permissions | match |
| field:"Permissions" kind:match |
Result | eq |
| field:"Result" kind:eq value:"success" |
displayName | eq |
| field:"displayName" kind:eq value:"DelegatedPermissionGrant.Scope" |
displayName | is_not_null | field:"displayName" kind:is_not_null | |
displayName | match |
| field:"displayName" kind:match value:"@" |
key | eq |
| field:"key" kind:eq value:"User-Agent" |
type | eq |
| field:"type" kind:eq value:"ServicePrincipal" |
userPrincipalName | match |
| field:"userPrincipalName" kind:match value:"@" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
Name | extend |
UPNSuffix | extend |