Techniques
Sample rules
Entra ID End-User Consent to Application with High-Risk Delegated Scopes
- source: elastic
- technicques:
- T1528
- T1566
Description
Identifies an end-user (non-admin) consent grant in Microsoft Entra ID to an application requesting delegated scopes commonly abused in illicit consent grant (OAuth phishing) attacks, such as mail, mailbox settings, contacts, files, SharePoint, OneNote, Teams chat and Exchange Web Services. Graph scopes are evaluated together with offline_access, which returns a long-lived refresh token and gives an attacker-controlled application durable, silent access to the victim’s data without stealing a password or re-prompting for MFA. The alert classifies the application owner as Microsoft first-party, in-tenant or external to help prioritize review.
Detection logic
FROM logs-azure.auditlogs-* metadata _id, _version, _index
| WHERE (azure.auditlogs.operation_name == "Consent to application"
OR event.action == "Consent to application")
AND event.outcome == "success"
// End-user (non-admin) consent only
AND TO_LOWER(`azure.auditlogs.properties.target_resources.0.modified_properties.0.new_value`) LIKE "*false*"
// Normalize ConsentAction.Permissions into a lower-cased, space-padded token string so " token " is an exact scope match.
// Raw: "[] => [[Id: .., ResourceId: .., ConsentType: Principal, Scope: Mail.Read offline_access .., ..]]"
| EVAL Esql.scopes = CONCAT(" ", REPLACE(TO_LOWER(`azure.auditlogs.properties.target_resources.0.modified_properties.4.new_value`), """[\[\],;"]""", " "), " ")
// additional_details.key/value are parallel arrays but ES|QL returns multivalues sorted, so pair via MV_CONTAINS, not position.
// Owner = AppOwnerOrganizationId (Microsoft tenants) OR well-known first-party public-client AppIds (not always stamped with owner org).
| EVAL
Esql.app_owner_type = CASE(
MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "f8cdef31-a31e-4b4a-93e4-5f571e91255a") // Microsoft Services
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "72f988bf-86f1-41af-91ab-2d7cd011db47") // Microsoft corp
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "14d82eec-204b-4c2f-b7e8-296a70dab67e") // Microsoft Graph Command Line Tools
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "de8bc8b5-d9f9-48b1-a8ad-b748da725064") // Graph Explorer
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "04b07795-8ddb-461a-bbee-02f9e1bf7b46") // Azure CLI
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "1950a258-227b-4e31-a9cf-717495945fc2") // Azure PowerShell
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "c44b4083-3bb0-49c1-b47d-974e53cbdf3c") // Azure Portal
OR MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "aebc6443-996d-45c2-90f0-388ff96faa56"), "microsoft", // VS Code
MV_CONTAINS(azure.auditlogs.properties.additional_details.value, azure.tenant_id), "tenant",
"external"),
// "Consent" = service principal was created by this consent, i.e. first time the app appears in the tenant
Esql.sp_provisioned_by_consent = MV_CONTAINS(azure.auditlogs.properties.additional_details.value, "Consent")
// High-risk scope families: in-the-wild consent phishing (MS IR playbook), 365-Stealer/GraphRunner defaults, Midnight Blizzard EWS.
// Positive enumeration excludes low-risk variants (Mail.ReadBasic, Chat.ReadBasic, Files.*.Selected) without lookarounds.
| EVAL
Esql.high_risk_scopes = TRIM(CONCAT(
CASE(Esql.scopes RLIKE """.* mail\.(read|readwrite|send)(\.shared)? .*""", "mail ", ""),
CASE(Esql.scopes RLIKE """.* mailbox(settings|item|folder)\.[a-z]+ .*""", "mailbox_settings ", ""),
CASE(Esql.scopes RLIKE """.* contacts\.[a-z.]+ .*""", "contacts ", ""),
CASE(Esql.scopes RLIKE """.* files\.(read|readwrite)(\.all|\.appfolder)? .*""", "files ", ""),
CASE(Esql.scopes RLIKE """.* sites\.[a-z]+\.all .*""", "sites ", ""),
CASE(Esql.scopes RLIKE """.* notes\.[a-z.]+ .*""", "notes ", ""),
CASE(Esql.scopes RLIKE """.* (chat\.(read|readwrite|create)(\.all)?|chatmessage\.[a-z]+) .*""", "teams_chat ", ""),
CASE(Esql.scopes RLIKE """.* (allsites\.[a-z]+|myfiles\.[a-z]+) .*""", "sharepoint_spo ", ""),
CASE(Esql.scopes RLIKE """.* (ews\.accessasuser\.all|full_access_as_user) .*""", "exchange_ews ", ""))),
// Low-risk scope families: recon / lower impact. ARM user_impersonation and Directory.AccessAsUser.All only count for
// non-Microsoft apps (Azure CLI / PowerShell / Portal / VS Code consent noise).
Esql.low_risk_scopes = TRIM(CONCAT(
CASE(Esql.scopes RLIKE """.* people\.read(\.all)? .*""", "people ", ""),
CASE(Esql.scopes RLIKE """.* calendars\.[a-z.]+ .*""", "calendars ", ""),
CASE(Esql.scopes RLIKE """.* onlinemeetings\.[a-z]+ .*""", "online_meetings ", ""),
CASE(Esql.scopes RLIKE """.* tasks\.[a-z.]+ .*""", "tasks ", ""),
CASE(Esql.scopes RLIKE """.* user\.(read\.all|readbasic\.all) .*""", "user_directory ", ""),
CASE(Esql.app_owner_type != "microsoft" AND Esql.scopes RLIKE """.* (user_impersonation|directory\.accessasuser\.all) .*""", "impersonation ", "")))
| EVAL
Esql.high_risk_scope_count = CASE(Esql.high_risk_scopes == "", 0, MV_COUNT(SPLIT(Esql.high_risk_scopes, " "))),
Esql.low_risk_scope_count = CASE(Esql.low_risk_scopes == "", 0, MV_COUNT(SPLIT(Esql.low_risk_scopes, " ")))
// Graph families require offline_access in the same event (durable access). SharePoint/EWS resource scopes are logged in a
// separate event without offline_access, so they alert on their own. External apps stacking 2+ low-risk scopes also alert.
| WHERE (Esql.scopes LIKE "* offline_access *" AND Esql.high_risk_scope_count >= 1)
OR Esql.high_risk_scopes LIKE "*sharepoint_spo*"
OR Esql.high_risk_scopes LIKE "*exchange_ews*"
OR (Esql.scopes LIKE "* offline_access *" AND Esql.low_risk_scope_count >= 2 AND Esql.app_owner_type == "external")
| EVAL Esql.target_service_principal_id = `azure.auditlogs.properties.target_resources.0.id`
| DROP Esql.scopes
| KEEP
_id,
_version,
_index,
@timestamp,
event.action,
event.outcome,
azure.tenant_id,
azure.auditlogs.operation_name,
azure.auditlogs.properties.correlation_id,
azure.auditlogs.properties.initiated_by.user.userPrincipalName,
azure.auditlogs.properties.initiated_by.user.id,
azure.auditlogs.properties.initiated_by.user.ipAddress,
`azure.auditlogs.properties.target_resources.0.display_name`,
`azure.auditlogs.properties.target_resources.0.id`,
`azure.auditlogs.properties.target_resources.0.modified_properties.4.new_value`,
azure.auditlogs.properties.additional_details.key,
azure.auditlogs.properties.additional_details.value,
Esql.*