A manager at a company I handle security for got an email from someone he actually knew — a staff member at a local school he’d corresponded with before, sending over a bid proposal. The email came from that person’s real account, through the school’s real mail servers. It passed SPF, DKIM, and DMARC. There was no lookalike domain, no urgency, no broken English. It arrived inside an existing thread and was signed with the sender’s real title and real phone number.

Eleven minutes later, someone else was logged into the manager’s account. He had multi-factor authentication on. He approved the prompt himself.

This is the part of the write-up where a lot of incident reports stop — remediate, rotate, move on. I didn’t want to stop, because the how here breaks a detection model most of us still rely on, and because the kit behind it turned out to be a commercial product with a fleet of customers. So I took it apart. What follows is the mechanism, the method I used to recover the kit’s source without ever running it, and what the people behind it actually look like once you get inside.

Identifying details — the people, the organizations, the specific infrastructure — are changed or withheld. The technical content is exactly as I found it.

Why MFA was never going to help

The thing on the other end of that link was not a fake Google login page. It was a live video of a real browser, running on a machine that belonged to the attacker, already sitting on the genuine accounts.google.com. The manager was looking at a picture of someone else’s screen, streamed to him in real time on a canvas element. When he typed his email, those keystrokes travelled over a WebSocket and were typed into that browser. When he typed his password, same thing. When Google sent an MFA prompt, it went to his real phone — because the real Google had received a real login attempt from the attacker’s browser — and he approved it.

Google saw an approved login and issued a session to the browser that asked for it. That browser was on the attacker’s server. Two seconds later, the attacker’s own device completed a Device Bound Session Credentials binding — cryptographically tying that freshly minted session to a machine two thousand miles from the victim.

Researchers call this Browser-in-the-Middle (BitM), and it is not the same thing as the reverse-proxy phishing (AiTM) that most detection is tuned for. The distinction is the whole story:

Sit with the consequence: every session-anomaly detection you have will come back clean, and that cleanliness proves nothing. Impossible travel: clean. Cookie reuse across ASNs: clean. What you are actually looking for is a successful, fully-authenticated, MFA-satisfied login that happens to originate from attacker infrastructure at the moment of the phish — an event that looks exactly like a legitimate login, because in every way the identity provider can measure, it was one.

I confirmed the compromise from the tenant’s own login-audit export. Two successful logins inside the eleven-minute window, both flagged “suspicious” by the provider, both satisfying the MFA challenge, each followed within two seconds by a device-binding event. The attacker held that authenticated, device-bound session for about three hours — and, notably, did nothing with it before session revocation and a password rotation cut it off. No mailbox rules, no recovery-email swap, no OAuth grant. Smash-and-grab that never got to the grab.

One more detail that matters for your runbook: the attacker’s login IPs were residential, on a consumer ISP, not the kit’s hosting provider. They egress through residential proxies precisely so that “block/alert on the hosting ASN” fails. If your BitM detection plan is an ASN blocklist, it would have missed this cleanly.

The delivery chain was built, at every hop, to avoid ever presenting something that a scanner could classify:

  1. The lure was a PDF with no active content. ReportLab-generated, 4 KB, two link annotations pointing at a bare URL. Zero JavaScript, no /OpenAction, no embedded files, no forms. Nothing for a PDF sandbox to detonate. The email body itself contained no links at all — the attachment was the entire vector.
  2. The first hop was a free site-builder page — a static, benign, entirely real page on a mainstream hosting service, there purely as a credibility prop. Nothing malicious to detect because there was nothing malicious on it.
  3. The second hop was a serverless “gate” — a cloud edge function — that made a server-side decision about what to serve you.
  4. The payload was never delivered at a URL. Depending on the gate’s decision, the phishing client was written into the page with document.write() (so it renders at the gate’s own URL, with no navigation event) or handed to the browser as a blob: URL generated on the fly. A blob: URL is local to your browser session; it cannot be scanned ahead of time, submitted to a blocklist, or blocked by a proxy.

That is the sentence to internalize: there is no URL to classify, so Safe Browsing has nothing to classify. The reputation-based half of your defenses is simply not in the game.

Wrapped around all of this was a serious anti-analysis stack, verified in the recovered source: a 60-second time-to-live on the bootstrap token so slow manual analysis auto-fails; an infinite debugger loop; console.* nulled out; a devtools timing detector that wipes the DOM to about:blank if it thinks it’s being inspected; property-getter tripwires; right-click and F12/Ctrl+Shift+I/J/C/Ctrl+U all trapped; no-referrer and noindex meta tags. And the fake page disguised itself as a Cloudflare “Just a moment…” Turnstile challenge — complete with a fabricated Ray ID and a beacon to the real Cloudflare analytics endpoint — so that a moment of friction reads as legitimate bot-protection rather than a phish.

Taking it apart without detonating it

