← Back to list

Beyond Sneaky 2FA: A Fork That Added a Pre-Stage to Block Analysts

Three mitmproxy captures. Four days apart. And underneath it all, what looked like the same commercial Adversary-in-the-Middle kit (AiTM) —…

Daniel K in OSINT Team · 2026-05-24 18:22 · 0 claps · 14.7 min read
#phishing #aitm #blue-team #threat-intelligence #cybersecurity
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

Phish Tales #11

Beyond Sneaky 2FA: A Fork That Added a Pre-Stage to Block Analysts

Three mitmproxy captures. Four days apart. And underneath it all, what looked like the same commercial Adversary-in-the-Middle kit (AiTM) — caught mid-evolution, running multiple versions simultaneously, and in its most recent deployment, hiding behind a fingerprinting layer sophisticated enough to stop most automated analysis tools before they ever see the phishing page.

This was not a targeted, handcrafted attack. This was the industrial version: a Phishing-as-a-Service operation running campaigns in parallel, with operators sharing infrastructure and the kit developer shipping updates in the background. The sessions were captured days apart. But the fingerprints connect them unmistakably.

Key Points

  • Three separate AiTM sessions against the same organization, captured May 7, 8, and 11, 2026 — an active, ongoing campaign.
  • Same kit, multiple versions running simultaneously: vrs=9.6.6 and vrs=7.8.4 on the same domain, and module=2.16.75 on a second domain with an entirely new parameter schema — evidence of a multi-tenant Phishing-as-a-Service backend.
  • QR code lure in a PDF with high chance of bypassing email URL scanners entirely. The PDF impersonated internal HR documents — salary and benefit updates — to maximize phishing success rates.
  • A bot gate impersonating a browser security check — a “Security Checkpoint / Protected by Shield” UI that looked like a legitimate browser verification but was entirely attacker-controlled. This page appeared identically across all three sessions and is the visual indicator linking them to one kit.
  • Campaign 3 introduced a two-domain pre-stage with collector.js — a browser fingerprinting payload that probed for VPN usage via WebRTC STUN, detected open DevTools, identified browser automation frameworks, and aborted collection if the victim switched tabs.
  • The AiTM kit used heavy JavaScript obfuscation with randomized variable names and a custom string table decoder — the same algorithm pattern across every JS file, in every session, on every domain. The pre-stage collector.js, by contrast, was left unobfuscated.
  • Real-time password validation: stolen credentials were checked against Microsoft’s authentication backend live.

The Lure: QR Code in a PDF, Dressed as HR

The lure arrived as a PDF attachment — the kind a mail gateway is less likely to detonate, and a URL scanner cannot follow.

Inside the PDF: a fake HR document. The specific framing varied but the theme was consistent — something that made an employee feel they needed to act now. After opening the PDF, they found a QR code. Scanning it with a phone took them directly to the attacker’s infrastructure, bypassing corporate endpoint security that might have caught the URL.

This technique — sometimes called quishing — has one underappreciated advantage beyond just evading URL scanners: it forces authentication to happen on a mobile device, where security tooling is typically thinner, MDM visibility is lower, and the smaller screen makes subtle visual differences in a spoofed login page harder to spot.

The Bot Gate: “Protected by Shield”

The pastel-gradient-colored antibot page

The pastel-gradient-colored antibot page

Before the phishing page loaded, the victim in all three sessions encountered the same screen: a centered dialog box on a soft pastel-gradient background, a padlock icon, the words ”SECURITY CHECKPOINT”, and a “Click to Verify” button with the tagline Protected by Shield. This identical page across all three captures is one of the clearest indicators linking them to a single kit — the same HTML, the same CSS class names, the same footer text.

It looked like a legitimate browser security check — a convincing imitation of Cloudflare Turnstile. But it was the kit’s own bot detection gate. Clicking “Click to Verify” did not trigger a CAPTCHA. It triggered a sequence of client-side checks and server interactions, invisible to the victim:

