Skip to content

Commit

Permalink
Enhanced Microsoft Exchange External Forwarding Detection (#1529)
Browse files Browse the repository at this point in the history
Co-authored-by: Panther Bot <[email protected]>
  • Loading branch information
arielkr256 and panther-bot-automation authored Mar 4, 2025
1 parent be4a294 commit 17515a1
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 105 deletions.
36 changes: 36 additions & 0 deletions global_helpers/panther_msft_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,39 @@ def get_target_name(event, target_type="User"):
def azure_success(event):
result = event.deep_get("properties", "result", default="")
return result == "success"


def is_external_address(address, primary_domain, onmicrosoft_domain):
"""Check if an email address is external to the organization.
Args:
address (str): The email address or SMTP address to check
primary_domain (str): The organization's primary domain (e.g. contoso.com)
onmicrosoft_domain (str): The tenant domain (e.g. contoso.onmicrosoft.com)
Returns:
bool: True if the address is external, False if internal
"""
if not address or (not primary_domain and not onmicrosoft_domain):
return True

# Clean up and normalize the address
address = address.lower()
if address.startswith("smtp:"):
address = address[5:]

# Check each address (might be multiple addresses separated by semicolon)
for addr in address.split(";"):
try:
domain = addr.strip().split("@")[1].lower()
# Skip if internal (matches onmicrosoft domain or primary domain/subdomain)
if (onmicrosoft_domain and domain == onmicrosoft_domain.lower()) or (
primary_domain
and (domain == primary_domain or domain.endswith("." + primary_domain))
):
continue
return True
except (IndexError, AttributeError):
return True

return False
4 changes: 3 additions & 1 deletion indexes/alpha-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,8 @@

## AWS GuardDuty

- [AWS GuardDuty Critical Severity Finding](../rules/aws_guardduty_rules/aws_guardduty_critical_sev_findings.yml)
- A critical-severity GuardDuty finding has been identified.
- [AWS GuardDuty Enabled](../policies/aws_guardduty_policies/aws_guardduty_enabled.yml)
- GuardDuty is a threat detection service that continuously monitors for malicious activity and unauthorized behavior.
- [AWS GuardDuty High Severity Finding](../rules/aws_guardduty_rules/aws_guardduty_high_sev_findings.yml)
Expand Down Expand Up @@ -1273,7 +1275,7 @@
## Microsoft365

- [Microsoft Exchange External Forwarding](../rules/microsoft_rules/microsoft_exchange_external_forwarding.yml)
- Detects creation of forwarding rule to external domains
- Detects when a user creates email forwarding rules to external organizations in Microsoft Exchange Online. This can indicate data exfiltration attempts, where an attacker sets up forwarding to collect emails outside the organization. The rule detects both mailbox forwarding (Set-Mailbox) and inbox rules (New-InboxRule).The detection includes: 1. External organization forwarding based on domain comparison 2. Suspicious forwarding patterns like: - Forwarding without keeping a copy - Deleting messages after forwarding - Stopping rule processing after forwarding3. Multiple forwarding destinations 4. Various forwarding methods (SMTP, redirect, forward as attachment)
- [Microsoft365 Brute Force Login by User](../rules/microsoft_rules/microsoft365_brute_force_login_by_user.yml)
- A Microsoft365 user was denied login access several times
- [Microsoft365 External Document Sharing](../rules/microsoft_rules/microsoft365_external_sharing.yml)
Expand Down
2 changes: 2 additions & 0 deletions indexes/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@

## AWS GuardDuty

- [AWS GuardDuty Critical Severity Finding](../rules/aws_guardduty_rules/aws_guardduty_critical_sev_findings.yml)
- A critical-severity GuardDuty finding has been identified.
- [AWS GuardDuty Enabled](../policies/aws_guardduty_policies/aws_guardduty_enabled.yml)
- GuardDuty is a threat detection service that continuously monitors for malicious activity and unauthorized behavior.
- [AWS GuardDuty High Severity Finding](../rules/aws_guardduty_rules/aws_guardduty_high_sev_findings.yml)
Expand Down
2 changes: 1 addition & 1 deletion indexes/detection-coverage.json

Large diffs are not rendered by default.

92 changes: 71 additions & 21 deletions rules/microsoft_rules/microsoft_exchange_external_forwarding.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,81 @@
from panther_config import config
from panther_msft_helpers import is_external_address, m365_alert_context

FORWARDING_PARAMETERS = {
"ForwardingSmtpAddress",
"ForwardTo",
"ForwardingAddress",
"RedirectTo",
"ForwardAsAttachmentTo",
}

SUSPICIOUS_PATTERNS = {
"DeliverToMailboxAndForward": "False", # Only forward, don't keep copy
"DeleteMessage": "True", # Delete after forwarding
"StopProcessingRules": "True", # Stop processing other rules
}


def rule(event):
if event.get("operation", "") in ("Set-Mailbox", "New-InboxRule"):
for param in event.get("parameters", []):
if param.get("Name", "") in ("ForwardingSmtpAddress", "ForwardTo", "ForwardingAddress"):
to_email = param.get("Value", "")
if (
to_email.lower().replace("smtp:", "")
in config.MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_EMAILS
):
return False
for domain in config.MS_EXCHANGE_ALLOWED_FORWARDING_DESTINATION_DOMAINS:
if to_email.lower().replace("smtp:", "").endswith(domain):
return False
"""Alert on suspicious or external email forwarding configurations."""
# Skip non-forwarding related operations
if event.get("operation") not in ("Set-Mailbox", "New-InboxRule"):
return False

# Get organization domains from userid and organizationname
onmicrosoft_domain = event.get("organizationname", "").lower()
userid = event.get("userid", "").lower()
try:
primary_domain = userid.split("@")[1]
except (IndexError, AttributeError):
primary_domain = onmicrosoft_domain if onmicrosoft_domain else None

if not primary_domain:
return True # Alert if we can't determine organization

# Check each parameter
for param in event.get("parameters", []):
param_name = param.get("Name", "")
param_value = param.get("Value", "")

# Check for suspicious patterns
if param_name in SUSPICIOUS_PATTERNS and param_value == SUSPICIOUS_PATTERNS[param_name]:
return True

# Check for external forwarding
if param_name in FORWARDING_PARAMETERS and param_value:
if is_external_address(param_value, primary_domain, onmicrosoft_domain):
return True

return False


def title(event):
to_email = "<no-recipient-found>"
for param in event.get("parameters", []):
if param.get("Name", "") in ("ForwardingSmtpAddress", "ForwardTo"):
to_email = param.get("Value", "")
break
parameters = event.get("parameters", [])
forwarding_addresses = []
suspicious_configs = []

for param in parameters:
param_name = param.get("Name", "")
param_value = param.get("Value", "")

if param_name in FORWARDING_PARAMETERS and param_value:
# Handle smtp: prefix
if param_value.lower().startswith("smtp:"):
param_value = param_value[5:]
# Handle multiple addresses
addresses = param_value.split(";")
forwarding_addresses.extend(addr.strip() for addr in addresses if addr.strip())
if param_name in SUSPICIOUS_PATTERNS and param_value == SUSPICIOUS_PATTERNS[param_name]:
suspicious_configs.append(f"{param_name}={param_value}")

to_emails = ", ".join(forwarding_addresses) if forwarding_addresses else "<no-recipient-found>"
suspicious_str = f" [Suspicious: {', '.join(suspicious_configs)}]" if suspicious_configs else ""

return (
"Microsoft365: External Forwarding Created From "
f"[{event.get('userid', '')}] to "
f"[{to_email}]"
f"Microsoft365: External Forwarding Created From [{event.get('userid', '')}] "
f"to [{to_emails}]{suspicious_str}"
)


def alert_context(event):
return m365_alert_context(event)
Loading

0 comments on commit 17515a1

Please sign in to comment.