
If you are in a hurry: the control that stops this is not the spam filter or MFA, it is DMARC at p=quarantine, and it is at the end. What is in between is why the other two are not enough, and that is the part worth reading, because this kind of attack is going to keep coming.
The same email, two verdicts
It starts with an observation that does not add up. Two phishing emails, identical, word for word, sent from the same IP address to the same victim. Seven hours apart. One landed in quarantine. The other was delivered to the inbox.
Nothing in the mail setup changed between them. Same filter, same rules, same licenses. The only thing that changed was one link inside the body of the message. And that detail, a single link, is the whole story.
I saw this reviewing the Microsoft 365 tenant of a mid-sized organization. I have stripped out the names, the domains and the internal addresses: what is left is the mechanics, which is what repeats from one place to the next.
The email was a payroll classic. The subject, reconstructed and without the real name, was: «lMPORTANT: [Employee] Payroll – Document Require Your Signature».
Look at the first word. It does not say «IMPORTANT» with a capital I. It says «lMPORTANT» with a lowercase L. In most email client fonts, a lowercase «l» and a capital «I» are drawn almost the same. It is a homoglyph, and it is deliberate: it helps dodge rules that look for the exact word and, on top of that, it exploits the fact that the eye reads the shape, not the letters.
But the homoglyph is not the interesting part. The interesting part is the authentication headers, because both copies of the email carried them identical:
Authentication headers, identical in both copies:
spf=fail (sender IP is 45.xx.xx.xx) smtp.mailfrom=[domain];
dkim=none (message not signed);
dmarc=fail action=none header.from=[domain];
Translated: the email claimed to come from the company’s own domain (an employee writing to themselves), but it left from an IP the domain does not authorize, unsigned, and it failed the DMARC check. Both copies. And still, one got in.
What decided which one got through

Here they are, side by side, exactly as Microsoft itself tagged them in the X-Forefront-Antispam-Report header:
| Time (UTC) | Link in the body | Verdict | Result |
|---|---|---|---|
| 16:48 | [...].web.app/[...] (Firebase, raw) | SCL:9 SFV:SPM CAT:HPHISH | quarantine |
| 23:35 | links.notification.intuit.com/ss/c/[...] | SCL:1 SFV:NSPM CAT:NONE | delivered |
SCL is the Spam Confidence Level, from 0 to 9. A 9 means «this is high-confidence phishing, get it out of the way». A 1 means «this is clean». The same email, with the same spoofed sender, with the same authentication failure, went from a 9 to a 1 by changing where the button pointed.
In the first copy, the «Review & Sign Document» button led to a raw URL hosted on Firebase. Free web.app domains carry accumulated bad reputation, and the content classifier penalizes it. Nine.
In the second, the attacker did not change the final destination. They wrapped it. The button pointed at links.notification.intuit.com, the tracking redirector Intuit (the QuickBooks and TurboTax people) uses to measure clicks in their own legitimate mail. It is a domain with excellent reputation. The classifier sees «Intuit» and gives it a thumbs up. One.
It is an open redirect abuse. Intuit’s tracking service accepts forwarding to wherever you ask and does not check where. The attacker puts their phishing link inside Intuit’s tracker, and inherits Intuit’s reputation for the ride. The filter does not judge intent, it judges the reputation of the first stop. And the first stop was spotless.
A detail that tends to surprise: this email was inside Safe Links, Microsoft’s URL-rewriting protection. It did not stop it. Safe Links rewrites the link to check it at click time, but Intuit’s redirector was, in effect, a trusted domain, so there was nothing to flag in the rewrite. The malicious chain lived on the other side of the redirector, out of its sight.
What was behind the link
So far it is an email. To find out what it was really after, you have to follow the link, and that is not done from your machine or your network. It is done in an isolated environment. I use any.run, a sandbox that loads the page inside a disposable machine and lets you see what happens without anything touching your system.
The final destination, after passing through Intuit’s redirector, was a three-step chain.
Step one. A domain on the .vu TLD (Vanuatu’s country code, cheap and lightly policed). Before the page even loaded, Cloudflare already put up its own screen: «Suspected Phishing», with an «Ignore & Proceed» button. Worth stopping here: even the infrastructure serving the page had flagged it. The victim who gets this far has had to click «ignore and proceed» over a red phishing warning. They do it. The urgency of payroll beats a warning that goes unread.
Step two. A fake captcha. «Security Verification / Confirm Access», with a countdown of 01:55. The captcha serves two purposes: it looks like a legitimate security step, and it slows down automated analysis systems. But the counter is the clue to what comes next. One minute and 55 seconds is not a captcha time. It is a countdown for something else.
Step three. The final page. It presented itself as a Microsoft Teams voicemail, «New voicemail from HR», with a «Verify your Microsoft identity to play the recorded message» message. And in the center, large, a verification code with two instructions: copy it, and paste it after clicking «Sign in with Microsoft».