Step 1: Webdriver and behaviour detection. Before any network request is made, the JavaScript checks for signs of automation directly in the browser environment — the presence of navigator.webdriver, legacy headless browser globals like window.callPhantom, window._phantom, and window.__nightmare, and whether fewer than two mouse or touch movement events have been recorded since page load. Any of these signals causes an immediate silent failure: the button shows “Verification Failed” after a random 5–10 second delay, then resets — giving no indication to an analyst that detection has occurred.

Step 2: Server configuration fetch. If the browser passes the local checks, the JavaScript fetches a JSON configuration object from the same URL with ?api=1 appended. The server returns a challenge nonce, difficulty level, worker count, and round count that parametrize the next step.

Step 3: Proof-of-work. The browser spawns parallel Web Workers to solve a SHA-256 based puzzle. Each worker repeatedly hashes a combination of the challenge nonce, a hash of the recorded mouse movements, and its own worker ID, checking whether the result’s first four bytes fall below a threshold determined by the difficulty level. The first worker to find a valid solution wins; all others are terminated. The winning value — the integer nonce that produced a valid hash — is what gets sent to the server as pow=. Using multiple workers means the puzzle is solved faster on real hardware with multiple cores, and much more slowly in constrained analysis environments.

Step 4: Canvas fingerprint. The JavaScript draws a randomised image on an invisible canvas — a Bézier curve with random control points and colours, two text characters at random positions and sizes, and a random rotation — and captures the result as a PNG. The rendering output varies subtly between real hardware, virtual machines, and headless environments due to differences in font rendering, GPU acceleration, and graphics stack implementations.

Canvas fingerprint image sent to the server

Canvas fingerprint image sent to the server

Step 5: Mouse and touch telemetry. Every mouse movement and touch event since page load has been recorded in a buffer capped at 150 entries. The recorded coordinates, a SHA-256 hash of them, and the raw JSON are all included in the challenge submission.

All five data points are submitted together in a single POST request. If the server returns {"success":true, "token": "..."}, the token value is placed into a hidden form field and the form auto-submits — carrying the verified token into the next stage. If the server rejects the submission, the button resets.

What the victim experienced: they clicked a button, watched a progress bar animate, and proceeded to the Microsoft login page. The entire challenge and verification sequence completed in a few seconds, invisibly.

// Configuration received from ?api=1
{
"cid": "875c1409ba7c78d3",
"nonce": "cfbd044be45b76d5",
"difficulty": 6,
"workers": 4,
"rounds": 10000
}
// POST fields submitted after solving
cid = 875c1409ba7c78d3
nonce = cfbd044be45b76d5
pow = [winning nonce integer]
canvas = data:image/png;base64,…
track = [{"x":912,"y":248,"t":9010},{"x":600,"y":180,"t":9187},…]

The bot-gate token was embedded by the server into the page’s background image URL as a base64-encoded blob. It carried hashed victim IP and User-Agent values with a five-minute expiry, binding each challenge instance to the specific browser that requested the page.

// Token payload (decoded):
{
"ip": "5d71072cbc71b97c7b24ac01b56a5f8b…", // SHA-256(visitor IP)
"ua": "cf096cfe1a9fe2a938670565dc68b3e4…", // SHA-256(User-Agent)
"exp": 1778157437, // 5-minute TTL
"img": 1 // challenge type
}

Hiding in Plain Sight: Two Layers of Decoy Content

Analysis of the bot gate and phishing page HTML revealed two content camouflage techniques, each serving a different purpose.

The first appeared in the phishing page HTML immediately after the bot gate: a food-themed content section — a well-documented signature of Sneaky 2FA and the indicator that confirmed the lineage of this kit.

<div class="ugjOgihSuHcontainer">
    <div class="section">
      <h2>Food</h2>
      <p>
        Explo
        <a class="ugjOgihSuH"></a>
        re a variety
        <a class="ugjOgihSuH"></a>
        of delicious
        <a class="ugjOgihSuH"></a>
        food opt
        <a class="ugjOgihSuH"></a>
        ions i
        <a class="ugjOgihSuH"></a>
        ncluding appetizers, main courses, and desserts.

