Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Suspicious application consent similar to PwnAuth

Back
Id39198934-62a0-4781-8416-a81265c03fd6
RulenameSuspicious application consent similar to PwnAuth
DescriptionThis 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.
SeverityMedium
TacticsCredentialAccess
DefenseEvasion
TechniquesT1528
T1550
Required data connectorsAzureActiveDirectory
KindScheduled
Query frequency1d
Query period14d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/MaliciousOAuthApp_PwnAuth.yaml
Version1.0.2
Arm template39198934-62a0-4781-8416-a81265c03fd6.json
Deploy To Azure
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])
id: 39198934-62a0-4781-8416-a81265c03fd6
requiredDataConnectors:
- dataTypes:
  - AuditLogs
  connectorId: AzureActiveDirectory
triggerOperator: gt
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/MaliciousOAuthApp_PwnAuth.yaml
relevantTechniques:
- T1528
- T1550
kind: Scheduled
name: Suspicious application consent similar to PwnAuth
queryFrequency: 1d
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])  
severity: Medium
triggerThreshold: 0
tactics:
- CredentialAccess
- DefenseEvasion
entityMappings:
- fieldMappings:
  - columnName: GrantInitiatedByUserPrincipalName
    identifier: FullName
  - columnName: Name
    identifier: Name
  - columnName: UPNSuffix
    identifier: UPNSuffix
  entityType: Account
- fieldMappings:
  - columnName: GrantInitiatedByAadUserId
    identifier: AadUserId
  entityType: Account
- fieldMappings:
  - columnName: GrantInitiatedByAppServicePrincipalId
    identifier: AadUserId
  entityType: Account
- fieldMappings:
  - columnName: GrantIpAddress
    identifier: Address
  entityType: IP
version: 1.0.2
queryPeriod: 14d
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.'  
status: Available
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2023-02-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/39198934-62a0-4781-8416-a81265c03fd6')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/39198934-62a0-4781-8416-a81265c03fd6')]",
      "properties": {
        "alertRuleTemplateName": "39198934-62a0-4781-8416-a81265c03fd6",
        "customDetails": null,
        "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).\nThe default permissions/scope for the PwnAuth toolkit are user.read, offline_access, mail.readwrite, mail.send, and files.read.all.\nConsent to applications with these permissions should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!\nFor further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'\n",
        "displayName": "Suspicious application consent similar to PwnAuth",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "GrantInitiatedByUserPrincipalName",
                "identifier": "FullName"
              },
              {
                "columnName": "Name",
                "identifier": "Name"
              },
              {
                "columnName": "UPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          },
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "GrantInitiatedByAadUserId",
                "identifier": "AadUserId"
              }
            ]
          },
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "GrantInitiatedByAppServicePrincipalId",
                "identifier": "AadUserId"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "GrantIpAddress",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/MaliciousOAuthApp_PwnAuth.yaml",
        "query": "let detectionTime = 1d;\nlet joinLookback = 14d;\nAuditLogs\n| where TimeGenerated > ago(detectionTime)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Consent to application\"\n| where TargetResources has \"offline\"\n| mv-apply TargetResource = TargetResources on \n  (\n      where TargetResource.type =~ \"ServicePrincipal\"\n      | extend AppDisplayName = tostring(TargetResource.displayName),\n               AppClientId = tostring(TargetResource.id),\n               props = TargetResource.modifiedProperties\n  )\n| 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\")))\n| mv-apply ConsentFull = props on \n  (\n      where ConsentFull.displayName =~ \"ConsentAction.Permissions\"\n  )\n| parse ConsentFull with * \"ConsentType: \" GrantConsentType \", Scope: \" GrantScope1 \"]\" *\n| where ConsentFull has_all (\"user.read\", \"offline_access\", \"mail.readwrite\", \"mail.send\", \"files.read.all\")\n| 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\n| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)\n| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)\n| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)\n| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)\n| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))\n| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)\n| mv-apply AdditionalDetail = AdditionalDetails on \n  (\n      where AdditionalDetail.key =~ \"User-Agent\"\n      | extend GrantUserAgent = AdditionalDetail.value\n  )\n| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId\n| join kind = leftouter (AuditLogs\n| where TimeGenerated > ago(joinLookback)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Add service principal\"\n  | mv-apply TargetResource = TargetResources on \n      (\n          where TargetResource.type =~ \"ServicePrincipal\"\n          | extend props = TargetResource.modifiedProperties,\n                  AppClientId = tostring(TargetResource.id)\n      )\n  | mv-apply Property = props on \n      (\n          where Property.displayName =~ \"AppAddress\" and Property.newValue has \"AddressType\"\n          | extend AppReplyURLs = trim('\"',tostring(Property.newValue))\n      )\n| distinct AppClientId, tostring(AppReplyURLs)\n)\non AppClientId\n| join kind = innerunique (AuditLogs\n| where TimeGenerated > ago(joinLookback)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Add OAuth2PermissionGrant\" or OperationName =~ \"Add delegated permission grant\"\n      | mv-apply TargetResource = TargetResources on \n          (\n              where TargetResource.type =~ \"ServicePrincipal\" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)\n              | extend GrantAuthentication = tostring(TargetResource.displayName)\n          )\n| extend GrantOperation = OperationName\n| project GrantAuthentication, GrantOperation, CorrelationId\n) on CorrelationId\n| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull\n| extend timestamp = TimeGenerated, Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P14D",
        "severity": "Medium",
        "status": "Available",
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess",
          "DefenseEvasion"
        ],
        "techniques": [
          "T1528",
          "T1550"
        ],
        "templateVersion": "1.0.2",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}