Detection rules › Kusto

NRT Multiple users email forwarded to same destination

Severity
medium
Group by
ClientIP
Author
Pete Bryan
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: 3b05727d-a8d1-477d-bbdd-d957da96ac7b
name: NRT 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
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
tactics:
  - Collection
  - Exfiltration
relevantTechniques:
  - T1114
  - T1020
query: |
  OfficeActivity
  | where OfficeWorkload =~ "Exchange"
  | where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")
  | mv-apply DynamicParameters = todynamic(Parameters) on (summarize ParsedParameters = make_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
  | mv-expand UserId to typeof(string)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 1.0.3
kind: NRT
metadata:
    source:
        kind: Community
    author:
        name: Pete Bryan
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Stage 1: source

OfficeActivity

Stage 2: where

| where OfficeWorkload =~ "Exchange"

Stage 3: where

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

Stage 4: kusto:mv-apply

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

Stage 5: evaluate

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

Stage 6: 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 7: where

| where isnotempty(DestinationMailAddress)

Stage 8: mv-expand

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

Stage 9: extend

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

Stage 10: extend

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

Stage 11: 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 12: where

| where DistinctUserCount > 1

Stage 13: mv-expand

| mv-expand UserId to typeof(string)

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