The visible text reads as a coherent sentence about food, but it is fragmented across a series of empty <a> anchor tags all sharing the class name ugjOgihSuH. To a human reader or a browser rendering the page, the text flows normally.

This technique targets web proxy content inspection and URL reputation crawlers that parse page text to classify whether a domain hosts legitimate content. A page with a heading, a paragraph about food, and normal prose scores as benign. The food theme in particular is deliberate — it is a low-suspicion, high-frequency topic that appears on millions of legitimate websites.

The second layer appeared in the bot gate HTML itself: all class names, element IDs, and visible body text were generated using a consonant-vowel-consonant pseudoword algorithmKobesi, Ijabewep, Ojalohu, Azemeje. These are not real words in any language. They follow a strict syllable pattern that produces strings which look plausible at a glance, pass spell-checker heuristics, and resist fingerprinting on specific identifier strings. The page body contained a fake article with a fabricated heading — Lost Stone Falls 5935 — and several paragraphs of flowing pseudoword text, giving the page a content structure that resembles a legitimate website.

html<body class="delivered Kobesi" id="Ijabewep">
<header class="Imab Ojalohu" id="Ireze">
<h1 class="Pumo Olubeci" id="Inezenat">Optimizing performance…</h1>
</header>
<main class="Jetuwagah Nusosoz" id="Ajizukel">
<article class="Nogazubo">
<h2 class="Itil">Lost Stone Falls 5935</h2>
<p class="Azemeje">Hilazad Owugimada Johe Punosu Hulul Ilun Geril Ubukud
Enigobudi Oneburage Iwigepude Fepedug Jiwune Ofehu Wubodop Ofimevef…</p>
</article>
</main>

The Bouncing Envelope: A Loading Screen with a Purpose

Between the bot gate and the Microsoft login clone, victims saw a brief animation — a bouncing envelope mimicking Microsoft Outlook’s loading screen. This was a trust-building element. It filled the time while the kit established its relay session with Microsoft’s real authentication backend. By the time the next screen appeared, the attacker was in position.

The Phishing Page: Microsoft Login with Your Own Branding

The victim landed on a clone of their organization’s Microsoft 365 sign-in page — their specific organization’s branded login, with the correct background illustration fetched live from Microsoft’s own CDN.

GET aadcdn.msauthimages.net/c1c6b6c8-[hash]-td8/logintenantbranding/0/illustration
?ts=638352296418189364

The kit performed a live tenant lookup based on the victim’s email domain, resolved the correct Azure AD tenant, and fetched the current branding image on demand. The victim saw exactly what they would have seen on the real Microsoft sign-in page.

The victim’s email address was pre-filled in the login form, decoded from the URL parameter email_from_url.

After the victim entered their password, the kit relayed it to Microsoft’s real authentication backend in real time and mirrored what status came back — wrong password, account not found, MFA required — adapting its behaviour at each step to match the legitimate login experience exactly. The victim had no way to distinguish the relay from the real thing.

When MFA was required, the kit presented the victim the correct method their account was configured to use — push notification, SMS code, number matching, or app verification — while the attacker’s server captured the resulting session token before forwarding the victim onward to a legitimate Microsoft page.

One Kit’s URL Fingerprint Across Three Campaigns

Every redirect URL told a story. Here is the URL a victim’s browser received after passing the bot gate in Campaign 1:

https://armstrongworldinxpadustries[.]vu/$[victim]@[org].ch
?email_from_url=[victim]%40[org].ch
&id=aaca9
&sid=f0cdf6452bbeedd1d662277b1f1b8cf7
&g=E1
&vrs=9.6.6
&token=6dd1b9e3992214328c9fc33c6a3d6cee

And here is the equivalent URL from Campaign 2, targeting a different victim at the same organization, 25 hours later:

https://armstrongworldinxpadustries[.]vu/$[victim]@[org].ch
?email_from_url=[victim]%40[org].ch
&id=db282
&sid=0bc1b341b35e588ad0cadc4fab295e80
&g=DF
&vrs=7.8.4
&token=83e227a610720dc71c55545ed868b5d7

Both URLs were on the same domain. The session IDs differed — expected for separate victims. But vrs= told a different story: one campaign ran version 9.6.6, the other 7.8.4. Two different kit versions, deployed simultaneously, on the same infrastructure.

This is the multi-tenant PhaaS signature. A single backend hosting multiple operator campaigns, each with its own kit version and victim batch.

The MFA relay session paths completed the picture. Each campaign received its own randomly generated 256-character session namespace:

Campaign 1: /PwPt-FoLdEr-[256 random chars]/
Campaign 2: /ShRe-FiLe-[256 random chars]/
Campaign 3: /ShRe-FiLe-[256 random chars]/

The namespace prefixes — PwPt-FoLdEr and ShRe-FiLe — appeared across both domains and persisted across sessions, functioning as stable kit-level identifiers independent of the rotating domain names.

Campaign 3: A New Domain and a Redesigned Architecture

The third session, four days after the first, introduced changes significant enough to deserve separate attention. The kit had moved to a new domain — nbcuniversalbql[.]vu — and the URL parameter schema was completely different:

https://nbcuniversalbql[.]vu/[base64-encoded-victim-address]
?email_from_url=[base64-encoded-victim-address]
&instance=f07c403d21b43d80
&cluster=4dd9c4c759cf
&module=2.16.75
&grant=e97e592b661b04424ce39d856764fa1b
&channel=vezfmkyfm
&queued=1778417719

The victim email was now base64-encoded in the URL path, rather than appearing in cleartext. The parameters id=, vrs=, and g= were replaced by instance=, cluster, module=, and grant=. The versioning scheme changed from vrs=9.6.6 to module=2.16.75. This was not a minor update — it read like a different tier of the same service, or a different operator configuration entirely.

But the most significant change was what happened before the victim reached this URL.

collector.js: The Pre-Stage That Does Its Homework

Before the redirect to nbcuniversalbql[.]vu, the victim’s browser landed on a different domain: voluntarism281[.]meinservicehub[.]com[.]de. It contained a JavaScript file: collector.js.

collector.js was written in plain, readable code and built the most comprehensive browser fingerprint I observed in a phishing campaign. It enumerated every accessible property of window, navigator, screen, document, and location — screen dimensions, color depth, timezone offset, installed plugins, hardware concurrency, device memory, language settings, touch point count, and more. But the passive enumeration was not the only thing. The interesting part was what the script probed actively.

WebRTC STUN: Revealing the Real IP Behind a VPN

var pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
});
pc.createDataChannel("");
pc.onicecandidate = function(e) {
if (e.candidate) {
var p = e.candidate.candidate.split(" ");
data._rtc.push({ i: p[4], t: p[7], p: p[2] });
}
};
pc.createOffer().then(o => pc.setLocalDescription(o));

WebRTC is the browser API that powers video calls and peer-to-peer communication. To establish connections, browsers use STUN servers to discover their own public IP address. This script opened a silent WebRTC connection and asked Google’s STUN server what the visitor’s real public IP was. The browser’s own ICE negotiation returned the result — including the srflx (server-reflexive) candidate containing the external IP as seen from the internet — which the script collected and included in the fingerprint payload sent to the server.

The reason why this matters is VPNs. When a researcher uses a VPN, their HTTP requests show the VPN’s exit IP. In some VPN configurations the STUN probe goes out through the real network interface, leaking the actual IP. If the WebRTC result differed from the HTTP connection IP, the victim was flagged — almost certainly a researcher or analyst — and received a silent dead end instead of the phishing page.

The probe used stun.l.google.com:19302 because blocking it would break Google Meet and Chrome’s built-in calling features. It is usually not firewalled on corporate networks.

DevTools Detection

var cnt = 0;
var fn = function() {};
fn.toString = function() { ++cnt; return ""; };
console.log(fn);
data.tostring = cnt;

When DevTools are open, Chrome calls toString() on objects passed to console.log to format them for display. This script overrode toString() with a counter. After the console.log call, a non-zero cnt meant that DevTools were open — and the victim was almost certainly an analyst.

Automation Framework Detection

Array.prototype.includes = function() { data.proto = true; };
document.createElement("video").canPlayType("video/mp4");

The script modified a built-in JavaScript function and watched whether the browser inadvertently used the modified version internally. Unmodified browsers typically do not. Browsers controlled by automation frameworks such as Selenium, Puppeteer, or Playwright — which alter how JavaScript interacts with browser internals as part of their injection mechanism, regardless of whether they run headless or in a visible window — triggered the modified prototype through an internal canPlayType call. If that happened, the visitor was flagged as running browser automation.

Abort on Tab Switch

window.addEventListener("pagehide", () => aborted = true);
document.addEventListener("visibilitychange", () => {
if (document.hidden) aborted = true;
});

This code watched whether the page was moved into the background, hidden, or unloaded. If that happened, it set an abort flag and stopped the fingerprinting process. Many automated analysis systems, web crawlers, and security sandboxes do not browse websites the way a real user does — they often load pages in background tabs, open many simultaneously, or run pages in headless environments that are never visible on screen. In those situations, the browser fires the visibilitychange event with document.hidden=true, or fires pagehide when the page is discarded. The script treated this as a sign of non-human behaviour and stopped. Only visitors actively viewing the page in the foreground received the redirect.

This is why automated sandboxes may return a “suspicious” verdict for pre-stage infrastructure rather than “malicious” — they observe the fingerprinting script and the domain, but the phishing page never loads in their context, leaving them nothing concrete to condemn.

All collected data was POSTed to a different subdomain: voice[.]meinservicehub[.]com[.]de. The landing page and collection endpoint were intentionally split. Blocking one did not block the other.

Only a visitor who passed the full fingerprint analysis received the redirect. Researchers, sandboxes, and automated scanners got a silent dead end before the phishing infrastructure was ever contacted.

JavaScript Obfuscation: A Consistent Kit-Level Fingerprint

All JavaScript files served by the AiTM kit itself — across both domains and all three sessions — shared the same obfuscation pattern: a global constants array of obfuscated operands, a decode function operating against a custom lookup table, a lazy string cache, and no-op wrapper functions whose purpose was to break static analysis tools that pattern-match on expression structure. Variable and function names were randomized to 7–10 characters throughout.

Each JS file used a different randomized lookup alphabet. Different alphabets mean the encoded string tables were non-interchangeable between files — a decoder extracted from one file would produce garbage on another. The build system almost certainly generated each file with a fresh random alphabet at compile time while keeping the decoder algorithm structurally identical.

That structural identity is what matters. The same algorithm pattern appearing across two domains, three victim sessions, and a four-day window is a kit-level fingerprint that persists even as domains and session namespaces rotate.

collector.js had none of this. It was left in fully readable code.

Is This a Known Kit?

Attribution required some digging — the kit shared indicators with several known PhaaS offerings, but the clearest evidence only emerged from a close reading of the HTML.

Two indicators in the captured HTML support attribution to Sneaky 2FA. First, the food-themed hidden content section — a well-documented Sneaky 2FA signature — appeared in the phishing page HTML immediately after the bot gate:

<div class="ugjOgihSuHcontainer">
  <div class="section">
    <h2>Food</h2>
    <p>Explo<a class="ugjOgihSuH"></a>re a variety
    <a class="ugjOgihSuH"></a>of delicious...</p>

Second, the soft pastel-gradient background of the bot gate is consistent with Sneaky 2FA’s documented visual style.

What the observed techniques add to the Sneaky 2FA baseline is substantial. The kit appears to be a fork of Sneaky 2FA that has diverged considerably from its documented baseline — the custom two-domain pre-stage with collector.js, the WebRTC STUN IP leak probe, the parallel Web Worker proof-of-work challenge, the entirely redesigned URL parameter schema in Campaign 3, and the PleskLin infrastructure stack are not documented for Sneaky 2FA.

I shared a preview of this write-up with the Sekoia Threat Detection & Research team, who confirmed they are already tracking this kit as a fork of Sneaky 2FA — consistent with both indicators found in the HTML. Their visibility across campaigns and kits at scale made this kind of confirmation possible.

