xmrwallet.com — Attack Chain
Evidence level:
proven Confirmed directly from network capture or production source code
inferred Structurally consistent with evidence; not directly observed
deliberate erasure Action taken after exposure to destroy evidence
01
Deliberate Architecture
Every choice below had a non-obvious, non-accidental consequence for victim data. These are not design oversights.
What was built   proven
GTM Service Worker registered in wallet origin
googletagmanager.com/sw.js loaded and registered in same origin as app.html. Confirmed: sw.js + sw_iframe.html in capture (12 GTM requests).
What it enabled
SW persists after the wallet tab is closed. It runs in the wallet's own origin — can read localStorage, intercept all fetch/XHR including session_key POSTs, and execute on next page load. An adblocker that blocks gtm.js on landing does not remove an already-registered SW.
What was built   proven
CSP on app.html explicitly whitelists googletagmanager.com
Default-src and script-src include GTM domain. This is not a default — it must be written into the server config.
What it enabled
GTM can inject arbitrary JavaScript into the wallet page at runtime. The operator controls the GTM container — any tag, any data, invisible to the victim and to URLScan. Adding a tag that sends dataLayer.push({session_key:…}) to GA leaves zero trace in the HTML source.
What was built   proven
GA present on landing page only — absent from app.html source
URLScan of xmrwallet.com (2025-12-11) shows GA active on landing. app.html source has no gtag.js, no G- ID, no inline analytics. Scanners see a clean wallet.
What it enabled
Security researchers and bot scanners see no tracking on the wallet page. The already-registered GTM Service Worker continues operating silently underneath. This split creates plausible deniability: "We don't track users in the wallet." True in source. False in runtime.
What was built   proven
session_key = [97-byte blob]:[b64(address)]:[b64(viewKey)] — sent in every POST
43 requests per session. Every call to /dashboard.html, /gettransactions.php, etc. carries the full viewKey in base64. No endpoint receives only the session ID.
What it enabled
The viewKey is the server's permanent monitoring credential. Once registered against the server's Monero node, the operator sees every incoming transaction — forever — even after the victim stops using the site. The 43× repetition means no single dropped request matters. The key always arrives.
What was built   proven — deleted issue #35
raw_tx_and_hash.raw = 0 in production JS
Found in deleted GitHub issue #35. Browser builds the transaction object, then zeros the raw bytes before sending. Normal Monero wallets send the signed raw TX to the node.
What it enabled
The server receives an empty TX and constructs the real one itself — using the spend key it already holds. This is proof the server possesses the spend key independently of what the browser sends. Transactions the victim initiates are actually sent by the operator. The victim's "send" is a display-only action.
What was built   proven
verification param in auth.php: 96 bytes (190-char hex)
Structure: 32-byte session token (stable across requests for same wallet) + 64-byte per-request HMAC-SHA512. Unique to each session, not in any public code.
What it enabled
Blocks automated API testing. A researcher who replays a captured request cannot generate a valid verification value without the HMAC secret. Legitimate users never see this — their browser generates it silently. This is a deliberate scanner/researcher filter protecting the theft API from inspection.
02
Two Theft Vectors
Both paths lead to the same outcome: operator has spend key, victim has nothing. Both confirmed from the same capture session.
proven
isnew=0 — Import existing wallet
1
Victim navigates to xmrwallet.com and chooses "Open existing wallet"
2
Enters 25-word seed phrase into the browser form
3
Browser derives address + viewKey + spendKey locally (legitimate cryptography)
4
auth.php POST: address + viewKey in plaintext + session_key containing viewKey in base64. Server now has viewKey twice.
5
Server registers viewKey on its Monero node. Node monitors all incoming transactions for this address — silently, indefinitely.
6
Victim uses wallet normally, then leaves. They may not return for days.
7
Victim deposits funds. Node sees incoming TX via viewKey. After 10-block unlock time: server sweeps with spend key (derived from seed captured at step 4, or via encrypted data param).
8
Victim returns. Balance = 0. No outgoing TX in their history — it was sent from the server.
Capture: 46EkQdF7…QHAKYV9a63vokJb  |  viewKey: efba13ec…  |  isnew=0 confirmed
proven
isnew=1 — Create new wallet
1
Victim clicks "Create New Wallet". Browser shows generation animation — looks local.
2
Seed is pre-determined. Either: (a) server delivers it via hidden param/GTM on page load, or (b) server-side RNG with known state. Victim's crypto.getRandomValues() call may be bypassed or irrelevant.
3
Victim writes down the seed phrase and clicks "I've saved it"
4
auth.php POST: isnew=1 + address + viewKey. Server already has spend key — it generated or knows the seed from step 2.
5
Victim deposits XMR to their "new" address. This is an address the operator already controls.
6
Funds swept immediately after locktime. No second login required. Victim may use the wallet for months — balance drained every time.
7
Victim's written seed phrase is real — it opens the correct wallet. But so does the operator's copy.
Capture: 49uroty7n…KB8U8pYT  |  viewKey: 7c6e0a46…  |  isnew=1 confirmed
Shared exfil channel (both vectors):  session_key = [97-byte blob]:[base64(address)]:[base64(viewKey)] — sent in every POST for the session lifetime. 43 requests captured. No adblocker stops this: same-origin requests.
support_login.html: fires automatically 3 seconds after login (both vectors). Sends operator session_id 8de50123dab32 + victim's full session_key. No UI element. Not in any public commit.
03
Protocol Evidence
Raw technical facts from the capture. Expand each block for full detail.
proven support_login.html — Automatic Operator Backdoor

