Detection rules › Splunk

O365 External Identity Policy Changed

Status
production
Severity
medium
Group by
aws::recipientAccountId, dest, object_name, signature, user, vendor_product
Author
Steven Dick
Source
github.com/splunk/security_content

The following analytic identifies when changes are made to the external guest policies within Azure AD. With Azure AD B2B collaboration, users and administrators can invite external users to collaborate with internal users. This detection also attempts to highlight what may have changed. External guest account invitations should be monitored by security teams as they could potentially lead to unauthorized access. An example of this attack vector was described at BlackHat 2022 by security researcher Dirk-Jan during his tall Backdooring and Hijacking Azure AD Accounts by Abusing External Identities.

Known false positives

  • Business approved changes by known administrators.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

PlatformRecord / event type
Microsoft 365Update policy.

Rule body

name: O365 External Identity Policy Changed
id: 29af1725-7a72-4d2d-8a18-e697e79a62d3
version: 9
creation_date: '2024-04-13'
modification_date: '2026-05-13'
author: Steven Dick
status: production
type: TTP
description: The following analytic identifies when changes are made to the external guest policies within Azure AD. With Azure AD B2B collaboration, users and administrators can invite external users to collaborate with internal users. This detection also attempts to highlight what may have changed. External guest account invitations should be monitored by security teams as they could potentially lead to unauthorized access. An example of this attack vector was described at BlackHat 2022 by security researcher Dirk-Jan during his tall `Backdooring and Hijacking Azure AD Accounts by Abusing External Identities`.
data_source:
    - Office 365 Universal Audit Log
search: "`o365_management_activity` Workload=AzureActiveDirectory Operation=\"Update policy.\" Target{}.ID=\"B2BManagementPolicy\" | eval object_attrs = mvindex('ModifiedProperties{}.NewValue',0), object_attrs_old = mvindex('ModifiedProperties{}.OldValue',0), object_name = mvindex('Target{}.ID',3), signature=Operation, user = case(match(mvindex('Actor{}.ID',-1),\"User\"),mvindex('Actor{}.ID',0),match(mvindex('Actor{}.ID',-1),\"ServicePrincipal\"), mvindex('Actor{}.ID',3),true(),mvindex('Actor{}.ID',0)) | spath input=object_attrs_old output=B2BOld path={} | spath input=B2BOld | rename B2BManagementPolicy.* as B2BManagementPolicyOld.* | spath input=object_attrs output=B2BNew path={} | spath input=B2BNew | eval object_attrs = 'B2BManagementPolicy.InvitationsAllowedAndBlockedDomainsPolicy.AllowedDomains{}' , object_attrs_old = 'B2BManagementPolicyOld.InvitationsAllowedAndBlockedDomainsPolicy.AllowedDomains{}' | eval diff_add=mvmap(object_attrs,if(isnull(mvfind(object_attrs_old,object_attrs)),object_attrs,null)) | eval diff_remove=mvmap(object_attrs_old,if(isnull(mvfind(object_attrs,object_attrs_old)),object_attrs_old,null)) | eval result = case(isnotnull(diff_add),\"Added \".mvjoin(diff_add,\",\"),isnotnull(diff_remove),\"Removed \".mvjoin(diff_remove,\",\")), action = case(isnotnull(diff_add),\"created\",isnotnull(diff_remove),\"deleted\") | stats values(object_attrs) as object_attrs, values(action) as action, values(result) as result, values(B2BManagementPolicy*) as B2BManagementPolicy*, count, min(_time) as firstTime, max(_time) as lastTime by user signature object_name dest vendor_account vendor_product | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `o365_external_identity_policy_changed_filter`"
how_to_implement: You must install the Splunk Microsoft Office 365 Add-on and ingest Office 365 management activity events.
known_false_positives: Business approved changes by known administrators.
references:
    - https://medium.com/tenable-techblog/roles-allowing-to-abuse-entra-id-federation-for-persistence-and-privilege-escalation-df9ca6e58360
    - https://learn.microsoft.com/en-us/entra/external-id/external-identities-overview
finding:
    title: User $user$ changed the external identity [$object_name$] policy
    entity:
        field: user
        type: user
        score: 50
analytic_story:
    - Azure Active Directory Persistence
asset_type: O365 Tenant
mitre_attack_id:
    - T1136.003
product:
    - Splunk Enterprise
    - Splunk Enterprise Security
    - Splunk Cloud
category: cloud
security_domain: threat

Stages and Predicates

Stage 1: search

`o365_management_activity` Workload=AzureActiveDirectory Operation="Update policy." Target{}.ID="B2BManagementPolicy"

Stage 2: eval

| eval object_attrs = mvindex('ModifiedProperties{}.NewValue',0), object_attrs_old = mvindex('ModifiedProperties{}.OldValue',0), object_name = mvindex('Target{}.ID',3), signature=Operation, user = case(match(mvindex('Actor{}.ID',-1),"User"),mvindex('Actor{}.ID',0),match(mvindex('Actor{}.ID',-1),"ServicePrincipal"), mvindex('Actor{}.ID',3),true(),mvindex('Actor{}.ID',0))
user =
ifmatch(mvindex('Actor{}.ID', -1), "User")mvindex('Actor{}.ID', 0)
elifmatch(mvindex('Actor{}.ID', -1), "ServicePrincipal")mvindex('Actor{}.ID', 3)
elsemvindex('Actor{}.ID', 0)

Stage 3: spath

| spath input=object_attrs_old output=B2BOld path={}

Stage 4: spath

| spath input=B2BOld

Stage 5: rename

| rename B2BManagementPolicy.* as B2BManagementPolicyOld.*

Stage 6: spath

| spath input=object_attrs output=B2BNew path={}

Stage 7: spath

| spath input=B2BNew

Stage 8: eval

| eval object_attrs = 'B2BManagementPolicy.InvitationsAllowedAndBlockedDomainsPolicy.AllowedDomains{}' , object_attrs_old = 'B2BManagementPolicyOld.InvitationsAllowedAndBlockedDomainsPolicy.AllowedDomains{}'

Stage 9: eval

| eval diff_add=mvmap(object_attrs,if(isnull(mvfind(object_attrs_old,object_attrs)),object_attrs,null))

Stage 10: eval

| eval diff_remove=mvmap(object_attrs_old,if(isnull(mvfind(object_attrs,object_attrs_old)),object_attrs_old,null))

Stage 11: eval

| eval result = case(isnotnull(diff_add),"Added ".mvjoin(diff_add,","),isnotnull(diff_remove),"Removed ".mvjoin(diff_remove,",")), action = case(isnotnull(diff_add),"created",isnotnull(diff_remove),"deleted")
action =
ifisnotnull(diff_add)"created"
else"deleted"
result =
ifisnotnull(diff_add)concat("Added ", mvjoin(diff_add, ","))
elseconcat("Removed ", mvjoin(diff_remove, ","))

Stage 12: stats

| stats values(object_attrs) as object_attrs, values(action) as action, values(result) as result, values(B2BManagementPolicy*) as B2BManagementPolicy*, count, min(_time) as firstTime, max(_time) as lastTime by user signature object_name dest vendor_account vendor_product

Stage 13: search

| `security_content_ctime(firstTime)`

Stage 14: search

| `security_content_ctime(lastTime)`

Stage 15: search

| `o365_external_identity_policy_changed_filter`

Indicators

These rows show field, operator, and value matches.