Microsoft 365 app permission escalation
An application registration's REQUESTED permissions changed to include app-only access to Exchange Online, Microsoft Graph or SharePoint Online.
What it detects
An application registration's REQUESTED permissions changed to include app-only access to Exchange Online, Microsoft Graph or SharePoint Online.
This is the step before consent. Consent to application. records the moment a permission is granted; Update application. records the moment it is asked for. Catching the request matters because the two are usually seconds apart in an attack and minutes-to-never apart in legitimate change management — and because the request names the app, which is what an operator needs to look at.
App-only permissions are the ones worth escalating to. They carry no user context, so no MFA applies, no conditional access applies, and no user is prompted or notified. full_access_as_app on Exchange Online reaches EVERY mailbox in the tenant; the Graph equivalents reach mail, files and the directory tenant-wide.
## Why this rule matches on structure, not permission names
cloud.identity.m365_high_privilege_consent lists permission names (Mail.ReadWrite, full_access_as_app, …) and works well on CONSENT records, which carry readable scope strings.
It cannot work here, and the reason is measurable rather than theoretical: an Update application. record expresses permissions as EntitlementId GUIDs under RequiredResourceAccess, and never as names. A real capture of an Exchange full_access_as_app assignment contains the string full_access_as_app **zero** times. Name matching on this record shape fails in every tenant, silently.
So this rule matches what the record actually says: an app-only permission (ImpersonationAccessGrants present) being requested against a high-value resource app id. The GUIDs stay in the evidence for the analyst to resolve — naming a permission we cannot verify offline would be a fabricated finding, which is worse than naming none.
MITRE ATT&CK mapping
False positives and triage
Ordinary application lifecycle produces this constantly. Every integration an MSP or its customer legitimately deploys — a backup product, a mail-signature service, a reporting tool, a migration utility — requests app permissions against exactly these resources. In a healthy tenant most hits are real projects.
The question is never "did an app request Graph access", it is:
* **Which app?** Resolve the AppId in the evidence. A recognisable vendor app deployed by the admin who deployed it is change management. An app with a generic name (Test, Office 365 Access, a plausible-but-wrong vendor name) is the classic consent-phishing shape. * **Who changed it, and from where?** A permission change made by an account that had a risky sign-in an hour earlier is an incident regardless of how normal the app looks. * **Was it consented?** A request that was never granted is a warning; the matching Consent to application. is the incident. The pair, in order, is the strongest form of this signal.
The permissions themselves appear as EntitlementId GUIDs. Resolve them against Microsoft's published app-role list for the resource app id in the same record — this product deliberately does not ship an offline copy of that mapping rather than risk naming the wrong permission in an alert.
The rule
Declarative YAML read by the detection engine — data, never executed. Confidence 70%. Source: rules/cloud/m365_app_permission_escalation.yaml, licensed under the Apache License 2.0.
id: cloud.identity.m365_app_permission_escalation
version: 1.0.0
status: stable
severity: high
confidence: 0.7
platforms: [application]
mitre_techniques: [T1098.003, T1550.001, T1078.004]
description: |
An application registration's REQUESTED permissions changed to include app-only
access to Exchange Online, Microsoft Graph or SharePoint Online.
This is the step before consent. `Consent to application.` records the moment a
permission is granted; `Update application.` records the moment it is asked
for. Catching the request matters because the two are usually seconds apart in
an attack and minutes-to-never apart in legitimate change management — and
because the request names the app, which is what an operator needs to look at.
App-only permissions are the ones worth escalating to. They carry no user
context, so no MFA applies, no conditional access applies, and no user is
prompted or notified. `full_access_as_app` on Exchange Online reaches EVERY
mailbox in the tenant; the Graph equivalents reach mail, files and the
directory tenant-wide.
## Why this rule matches on structure, not permission names
`cloud.identity.m365_high_privilege_consent` lists permission names
(`Mail.ReadWrite`, `full_access_as_app`, …) and works well on CONSENT records,
which carry readable scope strings.
It cannot work here, and the reason is measurable rather than theoretical: an
`Update application.` record expresses permissions as `EntitlementId` GUIDs
under `RequiredResourceAccess`, and never as names. A real capture of an
Exchange `full_access_as_app` assignment contains the string
`full_access_as_app` **zero** times. Name matching on this record shape fails
in every tenant, silently.
So this rule matches what the record actually says: an app-only permission
(`ImpersonationAccessGrants` present) being requested against a high-value
resource app id. The GUIDs stay in the evidence for the analyst to resolve —
naming a permission we cannot verify offline would be a fabricated finding,
which is worse than naming none.
match:
provider: Office365
event_action_any:
- "Update application."
- "Update service principal."
- "Add application."
# The requested-permission structure lives in ModifiedProperties and reaches
# rules only through `ext:` addressing (ADR-053) — the parser replaces the
# structured blob in the message with a placeholder, deliberately, so that
# credential material in a changed property cannot leak into a searchable
# summary.
ext:ModifiedProperties_contains_any:
# knowledge/business-context.yaml -> high_value_resource_apps
- "00000002-0000-0ff1-ce00-000000000000" # Office 365 Exchange Online
- "00000003-0000-0000-c000-000000000000" # Microsoft Graph
- "00000003-0000-0ff1-ce00-000000000000" # Office 365 SharePoint Online
correlation_keys: [user_name]
false_positive_guidance: |
Ordinary application lifecycle produces this constantly. Every integration an
MSP or its customer legitimately deploys — a backup product, a mail-signature
service, a reporting tool, a migration utility — requests app permissions
against exactly these resources. In a healthy tenant most hits are real
projects.
The question is never "did an app request Graph access", it is:
* **Which app?** Resolve the `AppId` in the evidence. A recognisable vendor
app deployed by the admin who deployed it is change management. An app with
a generic name (`Test`, `Office 365 Access`, a plausible-but-wrong vendor
name) is the classic consent-phishing shape.
* **Who changed it, and from where?** A permission change made by an account
that had a risky sign-in an hour earlier is an incident regardless of how
normal the app looks.
* **Was it consented?** A request that was never granted is a warning; the
matching `Consent to application.` is the incident. The pair, in order, is
the strongest form of this signal.
The permissions themselves appear as `EntitlementId` GUIDs. Resolve them against
Microsoft's published app-role list for the resource app id in the same record —
this product deliberately does not ship an offline copy of that mapping rather
than risk naming the wrong permission in an alert.
Related detections
Run this rule on your own logs — offline
KIROSEC runs this rule and the rest of the library over Windows, Linux, macOS and cloud audit logs on the analyst's own machine. Every alert links to the events that raised it, and nothing leaves the device.