Takeaways for Defenders and Analysts

  • QR code phishing bypasses email URL scanners by design. The URL never appeared in the email body as text — it existed only as pixels in an attached image. Train users to treat QR codes in email attachments with the same skepticism as unexpected links, especially when the PDF promises financial benefits.
  • “Protected by Shield” is not a legitimate security check. Genuine browser verification challenges do not appear as prerequisites to Microsoft login pages. If you encounter a bot gate on a path to a Microsoft login, treat the entire flow as suspicious regardless of how convincing the UI looks.
  • MFA is not sufficient against AiTM. The kit intercepted MFA in real time. The victim completed their normal authentication process with no indication anything went wrong. Phishing-resistant MFA (FIDO2 / passkeys) remains the most effective countermeasure — hardware-bound credentials cannot be relayed through a proxy.
  • The pre-stage domain is a detection opportunity. collector.js was served from a different domain than the phishing page. A request to a com.de domain loading a JavaScript file, followed within seconds by a redirect to a .vu domain, is a two-event behavioral signature that neither domain alone provides. Watch for the sequence in DNS and proxy logs.
  • Protect your analysis IP before visiting suspected phishing infrastructure. The WebRTC STUN probe can reveal your real IP even through a VPN. Practical options: disable WebRTC in your analysis browser (Firefox: media.peerconnection.enabled = false in about:config); use a browser extension that blocks WebRTC leaks; use a dedicated analysis VM on a network whose IP you are comfortable exposing; or use mobile data, which changes IP frequently and has no persistent tunnel to leak through.
  • Close DevTools before visiting pre-stage pages. The console.log + toString counter detects open DevTools before any fingerprint data is sent. Use network-level capture (mitmproxy, Burp, Wireshark) rather than browser DevTools to analyze pre-stage traffic.

Indicators of Compromise

All domains defanged.

Phishing Kit Domains

armstrongworldinxpadustries[.]vu
nbcuniversalbql[.]vu
voluntarism281[.]meinservicehub[.]com[.]de
voice[.]meinservicehub[.]com[.]de

URL Patterns

/$[victim-email]?email_from_url=…&id=[5hex]&vrs=[semver]&g=[2char]
/[base64(victim-email)]?instance=[16hex]&cluster=[12hex]&module=[semver]&grant=[32hex]
/[18–25 digits]?t=[token] (bot gate image / token endpoint)
/PwPt-FoLdEr-[256 random chars]/ (MFA relay, namespace 1)
/ShRe-FiLe-[256 random chars]/ (MFA relay, namespace 2)
/fingerboard[0–9]+/collector.js?v=[8hex] (pre-stage JS)

HTTP Response Header Fingerprint

X-Powered-By: PleskLin
Content-Encoding: zstd
Access-Control-Allow-Origin: *

HTML Decoy Content Indicators

Food-themed hidden content section with class “ugjOgihSuHcontainer” Text fragmented across empty <a class=”ugjOgihSuH”> anchor tags

HTML body/class/ID names follow CVC (consonant-vowel-consonant) pseudoword pattern with no semantic content

Bot Gate Challenge Token Structure

{"ip": "[SHA-256 of visitor IP]", "ua": "[SHA-256 of UA]", "exp": [unix+300], "img": [1|3]}

Bot Gate PoW Configuration (from ?api=1)

{"cid": "[16hex]", "nonce": "[16hex]", "difficulty": [int], "workers": [int], "rounds": [int]}

References:


메타데이터
post_id
806647baa393
slug
beyond-sneaky-2fa-a-fork-that-added-a-pre-stage-to-block-analysts-806647baa393
url
https://medium.com/@DanielsPhishTales/beyond-sneaky-2fa-a-fork-that-added-a-pre-stage-to-block-analysts-806647baa393
canonical_url
https://medium.com/@DanielsPhishTales/beyond-sneaky-2fa-a-fork-that-added-a-pre-stage-to-block-analysts-806647baa393
author_url
https://medium.com/@DanielsPhishTales
status
ok
fetched_at
2026-06-09 15:37:30