Detection rules › Kusto

Mail.Read Permissions Granted to Application

Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
PersistenceT1098 Account Manipulation

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

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

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
ActivityDisplayNamematch
  • Add app role assignment to service principal
  • Add delegated permission grant
  • Consent to application transforms: term
Categoryeq
  • ApplicationManagement
Permissionsmatch
  • Mail.Read
  • Mail.ReadWrite
Resulteq
  • success
displayNameeq
  • DelegatedPermissionGrant.Scope
displayNameis_not_null
  • (no value, null check)
displayNamematch
  • @ transforms: tostring, term
keyeq
  • User-Agent
modifiedPropertiesgt
  • 0 transforms: array_length
typeeq
  • ServicePrincipal
userPrincipalNamematch
  • @ transforms: tostring, term

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
Nameextend
UPNSuffixextend