← Verdict cards Cutsign
Hunt + harden Active campaign pattern BEC / payroll fraud Post-MFA session theft as of 2026-10-23

M365 token spray & replay campaign

Huntress tracked ten weeks (Jul 14 – Sep 22) of AiTM relays and device-code phishing capturing post-MFA M365 sessions, then replaying tokens from 193.26.115.0/24 (1337 Services GmbH, AS210558): 497 successful sign-ins at 79 organizations against 200,000+ failures. Endgame: hidden inbox rules for BEC and payroll fraud — finance-sector shaped.

Verdict: nothing to patch — this is a Conditional Access and identity-hygiene test, and the campaign graded it. MFA alone did not stop it: the tokens were stolen after MFA. What did stop it, outright, was Conditional Access enforced for all users, all cloud apps, all client types — four orgs blocked the replay phase cold. The work is two-sided: hunt the four behavior families (replayed sign-ins, rogue device registrations, MFA wipe-and-replace, hidden inbox rules) and close the CA gaps the campaign selects for. Expect the pattern to recur from new infrastructure — a near-identical June campaign ran from different hosting (LSHIY LLC), so key detections on behavior, not the IP block.

30-second read

Phase 1: AiTM relays and device-code phishing harvest live sessions. Phase 2: token replay from the 1337 Services /24 — sign-ins show OAuth2:Token / Cmsi:Cmsi with node or bare Mozilla/5.0 user agents from hosting space. Phase 3: persistence — at least 48 rogue Entra devices named like defaults (DESKTOP- + 8 uppercase hex), MFA methods wiped and re-registered within minutes in 19 orgs, hidden inbox rules with 1–3-character or punctuation-only names moving mail to RSS Feeds / Conversation History, created via the Outlook REST API. Two sign-in error codes are themselves signal: AADSTS53003 (CA blocked — but the credential is valid) and AADSTS50133 (replay of a revoked session).

Exposed if

  • Conditional Access does not cover all users, all cloud apps, all client types — gaps are exactly what the campaign selects for
  • Device-code flow unrestricted
  • Users reachable by AiTM / device-code phishing (that is: users)

Not exposed if

  • CA enforced for All Users / All Cloud Apps / All Client App types — blocked the replay phase in four victim orgs
  • Phishing-resistant MFA + device-code flow restricted narrows the capture phase too

Detection coverage — as of Oct 23

CategorySourceStatus
PlatformMicrosoft EntraPartial — Entra ID Protection "verified threat actor IP" detections flagged campaign IPs; no dedicated Microsoft advisory found. CA enforcement is the platform-native block
Primary researchHuntressFull hunt package — six published behavior hunts (sign-in shape, device naming, MFA churn, inbox rules, error-code triage) rendering live below; response guidance: revoke sessions, reset passwords, audit devices/MFA/rules
PrecedentHuntress — June campaignPattern recurs — near-identical June 2026 campaign from LSHIY LLC infrastructure; detections keyed to the /24 alone will miss the next wave
VM / EDR—Not applicable — no vulnerability; identity-log hunting and CA policy are the whole game

Do now

  • Verify CA truly covers all users / apps / client types — enumerate exclusions, don't assume
  • Run the four hunts across Jul 14 → now: replayed sign-ins, DESKTOP-[hex8] device registrations, MFA wipe→re-register inside minutes, short/punctuation-named inbox rules
  • Restrict device-code flow where not business-required
  • Triage by credential validity, not spray volume: an AADSTS53003 block still means the password/session was good — reset it

