A “contract to review” that was an EvilTokens device-code phish, sent from a trusted partner mailbox
A near miss: caught by a user report, never clicked
We worked a phish recently that’s worth sharing for two reasons. First, the delivery came from a legitimate, fully authenticated business-partner mailbox that sailed through every email-authentication and reputation check. Second, the payload wasn’t credential harvesting. It was a Microsoft device-code phish (the EvilTokens kit), which steals OAuth tokens by walking the victim through a real Microsoft login. No password gets captured, and the victim’s MFA is satisfied legitimately.
Nobody clicked, so this is a near miss. The details are still worth passing on, because the control that would have mattered most is not the one most mail stacks lean on.
Processing img i1h28yfl44uh1…
Figure 1. The full chain, from delivery to token theft. The delivery side looks entirely legitimate, which is why sender-reputation filtering waves it through.
How it started
A user reported an email that looked like a routine contract request. The subject was “Contract Document” and the body was a couple of polite lines asking the recipient to review a document and return it signed, with a “REVIEW HERE” link and a legitimate-looking company signature block. Nothing about the wording was unusual, which is the point.
The message came from a real Microsoft 365 mailbox belonging to a business partner we already correspond with. It passed SPF, passed DKIM (signed by the sender’s own domain) and passed DMARC. Our mail security gateway rated the sender a trusted contact and scored the message low across the board, with the BEC and spam models both in single digits. The sending mailbox was almost certainly compromised rather than spoofed. Every signal that sender-reputation filtering relies on said known-good sender, because from the mail plumbing’s perspective it was.
So the catch wasn’t the stack. It was a human who reported it.
The technique: device-code phishing (EvilTokens)
The “REVIEW HERE” link pointed to a page on Cloudflare Workers (*.workers.dev), themed as a “Secure document access” gate. Instead of presenting a fake Microsoft login to harvest a password, the kit starts the Microsoft OAuth 2.0 device authorization grant and gets the victim to approve it on Microsoft’s real infrastructure. Mechanically:
1. The lure page calls the kit’s backend (POST /api/device/start). The backend requests a device code from Microsoft and shows the victim a user code, directing them to microsoft.com/devicelogin.
2. The victim enters the code and signs in on Microsoft’s real pages (the flow lands on login.microsoftonline.com/common/oauth2/deviceauth), including MFA, because it is Microsoft. Nothing looks fake because nothing is fake at that step.
3. Meanwhile the victim’s browser polls the kit (GET /api/device/status/<id>) and the kit’s backend polls Microsoft’s token endpoint. When the victim approves, Microsoft issues access and refresh tokens to the kit.
4. The attacker now holds valid tokens. MFA is already satisfied, no password was ever typed into attacker infrastructure, and a refresh token buys durable access.
Processing img ir91s0gl44uh1…
Figure 2. Who talks to whom. Solid arrows were observed in the sandbox detonation. Dashed arrows are standard behavior of the grant that can’t be observed from outside. The final arrow did not happen in this incident.
Device codes are short-lived (typically about 15 minutes), which limits the window, though not by much for a document someone is already expecting to review.
Why it’s effective: it defeats the two things most users are trained on. There’s no lookalike domain to spot at the Microsoft step, because the sign-in really happens on Microsoft. And MFA doesn’t stop it, because the victim satisfies it themselves. Phishing-resistant MFA doesn’t help either, since the victim is authenticating legitimately to the real service. The sign-in also looks clean on the tenant side unless you’re specifically watching for the device-code grant.
EvilTokens is a phishing-as-a-service kit that automates exactly this against M365 and Entra ID. The /api/device/start plus /api/device/status/<id> pattern is the kit’s own orchestration and a useful behavioral fingerprint, but treat it as low-durability, since endpoint names are trivial for the operator to change.
What we saw on detonation
We detonated the link in a public sandbox (ANY.RUN) rather than clicking it live. It confirmed the family and showed the page’s behavior:
• The browser loads the workers.dev lure host and pulls the “Secure document access” app bundle.
• The page calls /api/device/start, then repeatedly polls /api/device/status/<id>. Those responses were flagged malicious by the sandbox’s feeds.
• The browser is sent into the Microsoft device-authorization flow at login.microsoftonline.com/common/oauth2/deviceauth.
• The sandbox raised “Microsoft OAuth device-code phishing detected”, Suricata phishing signatures and EvilTokens-specific signatures.
Caveat: a sandbox has no real credentials or MFA, so it can’t complete a Microsoft sign-in. Treat the detonation as confirmation of what the page and kit do, not as evidence that a token was issued or that anyone took the bait. What happens on the backend after approval (the kit polling Microsoft’s token endpoint and the token issuance) is how the grant works, not something we observed.
What we didn’t find
This one didn’t get traction, and the negative findings are the reassuring part:
• No clicks. We checked message trace and Security Onion and found no evidence that any recipient followed the link, so no device-code grant was started for one of our accounts. One limit worth stating: network telemetry only sees on-network devices, so a click from a phone or an off-network laptop would not show up there. A query for device-code sign-ins in Entra is what closes that gap.
• No token theft on our side. Because no flow was started by a real user, no access or refresh tokens were issued to the attacker for any of our identities.
• No endpoint execution. This is a browser-and-identity attack. There’s no dropped payload and no script execution, so there was nothing to hunt on the host beyond the browser history that would have existed if someone had clicked.
• Single-message scope. The lure was purged from all mailboxes after we confirmed it hadn’t been actioned.
Total time from delivery to a resolved, contained state was roughly two hours, most of it triage and detonation. The user report landed within minutes of delivery, which is what made the rest easy.
Indicators of Compromise
Attacker-controlled (defanged):
• Lure host: m365039482546-shared-live-document-review-sign.cloud-storage-id039582[.]workers[.]dev
• workers[.]dev is legitimate Cloudflare Workers infrastructure being abused. The attacker-chosen subdomain is the indicator, not the platform.
• Kit endpoints on that host: /api/device/start, /api/device/status/217906
Phishing page artifact hashes. These come from the sandbox report (third-party collection) and represent the page or app bundle, not a native binary. They will differ per deployment, so they have low value for matching. SSDEEP was in the source report but is too short to be useful for fuzzy matching.
• MD5: F1C148975A8D3BA0DB33D21BF5A48B87
• SHA1: 2F587D54E93F7915F6EF3D2143FEF82861796C3A
• SHA256: 7E2F458676684AE4D4658CA7E4566F3F6AFF973A5FFA401347E134597AF7F915
Delivery. The sender was a compromised but legitimate M365 mailbox belonging to a business partner (address withheld, since the mailbox owner is a victim). Full SPF, DKIM and DMARC pass. Worth stating plainly: “sender authenticated cleanly” is not evidence of anything here.
Deliberately omitted: the Cloudflare edge IPs the lure host resolved to. They are shared with unrelated sites and rotate quickly, so blocklisting them would cause collateral damage and catch nothing durable.
Threat family: EvilTokens (PhaaS, device-code phishing against M365 and Entra ID).
Prevention
The strongest control is to stop the flow from working at all:
• Block the device code flow with Entra Conditional Access (the “Authentication flows” condition), and allow it only for the specific users, devices or scenarios that need it, such as CLI tooling and input-constrained devices. Most user populations never need it interactively.
• Treat emailed links to developer-platform hosts (workers.dev, pages.dev and similar) as suspect regardless of sender. Link detonation, URL rewriting and link-host reputation can catch what sender reputation can’t.
• Tell users that a legitimate document link never asks them to type a code into a Microsoft sign-in page.
• Make reporting fast and low-friction. It beat the tooling here.
If someone had clicked
• Revoke the user’s sessions and refresh tokens, and reset credentials as appropriate.
• Review sign-ins for the window: the device-code sign-in itself (from the victim’s own IP) and any later non-interactive sign-ins from new IPs or ASNs.
• Check for new mailbox rules, new OAuth app consents and newly registered MFA methods or devices.
• Notify the partner whose mailbox is compromised, and report the lure host to Cloudflare for takedown.
Things worth hunting for
Structural patterns, not this campaign’s strings:
• Entra ID sign-ins using the device-code grant for users with no operational reason to use it. This is the highest-value, most durable detection for the whole class of attack. The sign-in is performed by the victim, from their normal location, so don’t expect an IP or ASN anomaly on this event.
• A later non-interactive or refresh-token sign-in for the same user from an IP or ASN new to that user, within hours of a device-code sign-in. That is where the attacker’s infrastructure shows up.
• Token grant followed by mailbox-rule creation, OAuth app consent or a new MFA method on the same account. That sequence is what an attacker does with fresh tokens.
• Inbound mail, including from trusted or established senders, with links to developer-platform hosting. Reputation on the sender tells you nothing about the link target.
• The kit’s request shape (low durability): a POST /api/device/start followed by repeated GET /api/device/status/<numeric id> to a non-corporate cloud host, interleaved with calls to the Microsoft device-auth endpoint.
MITRE ATT&CK mapping
Being honest about fit, because device-code phishing doesn’t map perfectly:
• T1566.002, Phishing: Spearphishing Link. Clean fit for delivery.
• T1199, Trusted Relationship. Partial fit. The attacker abused an existing trusted email relationship (a compromised partner mailbox) rather than trusted network access into our environment, which is what T1199 more strictly describes. Closest technique for “leveraged a trusted third party”, but imperfect.
• T1586.002, Compromise Accounts: Email Accounts. The upstream step that gave the attacker the partner mailbox. It happened outside our environment, so we infer it rather than observe it.
• T1528, Steal Application Access Token. Best fit for the objective. The whole point is obtaining OAuth access and refresh tokens.
• T1550.001, Use Alternate Authentication Material: Application Access Token. The follow-on, i.e. how stolen tokens would be used. Not observed here because the flow was never completed by a victim, but it’s where this goes if it lands.
Gap worth flagging: we didn’t find a clean, dedicated ATT&CK technique for phishing the OAuth device-code flow specifically, so it gets split across delivery (T1566.002) and outcome (T1528). If you’re tagging these, expect to use two IDs and a note, and check the current ATT&CK version, since cloud and identity coverage keeps growing.
Detection logic
Pseudocode, adapt to your own stack.
Identity layer (the important one):
# Rule A: any device-code sign-in outside the allowlist
alert when:
entra_signin.authentication_protocol == "deviceCode"
AND user NOT IN {known_devicecode_users_and_service_accounts}
# Do not gate on IP or ASN here. This sign-in comes from the victim,
# from their normal location, not from the attacker.
# Rule B: attacker token use following a device-code sign-in
alert when:
a deviceCode sign-in exists for user U
AND within 24h a non-interactive / refresh-token sign-in exists for U
AND that sign-in's ip or asn is first_seen_for_user
Mail layer:
flag inbound_message when:
sender is external
AND body_links match host_pattern("*.workers.dev" OR "*.pages.dev" OR other dev-platform hosts)
AND (subject/body matches lure_theme("review","sign","secure document","contract"))
# do NOT suppress on sender reputation or SPF/DKIM/DMARC pass
# expected noise: developers and marketers legitimately link to these hosts;
# tune with first-seen link host and lure keywords
Proxy / DNS layer (post-click, low durability):
alert when:
http_request.host matches "*.workers.dev"
AND (uri_path == "/api/device/start" OR uri_path matches "/api/device/status/\d+")
# pair with a hit to login.microsoftonline.com/common/oauth2/deviceauth
# from the same client within a short window for higher confidence
# endpoint names are easy for the operator to change
# without TLS inspection a network sensor (Zeek/Suricata) sees only
# the hostname (DNS/SNI), not the URI path. Then match on the lure
# host plus a login.microsoftonline.com connection from the same client.
Lessons learned
• Email authentication passing is not a safety signal against a compromised legitimate sender. SPF, DKIM, DMARC and a trusted-contact score were all green, and it was still a phish. Sender-reputation filtering can’t catch this class.
• Device-code phishing is an identity-layer problem, not a credential-layer one. No password is stolen and MFA is satisfied legitimately. Block the flow with Conditional Access, and watch for the deviceCode grant in Entra sign-in logs.
• Treat emailed links to developer-platform hosts as suspect regardless of sender. They’re cheap, disposable, TLS-fronted, and increasingly the default home for these kits.
• The human report was the whole game. It beat the tooling by a wide margin and turned a potential token-theft incident into a purge-and-close. Fast, low-friction user reporting is worth investing in.
• Sandbox detonation paid for itself. Rather than clicking live, a public detonation confirmed the family and gave us the kit’s /api/device/* behavioral tell within the hour. It confirms behavior, not impact. And a public submission is public, so check that the URL carries nothing that identifies a recipient first.
Comparing notes
If you’ve seen EvilTokens in the wild, I’d like to compare on a few things: other lure hosts using the same m365…-shared-live-document-review-sign / cloud-storage-id<digits> naming convention on workers[.]dev; any tenant-side telemetry that maps captured grants back to a specific attacker client or app ID (that would make the identity-layer detection much sharper); and whether you’re seeing the same compromised-partner-mailbox delivery pattern versus attacker-registered domains. Happy to share the full sandbox reference if it helps your own hunt.
Source: r/Security_WriteUps · by /u/Rambo_Security
