WRAITH: A Zypher-Built Tool for Persistent Microsoft Device Code Phishing via Email Image Defacing
- Tool
- github.com/theemperorspath/wraith
- Built by
- Zypher offensive engineering
- Category
- Custom offensive tool for authorised red team and phishing simulations
- Target
- Microsoft Entra ID / Microsoft 365 accounts
The problem device code phishing has always had
Microsoft's OAuth 2.0 device code flow is a legitimate authorisation grant for input-constrained clients: game consoles, CLI tools, IoT devices. The client asks Microsoft for a device code, the user goes to microsoft.com/devicelogin, enters the code, signs in with their normal credentials, and the client polls a token endpoint until Microsoft hands over an access token plus a refresh token. The tokens issued through this flow are indistinguishable from tokens issued through any other flow. They are scoped to Microsoft Graph, they carry the user's roles, and crucially they are already past MFA. MFA is enforced against the human at devicelogin, not against the client polling for the token. If the human completes MFA, the client wins.
This is the whole basis of device code phishing. An attacker generates a device code against a public Microsoft client ID (Office, Teams, Azure CLI), sends a target an email asking them to sign in at devicelogin with that code, and polls for the resulting token. Because the flow was designed for legitimate clients that cannot host a browser, Microsoft cannot easily block it. If the target enters the code, the attacker walks away with a token that survives MFA, session policies, and most conditional access rules that gate initial sign-in but do not re-check the token afterwards.
There has always been one problem, and it is severe enough that most engagements skip the technique. Device codes expire in fifteen minutes.
Fifteen minutes is nothing on a real corporate mailbox. The target might be in a meeting, at lunch, on the train, or three time zones behind you. They open the email an hour later. The code is dead. Every device code phishing engagement I have watched hits this wall on the first send. The operator then has to choose between two bad options:
- Send another email with a fresh code. Every resend increases the surface area for detection: security awareness reporting, mail filters that spot the repetition, and the target themselves noticing that the "verification" email arrives twice.
- Manually track the target's timezone and try to send within their working hours, gambling on when they will be at their desk. This does not scale beyond one target.
Both options make device code phishing look amateur. Real attackers would not resend an email six times with a different code each time. The technique burns as soon as the operational tempo is wrong.
The spark of the idea, from Truffle Security
The idea for WRAITH came from a Truffle Security paper called Email Graffiti. Truffle noticed that most major email providers pull remote images at view time rather than send time. If you can control what the image URL returns, you can change what the email says, retroactively, forever after the email was sent. They used this to vandalise old emails by claiming abandoned cloud storage buckets that historic emails still referenced. A YouTube notice from 2020 could be repainted years later because the bucket that hosted its header image had been left un-owned when the account was closed. Their framing was retrospective mischief. The interesting part for offense is the mechanism: the image the target sees is decided by the server, at the moment the mail client renders the email.
The moment you internalise that, the device code expiry problem stops looking like a problem. The code does not have to live in the email. The code has to live at the URL the email references. If the code the URL serves is fresh, the email is fresh, no matter how long it has been sitting in the inbox.
What WRAITH does
WRAITH stands for Weaponised Refresh Authentication with Infinite Token Harvesting. It is a Python-based offensive tool we built at Zypher to industrialise the technique. It runs on a VPS with a phishing domain and a valid TLS certificate, and it collapses the whole attack into a single service. In a loop, it does four things:
- Requests a device code from Microsoft against a chosen client ID (Office is the default because it carries the broadest Graph scopes).
- Renders the current code onto a PNG and serves that PNG at a fixed URL such as
https://verify.attacker.tld/code.png, with cache headers set so the target's mail client refetches on every open. - Polls Microsoft's token endpoint every few seconds while the code is valid, waiting for the target to complete the sign-in on the real Microsoft page.
- When the code expires without a token, it requests a new one and repaints the image. The URL never changes. Any open of the original email fetches the new PNG and shows the fresh code.
One email carries one image URL. That URL, until the target signs in, keeps a live valid device code painted on it. Whether the target opens the email fifteen minutes after send or two days after send, the code they see is one that was minted in the last few minutes and is still valid for the next fifteen. The operator does not resend anything. The infrastructure runs by itself.
Both screenshots above are the same email at the same URL. The only thing that changed was time.
The full attack chain
End to end, a WRAITH engagement runs like this. The operator prepares the phishing domain and TLS material. WRAITH's setup wizard handles most of it, including certbot invocation and config generation. Then the attack launches:
- Domain spoofing. WRAITH ships with SMTP tooling to send a single HTML email that appears to come from an internal address at the target company. Domains with DMARC set to
p=noneorp=quarantineare the sweet spot; strict DMARC installs get pre-flighted and the operator is warned before send. - Email template. The email is deliberately boring. Options ship for a colleague asking the team to verify their account, an IT security policy update with mild urgency, and a Microsoft session-expired notification. The verification code lives inside the email as an
<img>tag pointing at WRAITH's image URL. No JavaScript. No unusual attachments. Nothing a modern mail filter will flag. - Landing page. The email links the target to a WRAITH-hosted
/verifypage that mirrors the Microsoft device login prompt and shows the current code. This is optional: the operator can also link straight to the realmicrosoft.com/devicelogin. Both flows land the same tokens; the fake landing page gives the operator visibility into interaction events (image loads, clicks) before the target reaches Microsoft. - Server-side dynamism. WRAITH mints device codes on demand, renders them onto PNGs with the correct anti-cache headers, and rotates them automatically as they expire. Each image load is logged with IP and User-Agent so the operator can see when the target actually opened the email.
- Token capture. When the target completes the sign-in at
devicelogin, Microsoft returns an access token and refresh token to whatever client is polling. That client is WRAITH. Tokens land intoken_output.jsonand the polling loop shuts down. - Post-compromise enumeration. WRAITH runs an automated Graph API sweep the moment a token drops: users, groups, mailboxes, Teams chats, OneDrive files, directory roles, mail rules, and OAuth apps. Everything is written to
token_output_enum.json. The refresh token is stored separately so the operator can maintain access after the initial access token expires.
Why the image-defacing approach beats the alternatives
There are two obvious ways to keep a phishing artifact fresh. WRAITH uses images. Both alternatives have a shorter half-life.
Server-side redirect on a link
The naive answer is to make the phishing email contain a link to a WRAITH-hosted page, and let that page display the current code. The moment the target clicks, they hit a page with fresh content. This works, but it moves the interaction from a passive read to a deliberate click. It also puts the operator's phishing domain into the browser's URL bar, where the target and their AV will look at it more carefully. The email itself carries a URL that looks like a redirect, which is exactly what mail filters heuristically distrust.
The image approach keeps the interaction shape identical to a legitimate verification email. The code appears to be part of the message. The target never navigates to the operator's domain to see it.
Rewriting the email in place
IMAP-authenticated mail rewriting is possible but requires prior access, which is what device code phishing is supposed to be building toward. It is not a starting primitive.
Dynamic images sit in the sweet spot: no client-side interaction to trigger the refresh, no prior access required, no visible signal to the target that anything is happening between opens.
Anti-cache is load-bearing
The mechanism only works if the target's mail client fetches the image again on every open rather than serving the copy it grabbed the first time. WRAITH sets three headers on the PNG response and it is worth being explicit about them:
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Pragma: no-cache
Expires: 0
All three matter. Cache-Control is what modern HTTP intermediaries respect. Pragma is what older HTTP/1.0 caches respect. Expires: 0 is what a subset of email clients and mid-path proxies key off. In practice, Outlook and Apple Mail obey Cache-Control; a handful of niche webmail proxies only pay attention to Expires. Setting all three is cheap insurance.
Gmail is a special case. Google proxies remote images through their own image server (ggpht.com), which caches aggressively and does not respect Cache-Control: no-store on the origin. There are two ways around this. First, most corporate mailboxes on Google Workspace can be configured to not proxy images from allow-listed senders; if the domain looks internal enough, this can be enough. Second, appending a rotating query string to the image URL in the email HTML forces Gmail to see a new URL and refetch. WRAITH supports both by convention: pick the operator posture that matches the target's mail platform.
Field results
WRAITH has been part of Zypher phishing simulations for multiple client engagements including GetConnects. The single most useful result across those engagements was not the raw token capture rate; it was the delta between what the operator could measure with static device code phishing versus with WRAITH. On the static tooling we had used previously, the median open-to-code-entry delay put more than half of opens outside the 15-minute window. On WRAITH the same distribution of open times converted almost end to end, because "outside the window" stopped being a state that existed. In the engagements where token capture was in scope, WRAITH moved device code phishing from a technique we only ran against pilot targets to one we could confidently run against the full simulation population.
Detection during those engagements was low. The email itself contains no obvious indicators: no attachments, no JavaScript, no URL that looks like a redirect, no repeated sends. The image renders as a static verification code from the target's point of view. The verification code they see is a real code, minted by Microsoft, valid for the standard 15 minutes. Even manual inspection of the email HTML shows only a normal <img> reference to an HTTPS URL on a domain that resembles Microsoft's login infrastructure.
Post-compromise, briefly
The tokens WRAITH returns are ordinary Graph API tokens. Everything that Microsoft Graph exposes to the compromised user is reachable. In practice the enumeration script pulls:
- User directory and group memberships, including nested membership.
- Mailboxes and message search: replies to previous emails, calendar entries, contacts.
- OneDrive and SharePoint documents at the user's access level.
- Teams chats and channel memberships.
- Directory roles the user holds (Global Administrator, Application Administrator, and so on).
- Mail-forwarding rules and OAuth applications previously consented to by the user (a common route into persistence).
The refresh token is the more important artifact. Access tokens are short-lived. Refresh tokens live for up to 90 days by default and can be silently exchanged for new access tokens without any further user interaction. WRAITH has a --refresh mode that takes a saved token file and mints a fresh access token from the stored refresh token, so the operator can pick up an engagement days later without the target ever knowing.
Detection and defense
The defensive side is where this writeup is genuinely useful, because most of the recommendations are cheap and none of them are exotic.
- Disable the device code flow at the tenant level unless it is required. The vast majority of Entra tenants do not have any legitimate client that needs it. Conditional Access can block the device code grant type entirely. This is the single most effective control and it has essentially no downside.
- If the device code flow is required, gate it behind Conditional Access location and device policy. Restrict which users can complete a device code sign-in, and require a compliant device. This makes the flow unattractive as a phishing surface even if it stays enabled.
- Alert on unusual device code sign-ins. The sign-in log records
authenticationProtocol: deviceCode. In a mature tenant, the daily count of legitimate device code sign-ins per user is usually zero. Any non-zero count is worth reviewing. - Configure DMARC strictly. Domains at
p=quarantineorp=noneallow the email spoofing that makes the initial phish look internal.p=rejectwith a policy that covers subdomains removes that primitive from an external attacker. - Train the user on device codes specifically. Most security awareness programmes teach users to look at URLs and attachments. Device code phishing has neither. The correct rule is: if you did not just deliberately try to sign in on a second device, never enter a device code you received in an email.
- Watch for image-refresh patterns on your mail security appliance. A single message with a hotlinked image that returns wildly different content on repeated GETs is not a normal shape. It is not conclusive, but it is instrumentable.
Refresh tokens deserve their own note. Even after the initial compromise, tenant-level revocation of refresh tokens is available and instant. If an operator's activity is caught (or if the user reports the phishing email after the fact), revoking refresh tokens for the user closes the persistence window entirely.
What WRAITH is not
WRAITH is not a novel vulnerability. Device code phishing is a well-known technique. The image-defacing trick is not new either; Truffle's Email Graffiti described the mechanism at large, and dynamic email images predate that paper. What WRAITH is, is the Zypher-built composition of the two. Nobody had assembled them into a persistent device code phishing rig you can operate as a single tool. That composition is what makes the technique viable on real engagements at real target volumes, and it is why we shipped it as open-source tooling rather than keeping it internal.
It is also not a general-purpose OAuth phishing framework. It targets Microsoft's device code flow specifically. Google, Okta, and other IdPs have their own equivalents; the same email image-defacing trick would in principle apply, but the concrete implementation lives in WRAITH's Microsoft-shaped repo for now.