Detection rules › Kusto

Service Principal Assigned App Role With Sensitive Access

Severity
medium
Time window
1d
Group by
AddedPermissions, CorrelationId, Id, InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, ServicePrincipalDisplayName, ServicePrincipalObjectID
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read. A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions. Ensure that any assignment to a Service Principal is valid and appropriate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: dd78a122-d377-415a-afe9-f22e08d2112c
name: Service Principal Assigned App Role With Sensitive Access
description: |
  'Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read.
    A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions.
    Ensure that any assignment to a Service Principal is valid and appropriate.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
    // Add other permissions to this list as needed
    let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
    "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
    let auditList = 
    AuditLogs
    | where OperationName =~ "Add app role assignment to service principal"
    | mv-expand TargetResources[0].modifiedProperties
    | extend TargetResources_0_modifiedProperties = column_ifexists("TargetResources_0_modifiedProperties", '')
    | where isnotempty(TargetResources_0_modifiedProperties)
    ;
    let detailsList = auditList
    | where TargetResources_0_modifiedProperties.displayName =~ "AppRole.Value" or TargetResources_0_modifiedProperties.displayName =~ "DelegatedPermissionGrant.Scope"
    | extend Permissions = split((parse_json(tostring(TargetResources_0_modifiedProperties.newValue))), " ")
    | where Permissions has_any (permissions)
    | summarize AddedPermissions=make_set(Permissions,200) by CorrelationId
    | join kind=inner auditList on CorrelationId
    | 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(InitiatedBy.user.ipAddress)
    | extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
    | extend displayName = tostring(TargetResources_0_modifiedProperties.displayName), newValue = tostring(parse_json(tostring(TargetResources_0_modifiedProperties.newValue)))
    | where displayName == "ServicePrincipal.ObjectID" or displayName == "ServicePrincipal.DisplayName"
    | extend displayName = case(displayName == "ServicePrincipal.ObjectID", "ServicePrincipalObjectID", displayName == "ServicePrincipal.DisplayName", "ServicePrincipalDisplayName", displayName)
    | project TimeGenerated, CorrelationId, Id, AddedPermissions = tostring(AddedPermissions), InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, displayName, newValue
    ;
    detailsList | project Id, displayName, newValue
    | evaluate pivot(displayName, make_set(newValue))
    | join kind=inner detailsList on Id
    | extend ServicePrincipalObjectID = todynamic(column_ifexists("ServicePrincipalObjectID", "")), ServicePrincipalDisplayName = todynamic(column_ifexists("ServicePrincipalDisplayName", ""))
    | mv-expand ServicePrincipalObjectID, ServicePrincipalDisplayName
    | project-away Id1, displayName, newValue
    | extend ServicePrincipalObjectID = tostring(ServicePrincipalObjectID), ServicePrincipalDisplayName = tostring(ServicePrincipalDisplayName)
    | summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), EventIds = make_set(Id,200) by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID
    | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
      - identifier: ObjectGuid
        columnName: ServicePrincipalObjectID
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
version: 2.0.0
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 0: let

let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
"Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
let auditList = AuditLogs <inlined as stages below>;
let detailsList = auditList <inlined as stages below>;

Stage 1: source

AuditLogs

Stage 2: where

where OperationName =~ "Add app role assignment to service principal"

Stage 3: mv-expand

mv-expand modifiedProperties

Stage 4: extend

extend TargetResources_0_modifiedProperties

Stage 5: where

where isnotempty(TargetResources_0_modifiedProperties)

Stage 6: where

where (displayName =~ "AppRole.Value" or displayName =~ "DelegatedPermissionGrant.Scope")

Stage 7: extend

extend Permissions

Stage 8: where

where (Permissions contains ".All" or Permissions contains "ReadWrite" or Permissions contains "Mail." or Permissions contains "offline_access" or Permissions contains "Files.Read" or Permissions contains "Notes.Read" or Permissions contains "ChannelMessage.Read" or Permissions contains "Chat.Read" or Permissions contains "TeamsActivity.Read" or Permissions contains "Group.Read" or Permissions contains "EWS.AccessAsUser.All" or Permissions contains "EAS.AccessAsUser.All")

Stage 9: summarize

summarize AddedPermissions by CorrelationId

Stage 10: join

| join kind=inner auditList on CorrelationId

Stage 11: extend (7 consecutive steps)

extend InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, displayName, newValue

Stage 12: where

where (displayName =~ "ServicePrincipal.DisplayName" or displayName =~ "ServicePrincipal.ObjectID")

Stage 13: extend

extend displayName
displayName =
ifdisplayName == "ServicePrincipal.ObjectID""ServicePrincipalObjectID"
elifdisplayName == "ServicePrincipal.DisplayName""ServicePrincipalDisplayName"
elsedisplayName

Stage 14: project

project AddedPermissions, CorrelationId, Id, InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, TimeGenerated, displayName, newValue

Stage 15: project

project Id, displayName, newValue

Stage 16: evaluate

evaluate

Stage 17: join

join kind=inner (detailsList) on Id

Stage 18: extend

extend ServicePrincipalDisplayName, ServicePrincipalObjectID

Stage 19: mv-expand

mv-expand ServicePrincipalObjectID

Stage 20: project-away

project-away Id1, displayName, newValue

Stage 21: extend

extend ServicePrincipalDisplayName, ServicePrincipalObjectID

Stage 22: summarize

summarize EventIds, FirstSeen, LastSeen by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID

Stage 23: extend

extend InitiatingAccountName, InitiatingAccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNameeq
  • Add app role assignment to service principal
field:"OperationName" kind:eq value:"Add app role assignment to service principal"
Permissionsmatch
  • .All transforms: term
  • ChannelMessage.Read transforms: term
  • Chat.Read transforms: term
  • EAS.AccessAsUser.All transforms: term
  • EWS.AccessAsUser.All transforms: term
  • Files.Read transforms: term
  • Group.Read transforms: term
  • Mail. transforms: term
  • Notes.Read transforms: term
  • ReadWrite transforms: term
  • TeamsActivity.Read transforms: term
  • offline_access transforms: term
field:"Permissions" kind:match
TargetResources_0_modifiedPropertiesis_not_null
  • (no value, null check)
field:"TargetResources_0_modifiedProperties" kind:is_not_null
displayNameeq
  • AppRole.Value
  • DelegatedPermissionGrant.Scope
  • ServicePrincipal.DisplayName
  • ServicePrincipal.ObjectID
field:"displayName" kind:eq

Output fields

These fields are emitted when the rule matches.

FieldSource
AddedPermissionssummarize
CorrelationIdsummarize
EventIdssummarize
FirstSeensummarize
InitiatedBysummarize
InitiatingAadUserIdsummarize
InitiatingAppNamesummarize
InitiatingAppServicePrincipalIdsummarize
InitiatingIPAddresssummarize
InitiatingUserPrincipalNamesummarize
LastSeensummarize
ServicePrincipalDisplayNamesummarize
ServicePrincipalObjectIDsummarize
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend