A phishing email landed in my personal inbox this afternoon claiming MetaMask was rolling out a “verification requirement” and I needed to “activate fingerprint access” before a deadline. Content-wise it’s the standard crypto lure. What made me pull it apart is the authentication result Gmail stamped on it:

dkim=pass header.i=@moodlecloud.com
dkim=pass header.i=@amazonses.com
spf=pass  (54.240.27.18 is permitted for mail.gl.moodlecloud.com)
dmarc=pass (p=QUARANTINE) header.from=moodlecloud.com

Everything passes, and none of it is forged. The attacker never touched a mail server. They built a Moodle site and let Moodle send the email.

Delivery: Moodle’s messaging, working as designed

Moodle is a learning-management system. When one user sends another a site message, Moodle emails the recipient a copy. It uses the site’s configured outbound mail. On MoodleCloud, the hosted service, that’s Amazon SES under moodlecloud.com’s own SPF and DKIM. The headers say so directly:

From: "DappMetaMask Connect (via Web3-Portfolio)" <noreply@choomer-in3.moodlecloud.com>
To: damani15 kamani15 <[me]>
X-Mailer: PHPMailer 6.9.3
X-Moodle-Originating-Script: https://choomer-in3.moodlecloud.com => ip-10-80-52-92:message/lib.php:332
X-MoodleCloud-Site: choomer-in3

Put together, this is how the operator set it up:

The public config of the operator’s second site shows self-registration disabled. That’s consistent with the recipients being created by the admin, most likely a bulk CSV import of a target list, rather than anyone signing themselves up. I can’t see the import itself, so treat that part as inference.

From the mail filter’s point of view this is a legitimate notification from a legitimate SaaS domain. The only hostile part is the text in the body, which is what the operator controls. Most filters trust dmarc=pass from an established domain, and this one had it.

The body was lifted from someone else’s email

The HTML has two leftovers from where it came from:

The operator opened a Glassdoor email in Gmail, copied the rendered layout, swapped in their own text, and pasted it into a Moodle message. The fox logo isn’t on the sending site. It’s hot-linked from a second MoodleCloud site, consensys-ec03[.]moodlecloud[.]com, named after the company that makes MetaMask. The logo’s upload stamp in the pluginfile path is 1790275117, which is 18:38 UTC on the day of the send. My copy went out at 23:49:22 UTC, so the branding site was set up that afternoon.

The call-to-action wasn’t a Moodle link either. It was a SendGrid click-tracking URL (u10277184.ct.sendgrid[.]net/ls/click?upn=…) pasted straight into the message body. MoodleCloud sends through SES, so this link didn’t come from the delivery pipeline. The operator generated it on a SendGrid account they control, or one they’ve compromised. That buys them a second reputable domain in front of the real destination. The actual landing page doesn’t appear anywhere in the email.

The redirect chain

I resolved each hop by reading the Location header and never loading the page. I didn’t use my own tracking link past SendGrid.

HopHostRegisteredBehavior
1u10277184.ct.sendgrid[.]net—302 → frondime[.]com/?IDKE-031080928091840981
2frondime[.]com2026-09-02, Dominet (HK), CloudflareBare PHP 302 → hop 3. Redirects the same with or without the IDKE- token.
3smartsigmetacontract[.]com2026-09-17, Dynadot, CloudflareServes the kit, <title>MetaMask - Smart Shield</title>

The two landing domains were registered fifteen days apart, through different registrars. Both sit behind Cloudflare, so the origin server isn’t visible from outside.

The kit

The page walks the victim through two steps:

  1. “Link your email address.” The email is POSTed to email-send.php when they click Continue.
  2. “Link a wallet.” A 12–24 word recovery-phrase grid.

It’s wrapped in several paragraphs about a “MetaMask Smart Shield Contract”. Supposedly that “links your mnemonic recovery phrase with a smart contract on the blockchain” and emails you when a dApp misbehaves. No such thing exists. A recovery phrase is the wallet, and nothing legitimate ever asks for one.

The HTML has an inline script that assembles a message and POSTs it to mm.php when the victim clicks “Backup”:

🔐 WALLET RESTORE + EMAIL LINK
📧 Email: …
🔑 Seed Phrase: …
📝 Word Count: …