Rule one when the target is live phishing infrastructure: do not open it in a browser that holds anything valuable. My everyday Chrome profile carries live super-admin cookies for the tenant that just got hit. Detonating an unknown remote-browser kit in that profile is how you turn an investigation into a second incident. Everything below happened over Tor, with curl, in a context that had nothing to lose.

The interesting part is that I never needed to run the kit at all. The gate is, functionally, an encrypted decision oracle: you send it a request describing a visitor, and it returns the payload it would serve that visitor. So I stopped treating it as a website and started treating it as an API.

The gate’s crypto was recoverable straight from its own served JavaScript, under the obfuscation:

So I reimplemented it — maybe forty lines of Python wrapping openssl — to do exactly what the kit does: fetch the gate over Tor to pick up a fresh 60-second nonce, encrypt a well-formed decision request with the recovered key, POST it to the backend’s /create endpoint, and decrypt the reply. No browser. No credentials. The oracle handed back the full 227 KB phishing client on request.

# The whole trick, stripped to its shape. KEY_PASS lifted from the gate's own JS.
KEY = hashlib.sha256(KEY_PASS.encode()).digest()

def ask(email, nonce):
    payload = {"hash": nonce, "ttl": 60, "e": email, ...}   # request the kit would make
    blob = aes_cbc_encrypt(KEY, json.dumps(payload))         # encrypt it their way
    reply = post(f"{BACKEND}/create", data=blob, proxy=TOR)  # let the server decide
    return aes_cbc_decrypt(KEY, reply)                       # read what it would serve

Recovering a kit by replaying its own decision API is worth adding to your toolbox. It’s faster than dynamic analysis, it never executes attacker code in your environment, and it gets you the server-selected payload — the thing the operator actually serves real victims — rather than whatever a sandbox happens to trip.

The passcode I pulled out of their own code

Inside that recovered payload was the campaign configuration. It was “encrypted,” but only in the loosest sense: it was XOR’d against a string literal sitting in the same file. The whole scheme:

config = base64url_decode(blob)
plaintext = "".join(chr(c ^ SECRET[i % len(SECRET)]) for i, c in enumerate(config))

SECRET was right there in the source. Decode it and the config falls open into a pipe-delimited record — I’ll show the schema, not the values:

email | flags | server_url | allowed_links | admin_password

server_url was the WebSocket relay the victim’s stolen keystrokes were streaming to — the command-and-control. And admin_password was exactly what it looks like: the operator passcode. In this kit, that isn’t a login to some separate backend panel; it’s a backdoor built into the fake page itself. An operator types the passcode into the phishing login and it flips the page out of victim mode and into operator mode, letting them watch or drive the victim’s remote browser session live.

I derived it in one shot, from the attacker’s own code. I did not use it. Typing it against their infrastructure would be unauthorized access — the exact line that separates “security research” from “the thing I’m investigating them for” — and it would have contaminated a package I might hand to an abuse desk or law enforcement. The finding is that the passcode was derivable at all: they shipped the key and the ciphertext in the same file they hand to every victim.

Sophisticated kit, amateur operator

This is where “the people behind it” gets interesting, because there are two very different skill levels stacked on top of each other.

The kit is genuinely good. Multi-tenant, with per-campaign configuration blobs. Server-side selection of which brand to impersonate. An operator control panel with admin and scoped-admin roles. Password-manager detection, to catch the classic BitM tell where a victim’s autofill silently fails on the wrong origin. A double-buffered canvas renderer, bidirectional clipboard sync, multi-tab remote browsing. And a complete anti-analysis suite. Someone competent built this.

The operator running this particular campaign was not that someone. The server hosting the relay was a stock rented Windows box in a Los Angeles datacenter that still had its factory default hostname and RDP wide open to the public internet. The payload shipped with a plaintext API token in it. A safety guard in the code had been disabled by wrapping it in if (true). The config was left hardcoded in the served file — which is how I read it.

Those two things don’t normally coexist in one person. They coexist very naturally when the sophistication is the product and the operator is a customer. This is Phishing-as-a-Service.

I didn’t have to infer that, because one customer build shipped with the kit author’s own developer comments intact — including a documented “captcha contract” describing hooks that “the user” is expected to call, with a base template that “handles everything post-solve.” The audience for that documentation is the paying customer who supplies their own front-end and calls the platform’s API. It’s a product manual. There’s a base template, a generator that injects customer overrides into it, and a documented extension surface. Someone is selling this.

It’s a fleet, not a campaign

Once you have one relay, you can fingerprint the rest. The relay hid its real server behind a decoy 404 page pretending to be a different web server entirely — but that decoy page has a fixed byte-for-byte body, and a body hash is a pivot. Pivoting on it, and on the kit’s very specific naming convention for its serverless gates, surfaced the operation’s actual footprint: on the order of eighteen separate cloud accounts, multiple live gates and live backends, running the identical kit from spring through late summer. One dedicated relay server per campaign. Every lure themed as a procurement or bid-proposal invitation — including one that targeted a university.

