Detection rules › Kusto

Suspicious application consent similar to PwnAuth

Status
available
Severity
medium
Time window
14d
Group by
AppClientId, CorrelationId
Source
github.com/Azure/Azure-Sentinel

This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the FireEye PwnAuth toolkit (https://github.com/fireeye/PwnAuth). The default permissions/scope for the PwnAuth toolkit are user.read, offline_access, mail.readwrite, mail.send, and files.read.all. Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome! For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 39198934-62a0-4781-8416-a81265c03fd6
name: Suspicious application consent similar to PwnAuth
description: |
  'This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the FireEye PwnAuth toolkit (https://github.com/fireeye/PwnAuth).
  The default permissions/scope for the PwnAuth toolkit are user.read, offline_access, mail.readwrite, mail.send, and files.read.all.
  Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1528
  - T1550
query: |
  let detectionTime = 1d;
  let joinLookback = 14d;
  AuditLogs
  | where TimeGenerated > ago(detectionTime)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Consent to application"
  | where TargetResources has "offline"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "ServicePrincipal"
        | extend AppDisplayName = tostring(TargetResource.displayName),
                 AppClientId = tostring(TargetResource.id),
                 props = TargetResource.modifiedProperties
    )
  | where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
  | mv-apply ConsentFull = props on 
    (
        where ConsentFull.displayName =~ "ConsentAction.Permissions"
    )
  | parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
  | where ConsentFull has_all ("user.read", "offline_access", "mail.readwrite", "mail.send", "files.read.all")
  | where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
  | extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
  | extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
  | extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
  | extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
  | mv-apply AdditionalDetail = AdditionalDetails on 
    (
        where AdditionalDetail.key =~ "User-Agent"
        | extend GrantUserAgent = AdditionalDetail.value
    )
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
  | join kind = leftouter (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add service principal"
    | mv-apply TargetResource = TargetResources on 
        (
            where TargetResource.type =~ "ServicePrincipal"
            | extend props = TargetResource.modifiedProperties,
                    AppClientId = tostring(TargetResource.id)
        )
    | mv-apply Property = props on 
        (
            where Property.displayName =~ "AppAddress" and Property.newValue has "AddressType"
            | extend AppReplyURLs = trim('"',tostring(Property.newValue))
        )
  | distinct AppClientId, tostring(AppReplyURLs)
  )
  on AppClientId
  | join kind = innerunique (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
        | mv-apply TargetResource = TargetResources on 
            (
                where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
                | extend GrantAuthentication = tostring(TargetResource.displayName)
            )
  | extend GrantOperation = OperationName
  | project GrantAuthentication, GrantOperation, CorrelationId
  ) on CorrelationId
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
  | extend timestamp = TimeGenerated, Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: GrantInitiatedByUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: GrantIpAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let detectionTime = 1d;
let joinLookback = 14d;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(detectionTime)

Stage 3: where

| where LoggedByService =~ "Core Directory"

Stage 4: where

| where Category =~ "ApplicationManagement"

Stage 5: where

| where OperationName =~ "Consent to application"

Stage 6: where

| where TargetResources has "offline"

Stage 7: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "ServicePrincipal"
      | extend AppDisplayName = tostring(TargetResource.displayName),
               AppClientId = tostring(TargetResource.id),
               props = TargetResource.modifiedProperties
  )

Stage 8: where

| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))

Stage 9: kusto:mv-apply

| mv-apply ConsentFull = props on 
  (
      where ConsentFull.displayName =~ "ConsentAction.Permissions"
  )

Stage 10: parse

| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *

Stage 11: where

| where ConsentFull has_all ("user.read", "offline_access", "mail.readwrite", "mail.send", "files.read.all")

Stage 12: where

| where GrantConsentType != "AllPrincipals"

Stage 13: extend (6 consecutive steps)

| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)

Stage 14: kusto:mv-apply

| mv-apply AdditionalDetail = AdditionalDetails on 
  (
      where AdditionalDetail.key =~ "User-Agent"
      | extend GrantUserAgent = AdditionalDetail.value
  )

Stage 15: project

| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId

Stage 16: join

| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
  | mv-apply TargetResource = TargetResources on 
      (
          where TargetResource.type =~ "ServicePrincipal"
          | extend props = TargetResource.modifiedProperties,
                  AppClientId = tostring(TargetResource.id)
      )
  | mv-apply Property = props on 
      (
          where Property.displayName =~ "AppAddress" and Property.newValue has "AddressType"
          | extend AppReplyURLs = trim('"',tostring(Property.newValue))
      )
| distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId

Stage 17: join

| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
      | mv-apply TargetResource = TargetResources on 
          (
              where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
              | extend GrantAuthentication = tostring(TargetResource.displayName)
          )
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId

Stage 18: project

| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull

Stage 19: extend

| extend timestamp = TimeGenerated, Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • ApplicationManagement
field:"Category" kind:eq value:"ApplicationManagement"
ConsentFullmatch
  • files.read.all transforms: term
  • mail.readwrite transforms: term
  • mail.send transforms: term
  • offline_access transforms: term
  • user.read transforms: term
field:"ConsentFull" kind:match
GrantConsentTypene
  • AllPrincipals
field:"GrantConsentType" kind:ne value:"AllPrincipals"
LoggedByServiceeq
  • Core Directory
field:"azure_ad::logged_by_service" kind:eq value:"Core Directory"
OperationNameeq
  • Add OAuth2PermissionGrant
  • Add delegated permission grant
  • Add service principal
  • Consent to application
field:"OperationName" kind:eq
TargetResourcesmatch
  • offline transforms: term
field:"azure_ad::target_resources" kind:match value:"offline"
displayNameeq
  • AppAddress
  • ConsentAction.Permissions
field:"displayName" kind:eq
displayNameis_not_null
  • (no value, null check)
field:"displayName" kind:is_not_null
keyeq
  • User-Agent
field:"key" kind:eq value:"User-Agent"
newValuematch
  • AddressType transforms: term
field:"azure_ad::modified_properties_new" kind:match value:"AddressType"
typeeq
  • ServicePrincipal
field:"type" kind:eq value:"ServicePrincipal"

Output fields

These fields are emitted when the rule matches.

FieldSource
AppClientIdproject
AppDisplayNameproject
AppReplyURLsproject
ConsentFullproject
CorrelationIdproject
GrantAuthenticationproject
GrantConsentTypeproject
GrantInitiatedByproject
GrantInitiatedByAadUserIdproject
GrantInitiatedByAppNameproject
GrantInitiatedByAppServicePrincipalIdproject
GrantInitiatedByUserPrincipalNameproject
GrantIpAddressproject
GrantOperationproject
GrantScope1project
GrantUserAgentproject
OperationNameproject
TimeGeneratedproject
Nameextend
UPNSuffixextend
timestampextend