Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

GSA Enriched Office 365 - Multiple Users Email Forwarded to Same Destination

Back
Idd75e8289-d1cb-44d4-bd59-2f44a9172478
RulenameGSA Enriched Office 365 - Multiple Users Email Forwarded to Same Destination
DescriptionIdentifies 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.
SeverityMedium
TacticsCollection
Exfiltration
TechniquesT1114
T1020
Required data connectorsAzureActiveDirectory
Office365
KindScheduled
Query frequency1d
Query period7d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml
Version2.0.5
Arm templated75e8289-d1cb-44d4-bd59-2f44a9172478.json
Deploy To Azure
// Set query parameters
let queryfrequency = 1d;
let queryperiod = 7d;
// EnrichedMicrosoft365AuditLogs Query
let EnrichedEvents = EnrichedMicrosoft365AuditLogs
    | where TimeGenerated > ago(queryperiod)
    | where Workload =~ "Exchange"
    // Uncomment or adjust the following line based on actual field usage
    // | where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule")
    | where tostring(AdditionalProperties.Parameters) has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")
    | mv-apply DynamicParameters = todynamic(tostring(AdditionalProperties.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 and EndTime > ago(queryfrequency)
    | mv-expand UserId to typeof(string)
    | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]);
// OfficeActivity Query
let OfficeEvents = OfficeActivity
    | where TimeGenerated > ago(queryperiod)
    | where OfficeWorkload =~ "Exchange"
    // Uncomment or adjust the following line based on actual field usage
    // | 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(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]);
// Combine and Deduplicate Office and Enriched Logs
let CombinedEvents = OfficeEvents
    | union EnrichedEvents
    | summarize arg_min(StartTime, *) by tostring(DestinationMailAddress), ClientIP;
// Final Output
CombinedEvents
    | project StartTime, EndTime, DestinationMailAddress, ClientIP, DistinctUserCount, UserId, Ports, EventCount, AccountName, AccountUPNSuffix
relevantTechniques:
- T1114
- T1020
name: GSA Enriched Office 365 - Multiple Users Email Forwarded to Same Destination
requiredDataConnectors:
- dataTypes:
  - EnrichedMicrosoft365AuditLogs
  connectorId: AzureActiveDirectory
- dataTypes:
  - OfficeActivity (Exchange)
  connectorId: Office365
entityMappings:
- fieldMappings:
  - identifier: FullName
    columnName: UserId
  - identifier: Name
    columnName: AccountName
  - identifier: UPNSuffix
    columnName: AccountUPNSuffix
  entityType: Account
- fieldMappings:
  - identifier: Address
    columnName: ClientIP
  entityType: IP
triggerThreshold: 0
id: d75e8289-d1cb-44d4-bd59-2f44a9172478
tactics:
- Collection
- Exfiltration
version: 2.0.5
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml
queryPeriod: 7d
kind: Scheduled
queryFrequency: 1d
severity: Medium
status: Available
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.  
query: |
  // Set query parameters
  let queryfrequency = 1d;
  let queryperiod = 7d;
  // EnrichedMicrosoft365AuditLogs Query
  let EnrichedEvents = EnrichedMicrosoft365AuditLogs
      | where TimeGenerated > ago(queryperiod)
      | where Workload =~ "Exchange"
      // Uncomment or adjust the following line based on actual field usage
      // | where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule")
      | where tostring(AdditionalProperties.Parameters) has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")
      | mv-apply DynamicParameters = todynamic(tostring(AdditionalProperties.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 and EndTime > ago(queryfrequency)
      | mv-expand UserId to typeof(string)
      | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]);
  // OfficeActivity Query
  let OfficeEvents = OfficeActivity
      | where TimeGenerated > ago(queryperiod)
      | where OfficeWorkload =~ "Exchange"
      // Uncomment or adjust the following line based on actual field usage
      // | 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(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]);
  // Combine and Deduplicate Office and Enriched Logs
  let CombinedEvents = OfficeEvents
      | union EnrichedEvents
      | summarize arg_min(StartTime, *) by tostring(DestinationMailAddress), ClientIP;
  // Final Output
  CombinedEvents
      | project StartTime, EndTime, DestinationMailAddress, ClientIP, DistinctUserCount, UserId, Ports, EventCount, AccountName, AccountUPNSuffix  