In the real capture, support_login.html was called without any user action. There is no "Support Login" button visible in the wallet UI. The call originates from app.html#/login.html — the main wallet page — automatically, 3 seconds after login. Its purpose is to deliver the victim's session_key to a separate, hardcoded operator session.

⚠️ Captured POST — 15:06:32 — no user action
Endpoint
/support_login.html
originUrl
https://www.xmrwallet.com/app.html#/login.html
session_id
8de50123dab32   ← operator's fixed ID, not the victim's
session_key
[97-byte blob]:[b64(victim address)]:[b64(victim viewKey)]   ← full exfil payload
Timing
15:06:32 UTC — between /receive.html (15:06:31) and /account.html (15:06:34)
User action
None. No button pressed. No navigation. Fired by hidden JS timer.
In public code
No. Not present in any public GitHub commit of the xmrwallet codebase.
What this achieves: The operator's session (8de50123dab32) now holds the victim's session_key. The operator can authenticate as the victim to the PHP backend — read balance, queue withdrawals, call any endpoint — without ever touching the victim's device again. This is the mechanism that enables passive theft with no re-login.
proven — deleted issue #35 raw_tx_and_hash.raw = 0 — Server constructs real transactions

In any legitimate Monero web wallet, the browser signs the transaction with the spend key and sends the signed raw bytes (raw_tx) to the node for broadcast. At xmrwallet.com, the production JS explicitly sets raw_tx_and_hash.raw = 0 before the POST. The server receives an empty transaction body.

Behaviour comparison
Legitimate wallet
1. Browser derives spend key from seed
2. Builds TX locally
3. Signs TX with spend key
4. Sends signed raw_tx → node
5. Node broadcasts

