Microsoft 365 app permission escalation

Severity highCloud & SaaSSingle eventstablecloud.identity.m365_app_permission_escalation · v1.0.0

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

  • T1098.003Account Manipulation: Additional Cloud Roles· Persistence
  • T1550.001Use Alternate Authentication Material: Application Access Token· Defense Evasion
  • T1078.004Valid Accounts: Cloud Accounts· Initial Access

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.

Browse all detections