triggerOperator: gt
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2024-01-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/d75e8289-d1cb-44d4-bd59-2f44a9172478')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/d75e8289-d1cb-44d4-bd59-2f44a9172478')]",
      "properties": {
        "alertRuleTemplateName": "d75e8289-d1cb-44d4-bd59-2f44a9172478",
        "customDetails": null,
        "description": "Identifies when multiple (more than one) users' mailboxes are configured to forward to the same destination. \nThis could be an attacker-controlled destination mailbox configured to collect mail from multiple compromised user accounts.\n",
        "displayName": "GSA Enriched Office 365 - Multiple Users Email Forwarded to Same Destination",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "UserId",
                "identifier": "FullName"
              },
              {
                "columnName": "AccountName",
                "identifier": "Name"
              },
              {
                "columnName": "AccountUPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "ClientIP",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml",
        "query": "// Set query parameters\nlet queryfrequency = 1d;\nlet queryperiod = 7d;\n// EnrichedMicrosoft365AuditLogs Query\nlet EnrichedEvents = EnrichedMicrosoft365AuditLogs\n    | where TimeGenerated > ago(queryperiod)\n    | where Workload =~ \"Exchange\"\n    // Uncomment or adjust the following line based on actual field usage\n    // | where Operation in (\"Set-Mailbox\", \"New-InboxRule\", \"Set-InboxRule\")\n    | where tostring(AdditionalProperties.Parameters) has_any (\"ForwardTo\", \"RedirectTo\", \"ForwardingSmtpAddress\")\n    | mv-apply DynamicParameters = todynamic(tostring(AdditionalProperties.Parameters)) on (\n        summarize ParsedParameters = make_bag(pack(tostring(DynamicParameters.Name), DynamicParameters.Value))\n      )\n    | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source')\n    | extend DestinationMailAddress = tolower(case(\n        isnotempty(column_ifexists(\"ForwardTo\", \"\")), column_ifexists(\"ForwardTo\", \"\"),\n        isnotempty(column_ifexists(\"RedirectTo\", \"\")), column_ifexists(\"RedirectTo\", \"\"),\n        isnotempty(column_ifexists(\"ForwardingSmtpAddress\", \"\")), trim_start(@\"smtp:\", column_ifexists(\"ForwardingSmtpAddress\", \"\")),\n        \"\"))\n    | where isnotempty(DestinationMailAddress)\n    | mv-expand split(DestinationMailAddress, \";\")\n    | extend ClientIPValues = extract_all(@'\\[?(::ffff:)?(?P<IPAddress>(\\d+\\.\\d+\\.\\d+\\.\\d+)|[^\\]]+)\\]?([-:](?P<Port>\\d+))?', dynamic([\"IPAddress\", \"Port\"]), ClientIp)[0]\n    | extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1])\n    | 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\n    | where DistinctUserCount > 1 and EndTime > ago(queryfrequency)\n    | mv-expand UserId to typeof(string)\n    | extend AccountName = tostring(split(UserId, \"@\")[0]), AccountUPNSuffix = tostring(split(UserId, \"@\")[1]);\n// OfficeActivity Query\nlet OfficeEvents = OfficeActivity\n    | where TimeGenerated > ago(queryperiod)\n    | where OfficeWorkload =~ \"Exchange\"\n    // Uncomment or adjust the following line based on actual field usage\n    // | where Operation in (\"Set-Mailbox\", \"New-InboxRule\", \"Set-InboxRule\")\n    | where Parameters has_any (\"ForwardTo\", \"RedirectTo\", \"ForwardingSmtpAddress\")\n    | mv-apply DynamicParameters = todynamic(Parameters) on (\n        summarize ParsedParameters = make_bag(pack(tostring(DynamicParameters.Name), DynamicParameters.Value))\n      )\n    | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source')\n    | extend DestinationMailAddress = tolower(case(\n        isnotempty(column_ifexists(\"ForwardTo\", \"\")), column_ifexists(\"ForwardTo\", \"\"),\n        isnotempty(column_ifexists(\"RedirectTo\", \"\")), column_ifexists(\"RedirectTo\", \"\"),\n        isnotempty(column_ifexists(\"ForwardingSmtpAddress\", \"\")), trim_start(@\"smtp:\", column_ifexists(\"ForwardingSmtpAddress\", \"\")),\n        \"\"))\n    | where isnotempty(DestinationMailAddress)\n    | mv-expand split(DestinationMailAddress, \";\")\n    | extend ClientIPValues = extract_all(@'\\[?(::ffff:)?(?P<IPAddress>(\\d+\\.\\d+\\.\\d+\\.\\d+)|[^\\]]+)\\]?([-:](?P<Port>\\d+))?', dynamic([\"IPAddress\", \"Port\"]), ClientIP)[0]\n    | extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1])\n    | 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\n    | where DistinctUserCount > 1 and EndTime > ago(queryfrequency)\n    | mv-expand UserId to typeof(string)\n    | extend AccountName = tostring(split(UserId, \"@\")[0]), AccountUPNSuffix = tostring(split(UserId, \"@\")[1]);\n// Combine and Deduplicate Office and Enriched Logs\nlet CombinedEvents = OfficeEvents\n    | union EnrichedEvents\n    | summarize arg_min(StartTime, *) by tostring(DestinationMailAddress), ClientIP;\n// Final Output\nCombinedEvents\n    | project StartTime, EndTime, DestinationMailAddress, ClientIP, DistinctUserCount, UserId, Ports, EventCount, AccountName, AccountUPNSuffix\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P7D",
        "severity": "Medium",
        "status": "Available",
        "subTechniques": [],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "Collection",
          "Exfiltration"
        ],
        "techniques": [
          "T1020",
          "T1114"
        ],
        "templateVersion": "2.0.5",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}