Attackers may be using inbox rules to delete emails containing specific keywords, potentially removing evidence or communication with compromised users, indicating a follow-up to an initial compromise. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify and mitigate advanced persistent threat activity that could obscure further malicious actions.
KQL Query
let Keywords = dynamic(["helpdesk", " alert", " suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]);
OfficeActivity
| where OfficeWorkload =~ "Exchange"
| where Parameters has "Deleted Items" or Parameters has "Junk Email" or Parameters has "DeleteMessage"
| extend Events=todynamic(Parameters)
| parse Events with * "SubjectContainsWords" SubjectContainsWords '}'*
| parse Events with * "BodyContainsWords" BodyContainsWords '}'*
| parse Events with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*
| where SubjectContainsWords has_any (Keywords)
or BodyContainsWords has_any (Keywords)
or SubjectOrBodyContainsWords has_any (Keywords)
| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
| extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
| summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail
id: b79f6190-d104-4691-b7db-823e05980895
name: NRT Malicious Inbox Rule
description: |
'Often times after the initial compromise the attackers create inbox rules to delete emails that contain certain keywords.
This is done so as to limit ability to warn compromised users that they've been compromised. Below is a sample query that tries to detect this.
Reference: https://www.reddit.com/r/sysadmin/comments/7kyp0a/recent_phishing_attempts_my_experience_and_what/'
severity: Medium
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity
tactics:
- Persistence
- DefenseEvasion
relevantTechniques:
- T1098
- T1078
query: |
let Keywords = dynamic(["helpdesk", " alert", " suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]);
OfficeActivity
| where OfficeWorkload =~ "Exchange"
| where Parameters has "Deleted Items" or Parameters has "Junk Email" or Parameters has "DeleteMessage"
| extend Events=todynamic(Parameters)
| parse Events with * "SubjectContainsWords" SubjectContainsWords '}'*
| parse Events with * "BodyContainsWords" BodyContainsWords '}'*
| parse Events with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*
| where SubjectContainsWords has_any (Keywords)
or BodyContainsWords has_any (Keywords)
or SubjectOrBodyContainsWords has_any (Keywords)
| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
| extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
| summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: OriginatingServer
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIPAddress
version: 1.0.2
kind: NRT
metadata:
source:
kind: Community
author:
name: Pete Bryan
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
| Sentinel Table | Notes |
|---|---|
OfficeActivity | Ensure this data connector is enabled |
Scenario: Scheduled Email Cleanup Job
Description: A legitimate scheduled job runs to clean up old emails in the inbox using a keyword-based filter, such as Microsoft Exchange’s Inbox Rule or PowerShell script that deletes emails with specific keywords.
Filter/Exclusion: Exclude emails where the sender is a system account (e.g., [email protected]) or where the email was processed by a scheduled job (e.g., job_id in the header or a specific X-MS-Exchange-Organization-Message-Id pattern).
Scenario: Admin Task for Email Archiving
Description: An administrator uses a tool like Microsoft Outlook or Exchange Online Archiving to archive old emails containing keywords like “confidential” or “project update”.
Filter/Exclusion: Exclude emails where the sender is an admin account (e.g., [email protected]) or where the email was moved to an archive mailbox (e.g., X-MS-Exchange-Organization-Archive header).
Scenario: Automated Email Response System
Description: A service like Microsoft Flow or Power Automate is configured to automatically respond to or delete emails with specific keywords, such as “unsubscribe” or “confirmation”.
Filter/Exclusion: Exclude emails where the X-MS-Exchange-Organization-Message-Id matches known automation IDs or where the email was processed by a known automation service.
Scenario: User-Driven Inbox Rule for Personal Organization
Description: A user creates a personal inbox rule in Outlook or Exchange to delete emails with keywords like “meeting reminder” or “invoice” to keep their inbox clean.
Filter/Exclusion: Exclude emails where the sender is a user account (e.g., [email protected]) and the email was