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 O365 Attack Toolkit

Back
Idf948a32f-226c-4116-bddd-d95e91d97eb9
RulenameSuspicious application consent similar to O365 Attack Toolkit
DescriptionThis will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the MDSec O365 Attack Toolkit (https://github.com/mdsecactivebreach/o365-attack-toolkit).

The default permissions/scope for the MDSec O365 Attack toolkit change sometimes but often include contacts.read, user.read, mail.read, notes.read.all, mailboxsettings.readwrite, files.readwrite.all, mail.send, files.read, and files.read.all.

Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded, 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.
SeverityHigh
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_O365AttackToolkit.yaml
Version1.1.2
Arm templatef948a32f-226c-4116-bddd-d95e91d97eb9.json
Deploy To Azure
let detectionTime = 1d;
let joinLookback = 14d;
let threshold = 5;
let o365_attack_regex = "contacts.read|user.read|mail.read|notes.read.all|mailboxsettings.readwrite|Files.ReadWrite.All|mail.send|files.read|files.read.all";
let o365_attack = dynamic(["contacts.read", "user.read", "mail.read", "notes.read.all", "mailboxsettings.readwrite", "Files.ReadWrite.All", "mail.send", "files.read", "files.read.all"]);
AuditLogs
| where TimeGenerated > ago(detectionTime)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Consent to application"
| 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"))) // NOTE: a MATCH from this list will cause the alert to NOT fire - please modify for your environment!
| mv-apply ConsentFull = props on 
  (
      where ConsentFull.displayName =~ "ConsentAction.Permissions"
  )
| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 ", CreatedDateTime" * "]" *
| 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
| where ConsentFull has_any (o365_attack)  
| extend GrantScopeCount = countof(tolower(GrantScope1), o365_attack_regex, 'regex')
| where GrantScopeCount > threshold
| 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 Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
queryPeriod: 14d
version: 1.1.2
kind: Scheduled
triggerThreshold: 0
relevantTechniques:
- T1528
- T1550
triggerOperator: gt
entityMappings:
- 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: Account
- fieldMappings:
  - identifier: Address
    columnName: GrantIpAddress
  entityType: IP
- fieldMappings:
  - identifier: Name
    columnName: AppDisplayName
  entityType: CloudApplication
query: |
  let detectionTime = 1d;
  let joinLookback = 14d;
  let threshold = 5;
  let o365_attack_regex = "contacts.read|user.read|mail.read|notes.read.all|mailboxsettings.readwrite|Files.ReadWrite.All|mail.send|files.read|files.read.all";
  let o365_attack = dynamic(["contacts.read", "user.read", "mail.read", "notes.read.all", "mailboxsettings.readwrite", "Files.ReadWrite.All", "mail.send", "files.read", "files.read.all"]);
  AuditLogs
  | where TimeGenerated > ago(detectionTime)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Consent to application"
  | 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"))) // NOTE: a MATCH from this list will cause the alert to NOT fire - please modify for your environment!
  | mv-apply ConsentFull = props on 
    (
        where ConsentFull.displayName =~ "ConsentAction.Permissions"
    )
  | parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 ", CreatedDateTime" * "]" *
  | 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
  | where ConsentFull has_any (o365_attack)  
  | extend GrantScopeCount = countof(tolower(GrantScope1), o365_attack_regex, 'regex')
  | where GrantScopeCount > threshold
  | 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 Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])  
name: Suspicious application consent similar to O365 Attack Toolkit
queryFrequency: 1d
requiredDataConnectors:
- connectorId: AzureActiveDirectory
  dataTypes:
  - AuditLogs
description: |
  'This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the MDSec O365 Attack Toolkit (https://github.com/mdsecactivebreach/o365-attack-toolkit).
  The default permissions/scope for the MDSec O365 Attack toolkit change sometimes but often include contacts.read, user.read, mail.read, notes.read.all, mailboxsettings.readwrite, files.readwrite.all, mail.send, files.read, and files.read.all.
  Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded, 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
id: f948a32f-226c-4116-bddd-d95e91d97eb9
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/MaliciousOAuthApp_O365AttackToolkit.yaml
tactics:
- CredentialAccess
- DefenseEvasion
severity: High
{
  "$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/f948a32f-226c-4116-bddd-d95e91d97eb9')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/f948a32f-226c-4116-bddd-d95e91d97eb9')]",
      "properties": {
        "alertRuleTemplateName": "f948a32f-226c-4116-bddd-d95e91d97eb9",
        "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 MDSec O365 Attack Toolkit (https://github.com/mdsecactivebreach/o365-attack-toolkit).\nThe default permissions/scope for the MDSec O365 Attack toolkit change sometimes but often include contacts.read, user.read, mail.read, notes.read.all, mailboxsettings.readwrite, files.readwrite.all, mail.send, files.read, and files.read.all.\nConsent to applications with these permissions should be rare, especially as the knownApplications list is expanded, 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 O365 Attack Toolkit",
        "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"
              }
            ]
          },
          {
            "entityType": "CloudApplication",
            "fieldMappings": [
              {
                "columnName": "AppDisplayName",
                "identifier": "Name"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Entra ID/Analytic Rules/MaliciousOAuthApp_O365AttackToolkit.yaml",
        "query": "let detectionTime = 1d;\nlet joinLookback = 14d;\nlet threshold = 5;\nlet o365_attack_regex = \"contacts.read|user.read|mail.read|notes.read.all|mailboxsettings.readwrite|Files.ReadWrite.All|mail.send|files.read|files.read.all\";\nlet o365_attack = dynamic([\"contacts.read\", \"user.read\", \"mail.read\", \"notes.read.all\", \"mailboxsettings.readwrite\", \"Files.ReadWrite.All\", \"mail.send\", \"files.read\", \"files.read.all\"]);\nAuditLogs\n| where TimeGenerated > ago(detectionTime)\n| where LoggedByService =~ \"Core Directory\"\n| where Category =~ \"ApplicationManagement\"\n| where OperationName =~ \"Consent to application\"\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\"))) // NOTE: a MATCH from this list will cause the alert to NOT fire - please modify for your environment!\n| mv-apply ConsentFull = props on \n  (\n      where ConsentFull.displayName =~ \"ConsentAction.Permissions\"\n  )\n| parse ConsentFull with * \"ConsentType: \" GrantConsentType \", Scope: \" GrantScope1 \", CreatedDateTime\" * \"]\" *\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| where ConsentFull has_any (o365_attack)  \n| extend GrantScopeCount = countof(tolower(GrantScope1), o365_attack_regex, 'regex')\n| where GrantScopeCount > threshold\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) on 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 Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P14D",
        "severity": "High",
        "status": "Available",
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess",
          "DefenseEvasion"
        ],
        "techniques": [
          "T1528",
          "T1550"
        ],
        "templateVersion": "1.1.2",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}