Detection rules › Kusto

Multiple users email forwarded to same destination

Status
available
Severity
medium
Time window
7d
Group by
ClientIP
Source
github.com/Azure/Azure-Sentinel

'Identifies when multiple (more than one) users mailboxes are configured to forward to the same destination. This could be an attacker-controlled destination mailbox configured to collect mail from multiple compromised user accounts.'

MITRE ATT&CK coverage

TacticTechniques
Collection
Exfiltration

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 871ba14c-88ef-48aa-ad38-810f26760ca3
name: Multiple users email forwarded to same destination
description: |
  'Identifies when multiple (more than one) users mailboxes are configured to forward to the same destination. 
  This could be an attacker-controlled destination mailbox configured to collect mail from multiple compromised user accounts.'
severity: Medium
status: Available 
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (Exchange)
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - Exfiltration
relevantTechniques:
  - T1114
  - T1020
query: |
  let queryfrequency = 1d;
  let queryperiod = 7d;
  OfficeActivity
  | where TimeGenerated > ago(queryperiod)
  | where OfficeWorkload =~ "Exchange"
  //| where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule")
  | where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")
  | mv-apply DynamicParameters = todynamic(Parameters) on (summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)))
  | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source')
  | extend DestinationMailAddress = tolower(case(
      isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""),
      isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""),
      isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")),
      ""))
  | where isnotempty(DestinationMailAddress)
  | mv-expand split(DestinationMailAddress, ";")
  | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P<IPAddress>(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P<Port>\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0]
  | extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1])
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP
  | where DistinctUserCount > 1 and EndTime > ago(queryfrequency)
  | mv-expand UserId to typeof(string)
  | 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

Parameters

let queryfrequency = 1d;
let queryperiod = 7d;

Stage 1: source

OfficeActivity

Stage 2: where

| where TimeGenerated > ago(queryperiod)

Stage 3: where

| where OfficeWorkload =~ "Exchange"

Stage 4: where

| where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")

Stage 5: kusto:mv-apply

| mv-apply DynamicParameters = todynamic(Parameters) on (summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)))

Stage 6: evaluate

| evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source')

Stage 7: extend

| extend DestinationMailAddress = tolower(case(
    isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""),
    isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""),
    isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")),
    ""))

Stage 8: where

| where isnotempty(DestinationMailAddress)

Stage 9: mv-expand

| mv-expand split(DestinationMailAddress, ";")

Stage 10: extend

| extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P<IPAddress>(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P<Port>\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0]

Stage 11: extend

| extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1])

Stage 12: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP

Stage 13: where

| where DistinctUserCount > 1 and EndTime > ago(queryfrequency)

Stage 14: mv-expand

| mv-expand UserId to typeof(string)

Stage 15: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DestinationMailAddressis_not_null
  • (no value, null check)
field:"DestinationMailAddress" kind:is_not_null
DistinctUserCountgt
  • 1
field:"DistinctUserCount" kind:gt value:"1"
OfficeWorkloadeq
  • Exchange
field:"m365::Workload" kind:eq value:"Exchange"
Parametersmatch
  • ForwardTo transforms: term
  • ForwardingSmtpAddress transforms: term
  • RedirectTo transforms: term
field:"m365::Parameters" kind:match

Output fields

These fields are emitted when the rule matches.

FieldSource
ClientIPsummarize
DistinctUserCountsummarize
EndTimesummarize
EventCountsummarize
Portssummarize
StartTimesummarize
UserIdsummarize
AccountNameextend
AccountUPNSuffixextend