Detection rules › Kusto
Rare application consent
This will alert when the "Consent to application" operation occurs by a user that has not done this operation before or rarely does this. This could indicate that permissions to access the listed Azure App were provided to a malicious actor. Consent to application, Add service principal and Add OAuth2PermissionGrant should typically be rare events. This may help detect the Oauth2 attack that can be initiated by this publicly available tool - https://github.com/fireeye/PwnAuth For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule body
id: 83ba3057-9ea3-4759-bf6a-933f2e5bc7ee
name: Rare application consent
description: |
'This will alert when the "Consent to application" operation occurs by a user that has not done this operation before or rarely does this.
This could indicate that permissions to access the listed Azure App were provided to a malicious actor.
Consent to application, Add service principal and Add OAuth2PermissionGrant should typically be rare events.
This may help detect the Oauth2 attack that can be initiated by this publicly available tool - https://github.com/fireeye/PwnAuth
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 3
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1136
- T1068
query: |
let current = 1d;
let auditLookback = 7d;
// Setting threshold to 3 as a default, change as needed.
// Any operation that has been initiated by a user or app more than 3 times in the past 7 days will be excluded
let threshold = 3;
// Gather initial data from lookback period, excluding current, adjust current to more than a single day if no results
let AuditTrail = AuditLogs | where TimeGenerated >= ago(auditLookback) and TimeGenerated < ago(current)
// 2 other operations that can be part of malicious activity in this situation are
// "Add OAuth2PermissionGrant" and "Add service principal", extend the filter below to capture these too
| where OperationName has "Consent to application"
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName))
)
| summarize max(TimeGenerated), OperationCount = count() by OperationName, InitiatedBy, TargetResourceName
// only including operations initiated by a user or app that is above the threshold so we produce only rare and has not occurred in last 7 days
| where OperationCount > threshold;
// Gather current period of audit data
let RecentConsent = AuditLogs | where TimeGenerated >= ago(current)
| where OperationName has "Consent to application"
| extend IpAddress = case(
isnotempty(tostring(InitiatedBy.user.ipAddress)) and tostring(InitiatedBy.user.ipAddress) != 'null', tostring(InitiatedBy.user.ipAddress),
isnotempty(tostring(InitiatedBy.app.ipAddress)) and tostring(InitiatedBy.app.ipAddress) != 'null', tostring(InitiatedBy.app.ipAddress),
'Not Available')
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName)),
props = TargetResource.modifiedProperties
)
| parse props with * "ConsentType: " ConsentType "]" *
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
| project TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type;
// Exclude previously seen audit activity for "Consent to application" that was seen in the lookback period
// First for rare InitiatedBy
let RareConsentBy = RecentConsent | join kind= leftanti AuditTrail on OperationName, InitiatedBy
| extend Reason = "Previously unseen user consenting";
// Second for rare TargetResourceName
let RareConsentApp = RecentConsent | join kind= leftanti AuditTrail on OperationName, TargetResourceName
| extend Reason = "Previously unseen app granted consent";
RareConsentBy | union RareConsentApp
| summarize Reason = make_set(Reason,100) by TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type
| extend timestamp = TimeGenerated, Name = tolower(tostring(split(InitiatedBy,'@',0)[0])), UPNSuffix = tolower(tostring(split(InitiatedBy,'@',1)[0]))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatedBy
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: TargetResourceName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.1.5
kind: Scheduled
Stages and Predicates
Stage 0: let
let current = 1d;
let auditLookback = 7d;
let threshold = 3;
let AuditTrail = AuditLogs | where TimeGenerated >= ago(auditLookback) and TimeGenerated < ago(current)
| where OperationName has "Consent to application"
| extend InitiatedBy = iff(isnotempty(tostring(InitiatedBy.user.userPrincipalName)),
tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend TargetResourceName = tolower(tostring(TargetResource.displayName))
)
| summarize max(TimeGenerated), OperationCount = count() by OperationName, InitiatedBy, TargetResourceName
| where OperationCount > threshold;
let RecentConsent = AuditLogs <inlined as stages below>;
let RareConsentBy = RecentConsent <inlined as stages below>;
let RareConsentApp = RecentConsent <inlined as stages below>;
Stage 1: source
AuditLogs
Stage 2: where
where TimeGenerated >= ago(86400s)
Stage 3: where
where OperationName contains "Consent to application"
Stage 4: extend
extend IpAddress
IpAddress =ipAddress != "null"tostring(InitiatedBy.user.ipAddress)ipAddress != "null"tostring(InitiatedBy.app.ipAddress)'Not Available'Stage 5: extend
extend InitiatedBy
InitiatedBy =/* macro: isnotempty(tostring(InitiatedBy.user.userPrincipalName)) */tostring(InitiatedBy.user.userPrincipalName)tostring(InitiatedBy.app.displayName)Stage 6: kusto:mv-apply
kusto:mv-apply type =~ "ServicePrincipal"
Stage 7: parse
parse
Stage 8: kusto:mv-apply
kusto:mv-apply key =~ "User-Agent"
Stage 9: project
project Category, ConsentType, CorrelationId, InitiatedBy, IpAddress, OperationName, TargetResourceName, TimeGenerated, Type, UserAgent
Stage 10: join (negated)
join kind=leftanti (AuditTrail) on OperationName, InitiatedBy
Stage 11: extend
extend Reason
Stage 12: union
union of 1 branches
Stage 13: source
AuditLogs
Stage 14: where
where TimeGenerated >= ago(86400s)
Stage 15: where
where OperationName contains "Consent to application"
Stage 16: extend
extend IpAddress
IpAddress =ipAddress != "null"tostring(InitiatedBy.user.ipAddress)ipAddress != "null"tostring(InitiatedBy.app.ipAddress)'Not Available'Stage 17: extend
extend InitiatedBy
InitiatedBy =/* macro: isnotempty(tostring(InitiatedBy.user.userPrincipalName)) */tostring(InitiatedBy.user.userPrincipalName)tostring(InitiatedBy.app.displayName)Stage 18: kusto:mv-apply
kusto:mv-apply type =~ "ServicePrincipal"
Stage 19: parse
parse
Stage 20: kusto:mv-apply
kusto:mv-apply key =~ "User-Agent"
Stage 21: project
project Category, ConsentType, CorrelationId, InitiatedBy, IpAddress, OperationName, TargetResourceName, TimeGenerated, Type, UserAgent
Stage 22: join (negated)
join kind=leftanti (AuditTrail) on OperationName, TargetResourceName
Stage 23: extend
extend Reason
Stage 24: summarize
summarize Reason by TimeGenerated, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, ConsentType, UserAgent, CorrelationId, Type
Stage 25: extend
extend Name, UPNSuffix, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | match |
| field:"OperationName" kind:match value:"Consent to application" |
key | eq |
| field:"key" kind:eq value:"User-Agent" |
type | eq |
| field:"type" kind:eq value:"ServicePrincipal" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
OperationCount | gt | 3 | excludes:OperationCount field:"OperationCount" value:"3" |
OperationName | match | Consent to application | excludes:OperationName field:"OperationName" value:"Consent to application" |
type | eq | ServicePrincipal | excludes:type field:"type" value:"ServicePrincipal" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | summarize |
ConsentType | summarize |
CorrelationId | summarize |
InitiatedBy | summarize |
IpAddress | summarize |
OperationName | summarize |
Reason | summarize |
TargetResourceName | summarize |
TimeGenerated | summarize |
Type | summarize |
UserAgent | summarize |
Name | extend |
UPNSuffix | extend |
timestamp | extend |