Escalation triggers

  • Any successful sign-in from the campaign infrastructure → incident lane: revoke sessions, reset, audit devices/MFA/rules for that user
  • A rogue device or MFA-churn hit (persistence already in place)
  • Inbox rules keyed on payroll/banking terms → treat as active BEC, involve fraud response
  • New wave from fresh infrastructure (the June precedent says it's when, not if)

Inferred detections — while you wait

Huntress's published hunts (live above) come first; these are the generic-SIEM translations over exported Entra logs:


# sign-in logs
auth_protocol IN (OAuth2:Token, Cmsi:Cmsi)
AND user_agent IN ('node') OR user_agent = 'Mozilla/5.0'   # bare, no platform suffix
AND src.asn in (hosting providers)  → review mailbox + Device Registration Service touches


# audit logs — persistence pair
device registered matching /^(DESKTOP-|Device-|Work Pc)[0-9A-F]{8}$/ from hosting IPs
MFA method deleted AND new method added for same user within 10 minutes


# exchange — hidden rules
inbox rule name matches /^.{1,3}$/ or punctuation-only
AND (moves to 'RSS Feeds'/'RSS Subscriptions'/'Conversation History'
     OR conditions contain payroll/banking terms)

Splunk (index=*) / Elastic ES|QL (FROM logs-*) — Splunk over the Azure Monitor diagnostic export shape (category / properties.*) for Entra and the UAL shape for Exchange; Elastic over its Azure Logs and Microsoft 365 integrations (azure.signinlogs.*, azure.auditlogs.*, o365.audit.*); tune to your add-on or integration:


# replayed sessions: successful sign-ins with the campaign's client shape, Jul 14 → now
index=* category=SignInLogs earliest=-110d@d properties.status.errorCode=0
    (properties.userAgent IN ("node","Mozilla/5.0") OR properties.authenticationProtocol IN ("deviceCode"))
| rename properties.userPrincipalName as user, properties.ipAddress as src, properties.userAgent as ua,
    properties.autonomousSystemNumber as asn, properties.appDisplayName as app
| eval campaign_net=if(cidrmatch("193.26.115.0/24", src), "1337 Services /24", null())
| stats count values(ua) as uas values(asn) as asns values(app) as apps values(campaign_net) as net min(_time) as first by user src

# rogue device registrations from hosting space

index=* category=AuditLogs earliest=-110d@d operationName="Register device"
| rename properties.initiatedBy.user.userPrincipalName as user, properties.initiatedBy.user.ipAddress as src
| regex properties.targetResources{}.displayName="^(DESKTOP-|Device-|Work Pc)[0-9A-F]{8}$"
| stats count values(properties.targetResources{}.displayName) as devices by user src

# MFA wipe-and-replace inside 10 minutes

index=* category=AuditLogs earliest=-110d@d operationName IN ("User deleted security info","User registered security info")
| rename properties.targetResources{}.userPrincipalName as user
| transaction user maxspan=10m | where eventcount>=2 AND searchmatch("deleted") AND searchmatch("registered")
| table _time user duration

# hidden inbox rules (Exchange / UAL)

index=* earliest=-110d@d Operation IN ("New-InboxRule","Set-InboxRule")
| spath Parameters{} output=p | mvexpand p
| rex field=p "\"Name\":\"(?<pname>[^\"]+)\",\"Value\":\"(?<pval>[^\"]*)\""
| eval rule_name=if(pname="Name", pval, null()), folder=if(pname="MoveToFolder", pval, null())
| stats values(rule_name) as names values(folder) as folders by UserId ClientIP
| where match(names, "^.{1,3}$") OR match(names, "^[[:punct:]]+$") OR match(folders, "RSS|Conversation History")


// replayed sessions: successful sign-ins with the campaign's client shape, Jul 14 → now
FROM logs-*
| WHERE @timestamp > NOW() - 110 days AND event.dataset == "azure.signinlogs"
| WHERE azure.signinlogs.properties.status.error_code == 0
    AND (user_agent.original IN ("node", "Mozilla/5.0") OR azure.signinlogs.properties.authentication_protocol == "deviceCode")
| EVAL campaign_net = CASE(CIDR_MATCH(source.ip, "193.26.115.0/24"), "1337 Services /24")
| STATS count = COUNT(*), uas = VALUES(user_agent.original), asns = VALUES(source.as.number), apps = VALUES(azure.signinlogs.properties.app_display_name),
    net = VALUES(campaign_net), first = MIN(@timestamp) BY user.name, source.ip

// rogue device registrations from hosting space

FROM logs-*
| WHERE @timestamp > NOW() - 110 days AND event.dataset == "azure.auditlogs" AND azure.auditlogs.operation_name == "Register device"
| WHERE azure.auditlogs.properties.target_resources.0.display_name RLIKE "(DESKTOP-|Device-|Work Pc)[0-9A-F]{8}"
| STATS count = COUNT(*), devices = VALUES(azure.auditlogs.properties.target_resources.0.display_name)
    BY azure.auditlogs.properties.initiated_by.user.userPrincipalName, azure.auditlogs.properties.initiated_by.user.ipAddress

// MFA wipe-and-replace inside 10 minutes (fixed 10-minute buckets stand in for Splunk's transaction window)

FROM logs-*
| WHERE @timestamp > NOW() - 110 days AND event.dataset == "azure.auditlogs"
    AND azure.auditlogs.operation_name IN ("User deleted security info", "User registered security info")
| EVAL user = azure.auditlogs.properties.target_resources.0.user_principal_name, window = DATE_TRUNC(10 minutes, @timestamp)
| STATS ops = COUNT_DISTINCT(azure.auditlogs.operation_name), first = MIN(@timestamp), last = MAX(@timestamp) BY user, window
| WHERE ops == 2
| KEEP first, last, user

// hidden inbox rules (Exchange / UAL)

FROM logs-*
| WHERE @timestamp > NOW() - 110 days AND event.dataset == "o365.audit" AND event.action IN ("New-InboxRule", "Set-InboxRule")
| EVAL rule_name = o365.audit.Parameters.Name, folder = o365.audit.Parameters.MoveToFolder
| WHERE rule_name RLIKE ".{1,3}" OR rule_name RLIKE "[^A-Za-z0-9 ]+" OR folder RLIKE ".*(RSS|Conversation History).*"
| STATS names = VALUES(rule_name), folders = VALUES(folder) BY user.id, source.ip

Timeline

Sources

Huntress — token replay/spray (primary) · Huntress — June 2026 LSHIY campaign · Help Net Security · Infosecurity Magazine

Public OSINT only — contains no internal data Marcus Liberto · source Lane and triggers per CIRT triage-flow draft Tracked story: Cutsign 10-23 (deep read)