Runs Google's BotGuard engine in a jsdom sandbox (no Chrome, no browser) to mint real reCAPTCHA
tokens. The engine ships as a Closure-compiled JS blob that runs a custom bytecode VM with ~2048
registers and 42 opcodes. Instead of reimplementing the VM, this runs Google's own code inside a
fidelity shim: real Chrome fingerprint values, a same-realm MessageChannel polyfill, a WebGL
stub, and a Worker bridge so the engine's postMessage handshake completes. It does real network
calls to Google's /recaptcha/api2/clr endpoint with a protobuf body and gets back a valid token.
v3 works fully: tokens are Google-accepted (success: true, correct hostname/action). But
they score ~0.2-0.3. A fresh incognito browser scores the same band. The gap to
usable scores (0.5+) is i think profile reputation (aged cookies, browsing history), which can't be
synthesized in code.
v2 is a proof-of-concept: proves the browserless anchor flow (render widget, click checkbox,
fire /reload, set up the bframe iframe, capture the audio challenge URL) but does not solve the
image/audio challenge itself.
Fetch a fresh anchor for your sitekey:
$ SITEKEY=6L... ORIGIN=https://example.com OUT=live/v3-mine node fetch-anchor-generic.mjs
[api.js] 200 -> 1582B
version: XOqlk8PL_yVx6IdpLbpXdiLy | origin: https://example.com
[anchor] 200 -> 54906B
anchor valid=true error=false
[engine] 200 -> 823536B
[webworker] 200 -> 102B
saved -> live/v3-mine (valid=true)
Mint a token:
$ V3_DIR=live/v3-mine V3_ACTION=submit node live-v3-single.mjs
[load] parent execute= function | anchor set up= true
========== V3 SINGLE-JSDOM RESULT ==========
execute: RESOLVED len=2254 0cAFcWeA7Hcdbe3v9rqX9CaSgAX8...
log: 103
P.pm recaptcha-setup
ch1 p1.addEL
ch1 p1.start
Worker() created
anchor set up; payload=35944B initErr=none
...
anchor token timeline:
300ms len=1956 03AFcWeA51B_8PNwv4FazEQvzRFC...
NETWORK (anchor, real): 3
POST https://www.google.com/recaptcha/api2/clr?k=6L... 2115B
-> 200
errs: 0
FINAL anchor token: 1956 03AFcWeA51B_8PNwv4FazEQvzRFC...
Token is saved to dump/v3-token.txt.
Score it via Google's siteverify (needs the key's secret):
$ SECRET=6L... node siteverify.mjs
==> SCORE = 0.2 action=submit host=example.com
Two bytecodes are loaded from the /anchor response: the first is a literal init (strings,
integers, the encrypted key table, 36KB), the second is the actual program (fingerprint
collection, value generation, encryption). Both are base64-decoded then XOR-decrypted with a
seed derived from two keys.
String decryption uses two integer keys and a custom character table: each codepoint is XOR'd
with key1, then MOD'd with key2, then mapped to a character from the table. The encryption of the
final payload uses an LCG (linear congruential generator) seeded with performance.now() so
replays are time-locked.
For a deeper breakdown of the VM internals (opcode table, variant reader, LCG math, signal list), search GitHub for reCAPTCHA VM reversing.
npm install
# 1. fetch a fresh anchor for your sitekey
SITEKEY=6L... ORIGIN=https://yourdomain.com OUT=live/v3-mine node fetch-anchor-generic.mjs
# 2. mint a v3 token
V3_DIR=live/v3-mine V3_ACTION=submit node live-v3-single.mjs
# 3. score it (needs the key's secret)
SECRET=6L... node siteverify.mjs
For v2 (proof-of-concept, captures the anchor flow + audio challenge URL):
SITEKEY=6L... ORIGIN=https://yourdomain.com OUT=live/v2-mine node fetch-anchor-generic.mjs
V2_DIR=live/v2-mine node live-v2-single.mjs
| Env var | Default | Description |
|---|---|---|
SITEKEY |
required | reCAPTCHA sitekey (fetch-anchor-generic) |
ORIGIN |
http://localhost |
Target origin the sitekey is registered to |
OUT |
live/v3-own |
Output directory for the fetched anchor |
V3_DIR |
live/v3 |
Anchor directory to use for minting |
V3_ACTION |
submit |
Action string for grecaptcha.execute |
V3_TLS |
off | Set 1 to route network through node-tls-client (Chrome JA3) |
V3_TLS_PROXY |
off | Set 1 to route through an external TLS proxy on localhost:407 |
V3_CHROMEHDRS |
off | Set 1 to send Chrome client-hint headers on network requests |
V3_COOKIE |
off | Cookie string to send with network requests |
V3_STEALTH |
off | Set 1 to make overridden functions report [native code] |
V3_APIS |
off | Set 1 to fill missing browser APIs (AudioContext, RTC, etc.) |
V3_REALCANVAS |
off | Set 1 to use real canvas 2D instead of the stub |
V3_BEHAVIOR |
off | Set 1 to inject mouse/scroll/key events |
V3_SETTLE |
off | Wait N ms before execute (let the engine settle) |
V3_NOFID |
off | Set 1 for a deliberately bot-like profile (negative control) |
SECRET |
required | reCAPTCHA secret key (siteverify) |
| File | Description |
|---|---|
live-v3-single.mjs |
v3 token minter (single jsdom realm) |
live-v2-single.mjs |
v2 proof-of-concept (anchor + bframe + audio capture) |
fetch-anchor-generic.mjs |
fetch a fresh anchor + engine + webworker for any sitekey |
siteverify.mjs |
score a token via Google's siteverify |
dump/real-fp.json |
harvested Chrome fingerprint fed into the sandbox shim |
The v3 score is built from the real browser environment's collected signals (canvas/WebGL rendering, layout/paint timing, event streams, worker computation). jsdom can't generate those. Exhaustively tested: fingerprint fidelity, behavior injection, native-toString stealth, real 2D canvas, Chrome cookies, Chrome client-hint headers, TLS impersonation, missing-API fill. Only the worker handshake moved the score (0.1 to 0.2). None of the others helped.
Reference points: the VM scores ~0.2-0.3. A fresh incognito browser scores 0.1-0.3 (same band). An aged-profile browser scores 0.7-0.9 (reputation). The ceiling is reputation, not environment.
MIT. See LICENSE.
Telegram: @HK407.
For a working headless version with 0.6-0.9 scores, contact @B00H0.