And they left a structural weakness that is a gift to defenders: the kit’s encryption key is a constant across every deployment. The author rotates only an 8-character prefix between builds and keeps a fixed 24-character tail. One recovered key decrypts the /create channel of every contemporaneous deployment of this kit. It’s a single point of failure in their design and a durable detection asset in yours — combine the constant key, the health-check endpoint that returns a literal ok, the bootstrap variable, and the procurement-themed naming convention, and you have a high-confidence signature for finding future deployments before a single lure goes out.

The lineage: AiTM last winter, BitM now

Other researchers publicly documented an earlier version of this same kit back in February. Same delivery chain — PDF to a site-builder page to a serverless gate to a blob:-delivered payload. Same crypto construction. Overlapping key material at the same offset — a 24-character run identical between the two, with a random-collision probability somewhere around 1 in 10⁴³. It is, to a near certainty, the same codebase.

But the February version was reverse-proxy AiTM — it fetched the real login page, rewrote the domains, and copied session cookies. The version that hit us, roughly six months later, is Browser-in-the-Middle — it streams a whole remote browser and never touches a cookie. That’s the trajectory: the same builder, upgrading from stealing the session to renting you a window onto a session that lives entirely on their side. If you’re planning defenses around what phishing kits do today, plan for that direction of travel.

Why I won’t tell you what country they’re in

Here’s the honest part. I can characterize the roles — author versus customer — with real confidence. I cannot tell you the operator’s nationality, and neither can anyone who looks at this carefully.

There is one string in the kit that reads as a regional tell. It’s worthless for attribution, because it’s a kit-author constant baked into every single customer’s build — it tells you something about whoever wrote the kit and nothing whatsoever about whoever ran this campaign. There’s a second, marginally better signal in the operator-supplied config, but it’s a single token sitting in an array otherwise full of filler. And cutting directly against both, the earlier public analysis of this same codebase assessed a Western operator, inferred from a holiday-period pause in the author’s commit history. Two weak signals pointing in different directions is not attribution. It’s a coin flip you dress up in a report.

So I’ll state it the way it should be stated: unresolved. What I can say is that this operator had no local knowledge of the target. They didn’t know what the school’s acronym stood for — the lure PDF confidently expanded it into a plausible-sounding name that doesn’t exist. That confident, fluent, wrong expansion is itself a fingerprint: it’s what a language model does when it fills a gap with something that sounds right. The whole operation carries that signature. The personalization that made the email convincing didn’t come from research or from the operator at all — it came from the hijacked mailbox, which supplied a real relationship and a real thread for free. The generic landing page underneath still had unfilled template placeholders and a default site title. Bespoke lure, generic infrastructure, no one home who knew anything about the victim.

What actually detects and stops this

Detecting it. Stop looking at hosting ASNs — the operator egresses through residential proxies, and chasing the datacenter would have missed this entirely. What actually surfaced it was the identity provider’s own “suspicious login” signal, plus a device-binding event from an unfamiliar machine, correlated against the lure-delivery timestamp. Upstream of any victim, the setup itself is catchable in Certificate Transparency logs: a wildcard cert appearing on a freshly-registered nonsense-word domain, followed days later by a single-label certificate for one random subdomain of it, is the shape of this kit going live. That fires before the lure is sent. Feed it to a CT watcher and you can catch the next campaign during construction.

Stopping it. MFA did not help, and it structurally cannot help against BitM — the victim really does approve a real prompt for a real login. A pushed prompt or a six-digit code is a permission slip, not a lock on a particular device; it rides down the wire into the attacker’s browser like everything else. And password rotation alone is not containment — the session was minted on the attacker’s machine and survives a password change. Session revocation is mandatory the moment you suspect BitM.

The one thing that defeats it by construction is origin-bound authentication — passkeys / WebAuthn. A passkey is cryptographically bound to the origin it was created for and will only produce a signature when the browser is actually talking to that origin, and it must be physically present on the machine running that browser. In this attack the browser talking to Google was on the attacker’s server; the victim’s authenticator was on his desk. There is no keystroke you can relay that makes a passkey two thousand miles away sign for a browser it isn’t attached to. The login simply cannot complete. It’s the address-bar check every user is told to do and eventually forgets, performed by math instead of by a tired person at noon on a Tuesday.

The old advice was to look for the tells — bad grammar, odd phrasing, a sense that something’s off. Those tells are gone. This email had none of them, because it was written by a model that doesn’t make those mistakes and sent from a real account belonging to someone who really knew the recipient. The one mistake in the whole operation was a machine confidently getting a name wrong. When that’s the tell, “train people to notice” has run out of road, and the answer has to be controls that hold when nobody notices anything at all.