That code is the whole trap. And to understand why, you have to understand what it is.
Why MFA does not help here
The instinctive reaction to any phishing is «well, we have MFA, if they steal the password they still cannot get in». With this attack, MFA does not save you. And the reason is that the attacker is not after your password.
That code on the screen is a real device code, generated by the attacker against Microsoft’s genuine endpoint, seconds before showing it to you.
The device code flow exists for legitimate cases: signing in on gadgets without a comfortable keyboard, like a smart TV or a console. The device shows you a short code and tells you «go to microsoft.com/devicelogin on your phone and type this code». You type it, you authenticate on your phone as normal, and the device gets authorized. Microsoft designed this on purpose to separate the screen where you see the code from the screen where you authenticate.

The attacker hijacks that mechanism. The sequence is this:
- The attacker asks Microsoft for a device code. Microsoft gives it to them, with its lifetime window (the famous ~15 minutes, although the fake counter said 01:55 to create urgency).
- The attacker shows you that code on their fake page.
- You, believing you are «verifying your identity», copy the code and click «Sign in with Microsoft», which takes you to the real Microsoft page.
- There you type your username, your password, and you approve your MFA. All legitimate. All on the real microsoft.com. Your password manager does not object, because the domain is correct. Your authenticator app approves, because it is you.
- When you approve, you are not authorizing your smart TV. You are authorizing the attacker’s session. Microsoft hands the access token to them.
The attacker gets in with a valid token, issued by Microsoft, backed by your MFA. There is no password to steal because they do not need it. There is no second factor to beat because you approved it for them.
That is why the 01:55 counter. The device code expires after fifteen minutes, and the attacker needs you to use it before then. The counter squeezes far harder than it needs to, so you do not stop to think, but the rush is not just theater: there is a real expiry behind it.
And this is neither an old trick nor an isolated case. On 6 April 2026, Microsoft Defender’s research team published the analysis of a large-scale campaign of exactly this kind, which Microsoft links to EvilTokens, a phishing kit sold as a service. The twist had to do with that very counter: to dodge the fifteen-minute expiry, the campaign did not generate the code in advance, but at the moment the victim clicked. And the emails were written with AI, tailored to each victim’s role: tenders, invoices, manufacturing workflows. Microsoft’s recommendation in that same post is the one in the next section: block the device code flow where it is not needed.
This is the uncomfortable part for anyone who has sold MFA as the solution. Microsoft reckons MFA cuts the risk of account compromise by more than 99%, and that holds for the phishing that steals credentials. This attack lives in the remaining 1%, because it does not steal credentials. It asks you to use yours, correctly, to authorize someone else.
How to know if anyone fell for it
A delivered email is not a breach. Between the email arriving and someone pasting the code there are several «ifs». The question that really matters, and the one you can answer yourself, is: did any device code sign-in complete?
In Microsoft 365 this is checked in the sign-in logs. If you have Graph access, the direct query is this:
Connect-MgGraph -Scopes "AuditLog.Read.All","Directory.Read.All" -NoWelcome
$from = (Get-Date).AddDays(-16).ToString('yyyy-MM-ddTHH:mm:ssZ')
$uri = "https://graph.microsoft.com/beta/auditLogs/signIns" +
"?`$filter=createdDateTime ge $from and authenticationProtocol eq 'deviceCode'"
$r = Invoke-MgGraphRequest -Method GET -Uri $uri
@($r.value).Count
What you are looking for is authenticationProtocol eq 'deviceCode'. In a normal tenant, where nobody uses corporate smart TVs, this number should be zero. Any result is, at the very least, something to look at closely: who, from what IP, against what application.
In the case behind this article, the result was zero over sixteen days. The email got in, but the attack did not complete. Nobody pasted the code. And that check, that zero, is what turns a scare into a closed incident.
It is worth leaving as a permanent control. A device code sign-in in a company that does not use the flow for anything legitimate is a signal so clean it has almost no false positives. If your SIEM or your tenant lets you alert on that field, it is one of the cheapest and most rewarding you can set up.
And one step further, because detecting closes the incident but does not remove the door: if your organization does not use the device code flow at all, block it. Since 2025, Entra ID Conditional Access has an authentication flows condition that cuts the device code flow off at the root (Conditions → Authentication flows → Device code flow → Block). After the STORM-2372 campaign of February 2025, Microsoft added to its managed policies one called, literally, «Block device code flow». It shows up in your tenant in report-only mode and, if you leave it, Microsoft turns it on after at least 30 days. It is worth going into Conditional Access to check whether you already have it. Mind the exceptions: blocking it bluntly can interfere with device registration and with Microsoft Teams Rooms devices, which do use this flow (Microsoft has a guide to exclude them without opening the door to the rest). Deploy it in report-only mode first and see who you touch before you enforce it.
What does not fix it
Before getting to the control that does stop this, three that do not, because they are exactly the three people assume are enough.
Blocking the attacker’s domains. It is the instinct: the IOC comes out, you block it. It works for yesterday’s email. Today’s .vu domain costs the attacker a few dollars and ten minutes, and tomorrow they use another. Blocking domains is cleanup, not defense. Necessary, but you are always behind.
Trusting the content filter. We have already seen it get bypassed with a single link change. The filter scores reputation, and reputation can be borrowed. Any defense that depends on the attacker using bad-reputation infrastructure falls the day the attacker uses Intuit’s, or Google’s, or yours.
MFA. For everything in the previous section. It is essential, but against this specific attack it does not intervene.
The three have something in common: they all act after accepting that the email is plausible. The filter decides whether the content smells bad. MFA decides whether the one authenticating is you. Neither asks the earlier question: does this email actually come from who it claims to?
What does: DMARC, at the authentication layer
Go back to the headers from the start:
The headers, again:
spf=fail; dkim=none; dmarc=fail action=none header.from=[domain]
dmarc=fail. Both emails, the blocked one and the delivered one, failed DMARC. An employee does not write to themselves from a foreign IP unsigned. It is an exact forgery of the domain’s own name, and DMARC is made precisely for that.
The key is in action=none. The domain’s DMARC policy was at p=none, which means «detect the failure, but do nothing, just report it». The email failed, the system watched it fail, and let it through anyway, because that is what p=none orders.
The fix lives in one line of DNS, the domain’s DMARC record policy. But careful, it is not a switch you flip from p=none to blocking and done. It is a three-rung journey, and each rung is climbed by reading the reports DMARC itself sends you:
p=none. Does nothing: it only observes and sends you reports of who sends mail in your name. It is where everyone starts, and where almost everyone stays.p=quarantine. What fails authentication goes to the junk folder. This is where the email in this case would already have been stopped.p=reject. What fails is rejected at the door, it never reaches the mailbox. It is the final destination, and the one you want to reach.
A record on the quarantine rung, with the reports already turned on, looks like this:
v=DMARC1; p=quarantine; sp=quarantine; rua=mailto:[...]; ruf=mailto:[...]; fo=1
And here is why this works when the filter does not: DMARC judges authentication, not content, and its verdict overrules the classifier’s. The 23:35 email got through because the classifier gave it an SCL:1. With p=quarantine, that SCL:1 would not matter: the email fails DMARC alignment, and a quarantine policy sets it aside for that failure, no matter how clean the filter thinks the content is. The authentication layer does not vote alongside the filter, it overrides it.
It is the difference between a doorman who checks whether your face looks shifty (the filter) and one who checks whether your ID is genuine (DMARC). The second one does not care about your face.
A couple of warnings, because p=quarantine is not a switch you flip lightly:
- You are going to block things of yours you did not know existed. Any system that sends mail «from» your domain without being in your SPF (a CRM, a printer, a billing service, an internal subdomain someone set up years ago) will start falling into quarantine. Before changing anything, you have to read the DMARC aggregate reports (the
rua) for weeks and inventory who legitimately sends in your name. Climbing a rung on day one is the best way to block your own payroll: first you read the reports for weeks, and only then you tighten. - The report signal arrives late. An email is evaluated when it arrives, not when the report is issued, and aggregate reports run a day or two behind and in daily batches. After changing the policy, do not draw conclusions the same day. The first real proof that it bites arrives days later.
And do not stop at p=quarantine. Quarantine already bites, but it leaves the bad mail in a folder from where someone can still rescue it by mistake. When the reports confirm, weeks in a row, that nothing legitimate of yours is falling there, climb to the last rung, p=reject: that is the end of the journey, and it is those same reports that tell you when you are ready to take it.
The proof it worked is not in the reports
When p=quarantine was set on the domain in this case, the confirmation that it had worked was not a percentage on a dashboard. It was the attacker’s behavior.
Four days after tightening the policy, the self-impersonations of that domain against the victim stopped dead. Zero. And there appeared, almost the same week, emails with the same pattern but impersonating a sibling domain of the same organization, one that was still at p=none.
The attacker moved. They did not abandon the victim, they changed doors. That move is the best possible confirmation that the first door had really closed. If the old domain still worked for them, they would not have bothered switching. A percentage in a report tells you the policy is applied; the attacker packing up and going elsewhere tells you it hurts.
The alert the filter ate
One last detail, because it is the one least told and the one that stings the most.
People reported the email. One of the employees who received it forwarded did the right thing: they forwarded it to several colleagues and to the internal support address to warn them. They reported well, on time, exactly as you ask them to in every awareness training.
The warning did not reach support. Why? Because the warning email contained, quoted, the original phishing email. The filter classified it again as phishing (which it was) and sent it to quarantine. The user’s report was eaten by the same filter that had not stopped the original.
The lesson is uncomfortable: if your only channel for people to report phishing is «forward it to support», that channel goes through the content filter, and the filter does not tell «I am forwarding you a phishing to warn you» apart from «I am sending you a phishing». The legitimate report and the attack are, byte for byte, almost the same email.
The way out is a channel that does not go through the content filter: Outlook’s native «Report» button, tied to a report-submission policy that delivers the sample to an internal mailbox in addition to Microsoft. That path is not reclassified. Without it, your most responsible users are warning you at an address that never receives the warning, and neither they nor you know it.
What is left when this campaign is forgotten
The specific .vu domain no longer matters. It will be dead in weeks. What stays is the shape of the attack, and that is going to repeat.
Three things to take away:
The filter scores reputation, not intent, and reputation is for rent. Any defense that depends on the bad guy using bad-reputation infrastructure has an expiry date. The jump from SCL:9 to SCL:1 with a single link change is all the proof you need.
MFA solves credential theft, not session handover. Device code phishing, and its cousin AiTM with a reverse proxy, do not steal your password: they get you to authorize the attacker with your own credentials, correctly. Against that, what helps is conditional access that ties the token to a known device and an expected location, and limiting the device code flow to the few cases that genuinely use it.
The only layer that asks the right question is authentication. SPF, DKIM and DMARC do not look at the content. They look at whether the email comes from who it claims. For the exact forgery of your own domain, DMARC at p=quarantine or p=reject is not one more layer: it is the only one that cuts the attack before the content even gets a vote.
And a fourth, more fundamental one: when an email gets through, the question is not «how do I improve the filter?». The filter will always be a step behind the ingenuity of whoever sends. The question is «what layer would have stopped this without depending on being right about the content?». Almost always, the answer is in authentication, and almost always it was there, switched off, waiting for someone to turn it on.
I wrote this after running into it reviewing a Microsoft 365 tenant. The data is anonymized on purpose: the value is in the mechanics, not the target. And p=none is not some rare oversight: it is the default state most domains sit at, because finishing the DMARC journey is work and almost nobody finishes it. If you run Microsoft 365 and still have any domain there, that is the one they will use.
What to do this week
- Check whether anyone already fell for it. Run the Graph query above and leave it as an alert: in a company that does not use the device code flow, any result deserves a look.
- Look for «Block device code flow» in Conditional Access. If Microsoft already created it for you, review who it touches and turn it on. If not, create it in report-only mode, exclude what genuinely uses it (Teams Rooms), and enforce it afterwards.
- Check which DMARC rung each of your domains is on, including the ones you do not send mail from. If any is still at
p=none, start reading itsruareports this week. - Give your users a reporting channel that does not go through the filter: Outlook’s «Report» button with a report-submission policy to an internal mailbox. And stop asking them to forward phishing to support.
More from IT Rafa
- Transactional email: why your own server does not deliver, and how to align SPF, DKIM and DMARC without breaking your legitimate mail
- Check if your email is in a data breach, the blog’s tool