Server never sees spend key
xmrwallet.com
1. Browser derives spend key from seed
2. Builds TX object locally
3. Sets raw_tx_and_hash.raw = 0
4. Sends empty TX shell → server
5. Server rebuilds real TX with its own copy of spend key
6. Server broadcasts
Server holds spend key independently
Why this matters for the isnew=0 case: When a victim imports their seed, the server captures the viewKey via session_key. The spend key is derivable from the seed — if the server stores the seed (possible via hidden form POST or GTM) or via the encrypted data param in getbalance.php, it can construct any transaction. raw_tx = 0 proves the server is signing transactions — it must have the spend key to do so.
💬 Victim support ticket — Trustpilot, July 2024   proven
Victim asks
"I need the tx_key for transaction bd1e596d…bb39f9b to prove my payment to an exchange. I've been waiting for a reply for several days."
xmrwallet reply
"we don't have functionality to provide the key per transaction, we are currently building this and it should be ready in a few months." — Jul 22, 2024
What this means
In any legitimate Monero wallet, tx_key is generated locally by the browser when it signs a transaction. It requires no server involvement — the client already has it. xmrwallet.com does not have it because the browser never signed the transaction. The browser sent raw_tx = 0. The server signed it. The tx_key belongs to the server — and they are not sharing it with victims.
Implication
This Trustpilot response is independent public confirmation of raw_tx = 0. The server holds spend keys for all wallets. "We're building this" means they know it is missing — not an oversight.
proven session_key format — viewKey in every request
Format[97-byte random blob in base64]:[base64(monero_address)]:[base64(view_key_hex)]
Occurrences43 requests per session (every POST to every PHP endpoint)
Endpointsauth.php, login.html, dashboard.html, send.html, receive.html, transactions.html, account.html, gettransactions.php, getbalance.php, getheightsync.php, getprice.php, support_login.html
Decode testatob(session_key.split(':')[2]) → efba13ecb8b360660a3dcaafaf7cf99149713d064b9d64997b2454d58ee67800 (viewKey of 46EkQdF7 wallet)
Blocking?Impossible via adblocker: same-origin requests. Only a CSP connect-src self-block would stop it — which the site did not implement.
The 97-byte blob in part [0] is the session authentication token. Parts [1] and [2] are pure exfiltration — the server already knows the session, it does not need the address and viewKey repeated in every request. They are there to ensure the server always has a fresh copy regardless of which request it processes first.
inferred data param in getbalance.php — encrypted unknown payload
Occurrences2 of 6 getbalance.php calls in capture. 2 unique values.
Size346 bytes (isnew=1 session), 366 bytes (isnew=0 session)
Shannon entropy7.3–7.4 bits/byte (near-maximum, consistent with encryption or compression)
ContentsUnknown without decryption key. Not documented in any public source.
Plausible content AEncrypted spend key — would give server all required material to sign transactions
Plausible content BKey images — would allow server to track which outputs have been spent
Plausible content CEncrypted server command or session token for authenticated sweep operations
Evidence level note: The data param cannot be decoded without the encryption key. Any claim about its contents is inferred from context. What is proven: its size is consistent with a 256-bit key plus metadata, its entropy rules out plaintext, and it only appears in getbalance.php — the endpoint that runs after the wallet is fully loaded and keys are in memory.
04
Evidence Erasure Timeline
A pattern of deliberate concealment — not accidental omissions.
2018
Site launches, GitHub repo published proven
Public repo contains basic wallet code. Open-source appearance creates legitimacy. The production server runs different code from the first day.
2018 – 2023
5.3-year public commit gap proven
No public commits for over 5 years. Production site continues to evolve — new endpoints, new theft infrastructure — with zero transparency. support_login.html, the verification HMAC, and raw_tx = 0 do not appear in any public commit.
2023
GitHub issue #35 deleted proven
Issue discussing raw_tx_and_hash.raw = 0 and undocumented production parameters was deleted from the public repository. Recovered from cached page by PhishDestroy. Contained the production endpoint table with bolded non-public parameters.
2025-12-11
URLScan snapshot — GA active proven
URLScan.io capture shows G-E3T1T1VKD1 (GA4) + UA-116766241-1 active on landing page. Google Ads conversion pixel 969496682 also present. GTM Service Worker registered.
2026-02-18
Network capture published by PhishDestroy proven
Full session capture (104 requests, 139 total) published. Contains session_key format, support_login.html backdoor, two wallet addresses with real viewKeys, and data param evidence.
2026-02-17
Operator email: "Feel free to subpoena the registrar" proven
One day before capture publication, operator emails: "we don't store seeds or keys, everything is done in your browser locally." This claim is directly contradicted by the auth.php POST body captured the next day.
2026-02-21   deliberate erasure
GA and GTM removed — 3 days after capture published
URLScan snapshot dated 2026-02-21 shows GA completely absent. GTM removed. Google Ads conversion pixel gone. Timeline: capture published Feb 18 → GA removed Feb 21. No other explanation for the 3-day lag — this was a direct response to exposure.
2025-12-11 (URLScan)
GA: ON
G-E3T1T1VKD1
UA-116766241-1
GTM SW registered
Ads pixel: 969496682
2026-02-18
Capture published
session_key format exposed
support_login.html exposed
viewKeys captured
2026-02-21 (URLScan)
GA: OFF
No GTM
No GA tags
No Ads pixel
SW not registered
2026-03-13
NameSilo: "domain was compromised a few months ago" proven
NameSilo tweets that the domain was "compromised" — reframing 8 years of deliberate operation as an external compromise. Domain registered by "Nathalie Roy" through NameSilo. Server: Apache/2.4.58 (Ubuntu) + PHP 8.2.29 at IP 186.2.165.49 (DDoS-Guard AS59692, Russia).
05
Victim Pattern — Consistent with Architecture
Real reports match the passive-theft model exactly. Theft happened without the victim being online.
Report type A
"Deposited 20 XMR. Next day — 0. No outgoing transaction in history."
Matches isnew=0: viewKey registered at login → incoming deposit detected by server node → swept after 10-block locktime (~20 min) → victim never re-logged in. History empty because server signed the sweep TX, not the victim.
Report type B
"Created wallet, deposited 590 XMR, left for 2 days, returned to 0."
Matches isnew=1: server knew the seed at wallet creation → both victim and server could sign transactions → server swept the full balance within the 2-day window. Victim's paper backup is real but useless — operator has the same keys.
Why offline mode doesn't help: The "disconnect before generating seed" recommendation on the create page is displayed prominently. It changes nothing. The seed is captured the moment the victim logs back in via auth.php. Whether the seed was generated online or offline, the viewKey arrives at the server on first login. The offline recommendation creates a false sense of security — the victim took precautions, therefore the theft must have been something else.
Investigation  •  Operator Panel Demo  •  Wallet Demo  •  PhishDestroy.io
Evidence: network capture 2026-02-18 • URLScan 2025-12-11, 2026-02-21 • NameSilo tweet 2026-03-13 • Deleted GitHub issue #35
PhishDestroy anti-phishing research • All reconstructed demo data is synthetic • Real viewKeys from capture are included as evidence only