Detection rules › Kusto

Office Policy Tampering

Status
available
Severity
medium
Time window
1d
Group by
ClientIP, Operation, Parameters, Port, ResultStatus, UserId, UserType
Source
github.com/Azure/Azure-Sentinel

Identifies if any tampering is done to either auditlog, ATP Safelink, SafeAttachment, AntiPhish or Dlp policy. An adversary may use this technique to evade detection or avoid other policy based defenses. References: https://docs.microsoft.com/powershell/module/exchange/advanced-threat-protection/remove-antiphishrule?view=exchange-ps.

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1098 Account Manipulation
StealthT1562 Impair Defenses

Event coverage

Rule body kusto

id: fbd72eb8-087e-466b-bd54-1ca6ea08c6d3
name: Office Policy Tampering
description: |
  'Identifies if any tampering is done to either auditlog, ATP Safelink, SafeAttachment, AntiPhish or Dlp policy. 
  An adversary may use this technique to evade detection or avoid other policy based defenses.
  References: https://docs.microsoft.com/powershell/module/exchange/advanced-threat-protection/remove-antiphishrule?view=exchange-ps.'
severity: Medium
status: Available 
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (Exchange)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - DefenseEvasion
relevantTechniques:
  - T1098
  - T1562
query: |
  let opList = OfficeActivity 
  | summarize by Operation
  //| where Operation startswith "Remove-" or Operation startswith "Disable-"
  | where Operation has_any ("Remove", "Disable")
  | where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit"
  | summarize make_set(Operation, 500);
  OfficeActivity
  // Only admin or global-admin can disable/remove policy
  | where RecordType =~ "ExchangeAdmin"
  | where UserType in~ ("Admin","DcAdmin")
  // Pass in interesting Operation list
  | where Operation in~ (opList)
  | extend ClientIPOnly = case( 
  ClientIP has ".", tostring(split(ClientIP,":")[0]), 
  ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))),
  ClientIP
  )  
  | extend Port = case(
  ClientIP has ".", (split(ClientIP,":")[1]),
  ClientIP has "[", tostring(split(ClientIP,"]:")[1]),
  ClientIP
  )
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters
  | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 2.0.3
kind: Scheduled

Stages and Predicates

Let binding: opList

let opList = OfficeActivity 
| summarize by Operation
| where Operation has_any ("Remove", "Disable")
| where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit"
| summarize make_set(Operation, 500);

Stage 1: source

OfficeActivity

Stage 2: where

| where RecordType =~ "ExchangeAdmin"

Stage 3: where

| where UserType in~ ("Admin","DcAdmin")

Stage 4: where

| where Operation in~ (opList)

References opList (defined above).

Stage 5: extend

| extend ClientIPOnly = case( 
ClientIP has ".", tostring(split(ClientIP,":")[0]), 
ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))),
ClientIP
)
ClientIPOnly =
ifClientIP has "."tostring(split(ClientIP, ":")[0])
elifClientIP has "["tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0])))
elseClientIP

Stage 6: extend

| extend Port = case(
ClientIP has ".", (split(ClientIP,":")[1]),
ClientIP has "[", tostring(split(ClientIP,"]:")[1]),
ClientIP
)
Port =
ifClientIP has "."split(ClientIP, ":")[1]
elifClientIP has "["tostring(split(ClientIP, "]:")[1])
elseClientIP

Stage 7: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters

Stage 8: extend

| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Operationin
  • opList
RecordTypeeq
  • ExchangeAdmin
UserTypein
  • Admin
  • DcAdmin

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
ClientIPsummarize
EndTimeUtcsummarize
Operationsummarize
OperationCountsummarize
Parameterssummarize
Portsummarize
ResultStatussummarize
StartTimeUtcsummarize
UserIdsummarize
UserTypesummarize
AccountNameextend
AccountUPNSuffixextend