The emoji-labeled block is the house style of Telegram-bot exfiltration. The bot token, if it is Telegram, stays on the server in mm.php. I checked everything the browser receives, and nothing in it contains a token, a chat_id, a webhook or an api.telegram.org call. Kits that send straight from the browser to Telegram hand you the token. This one doesn’t.

The HTML is full of comments like <!-- ORIGINAL SRP GRID - EXACT STRUCTURE FOR m.js --> and <!-- ORIGINAL BUTTON - KEPT EXACTLY AS IS -->. Someone took an older fake “import wallet” kit, kept its script, and bolted a new email-capture step on the front.

m.js: it sends your phrase while you’re still typing

The real collector is m.js, a 60 KB single-line file run through javascript-obfuscator:

I evaluated only the array, decoder and rotation loop in a Node vm context, with no DOM and no network. Then I replaced every decoder call with the string it returns and pretty-printed the result. Readable, it does four things:

The victim never has to press anything. Five words typed and a walk-away is five words delivered.

I confirmed that behavior instead of trusting my reading of the code:

The capture:

 3.7s  POST email-send.php  email=victim@example.org
10.4s  POST mm.php          message=abandon abandon abandon abandon abandon (IP: 203.0.113.7)
       --- typed 5 words and stopped ---
18.6s  POST mm.php          message=abandon abandon … abandon about (IP: 203.0.113.7)
21.0s  Backup button: still disabled

The partial phrase left the page after five words, eight seconds before the phrase was complete.

The kit is broken, and it doesn’t matter

The “Backup” button never enables, so the inline script’s tidy WALLET RESTORE + EMAIL LINK message, the one that pairs the email with the phrase, never fires. Here’s why:

  1. The inline script builds the 12 input boxes and attaches the listeners that enable Backup.
  2. m.js runs on document.ready, empties the same container, and rebuilds it with its own inputs.
  3. The inline listeners are gone with the old boxes. m.js’s own submit path is wired to #password, #confirm-password and a #termsModal that don’t exist on this page, which are leftovers from the kit’s previous life.

There’s a second break: the inline script throws a TypeError on load, because the success modal it references (#successModal) isn’t on the page.

None of that saves the victim. The streaming collector goes around all of it. The operator gets the email from one endpoint and the phrase, with IP, from the other, and can pair them by source IP and timing.

One more fingerprint. m.js carries its own copy of the BIP39 English wordlist to validate words. It has 2,047 of the 2,048 words, because rebel is missing. Any real phrase containing “rebel” would fail the kit’s validation with “contains invalid words”. That only affects the dead submit path, but a missing word is a distinctive marker for tying other copies of this kit together.

The second site

consensys-ec03[.]moodlecloud[.]com was still up after the sending site had disappeared. The unauthenticated config endpoint the Moodle mobile app uses (tool_mobile_get_public_config) returns:

It still serves the email’s logo. It isn’t in maintenance mode, and it’s set up to send the next wave.

One practical note if you go poking at MoodleCloud yourself: its load balancer returns 403 to a bare Mozilla/5.0 user-agent. I briefly read that as “site suspended”. Sending a full browser UA returned the login page. Check your own request before you read anything into a 403.

What would have caught it

Where it stands

As of publication, the sending site already returns MoodleCloud’s “not found” page. Everything else was still live when I last checked:

I’ve reported them to MoodleCloud, SendGrid, Cloudflare, both registrars and MetaMask. I’ll update this post as they come down.

Indicators

Defanged. Recorded 2026-09-24/25 UTC.

Sending site     choomer-in3[.]moodlecloud[.]com   (sitename "Web3-Portfolio", sender uid 2 "DappMetaMask Connect")
Asset site       consensys-ec03[.]moodlecloud[.]com (sitename "Web3-Portfolio")
Redirector       u10277184.ct.sendgrid[.]net
Redirector       frondime[.]com          reg 2026-09-02  Dominet (HK)
Kit              smartsigmetacontract[.]com  reg 2026-09-17  Dynadot
Kit endpoints    /email-send.php  /mm.php  /m.js
Kit title        "MetaMask - Smart Shield"
m.js  sha256     8ceef55ff56fb7c1bfc11e883f2c42d7abf5f97286d8cf5a0f5c7411b68c4bd5
page  sha256     a392a1c05f1ef89468b6b8aea337fa110235a63b1d9d3794d5360bbadfa95dc0
Marker           embedded BIP39 list missing "rebel" (2,047 words)