'use strict'; /** * hCaptcha Solver (Node.js) * * 完整流程: * checksiteconfig → hsw(req) 计算 n → 构建加密体 → getcaptcha → 解密响应 → 拿 token * * 使用 hsw.js 在 Node 沙盒中运行(全局污染方式) */ const vm = require('vm'); const { readFileSync } = require('fs'); const { join } = require('path'); const msgpack = require('msgpack-lite'); const windowMock = require('./sandbox/mocks/window'); const { applySandboxPatches } = require('./sandbox/mocks/index'); const { Logger } = require('./utils/logger'); const logger = new Logger('hcaptcha_solver'); // 保存原始 fetch(供真实网络请求使用) const realFetch = globalThis.fetch; // ── 常量 ────────────────────────────────────────────────────── const HCAPTCHA_API = 'https://api.hcaptcha.com'; const HCAPTCHA_JS = 'https://js.hcaptcha.com/1/api.js'; const HCAPTCHA_CDN = 'https://newassets.hcaptcha.com'; // ── 伪造 motion data ────────────────────────────────────────── function generateMotionData() { const now = Date.now(); const st = now - 3000 - Math.floor(Math.random() * 1000); // 随机鼠标轨迹 const mm = []; let x = 200 + Math.floor(Math.random() * 100); let y = 300 + Math.floor(Math.random() * 100); const steps = 15 + Math.floor(Math.random() * 10); for (let i = 0; i < steps; i++) { x += Math.floor(Math.random() * 6) - 3; y += Math.floor(Math.random() * 6) - 3; mm.push([x, y, st + i * (80 + Math.floor(Math.random() * 40))]); } return { st, dct: st + 200 + Math.floor(Math.random() * 100), mm, 'mm-mp': 15.42857142857143, md: [[mm[0][0], mm[0][1], mm[0][2] + 500]], 'md-mp': 0, mu: [[mm[0][0], mm[0][1], mm[0][2] + 600]], 'mu-mp': 0, kd: [], 'kd-mp': 0, ku: [], 'ku-mp': 0, topLevel: { st, sc: { availWidth: 1920, availHeight: 1040, width: 1920, height: 1080, colorDepth: 24, pixelDepth: 24, availLeft: 0, availTop: 0, }, nv: { hardwareConcurrency: 8, deviceMemory: 8, }, dr: '', inv: false, exec: false, }, v: 1, }; } // ── HSW 沙盒管理 ────────────────────────────────────────────── class HswBridge { constructor() { this.hswFn = null; this.initialized = false; this._ctx = null; } /** * 构建 vm 沙盒上下文,将 mock window 注入隔离环境 * @param {object} fingerprint - 指纹覆盖 */ _buildContext(fingerprint) { const ctx = Object.create(null); // 把 windowMock 上所有 key 复制进 ctx(浅拷贝) for (const key of Reflect.ownKeys(windowMock)) { try { ctx[key] = windowMock[key]; } catch (_) {} } // vm 必需的自引用 ctx.global = ctx; ctx.globalThis = ctx; ctx.window = ctx; ctx.self = ctx; // 透传 console(调试用) ctx.console = console; // 保证 Promise / 定时器在 vm 里可用 ctx.Promise = Promise; ctx.setTimeout = setTimeout; ctx.clearTimeout = clearTimeout; ctx.setInterval = setInterval; ctx.clearInterval = clearInterval; ctx.queueMicrotask = queueMicrotask; // 应用指纹覆盖 if (fingerprint.userAgent && ctx.navigator) { ctx.navigator.userAgent = fingerprint.userAgent; ctx.navigator.appVersion = fingerprint.userAgent.replace('Mozilla/', ''); } if (fingerprint.platform && ctx.navigator) { ctx.navigator.platform = fingerprint.platform; } if (fingerprint.host && ctx.location?.ancestorOrigins) { ctx.location.ancestorOrigins[0] = `https://${fingerprint.host}`; } const vmCtx = vm.createContext(ctx); // Apply escape defense + error stack rewriting AFTER context creation applySandboxPatches(vmCtx); return vmCtx; } /** * 在 vm 沙盒中加载并执行 hsw.js * @param {string} hswPath - hsw.js 文件路径 * @param {object} fingerprint - 指纹覆盖 */ async init(hswPath, fingerprint = {}) { if (this.initialized) return; const code = readFileSync(hswPath, 'utf-8'); logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`); const ctx = this._buildContext(fingerprint); this._ctx = ctx; const script = new vm.Script(code, { filename: 'hsw.js' }); try { script.runInContext(ctx, { timeout: 10000 }); } catch (err) { logger.error(`hsw.js 执行失败: ${err.message}`); throw err; } // 查找 hsw 函数 if (typeof ctx.hsw === 'function') { this.hswFn = ctx.hsw; } else if (typeof ctx.window?.hsw === 'function') { this.hswFn = ctx.window.hsw; } if (!this.hswFn) { throw new Error('hsw function not found after execution'); } this.initialized = true; logger.success('Bridge 已就绪 (vm 沙盒)'); } /** 计算 PoW n 值: hsw(req_jwt_string) */ async getN(req) { return this.hswFn(req); } /** 加密请求体: hsw(1, data) */ async encrypt(data) { return this.hswFn(1, data); } /** 解密响应体: hsw(0, data) */ async decrypt(data) { return this.hswFn(0, data); } } // ── HTTP 工具 ───────────────────────────────────────────────── const DEFAULT_HEADERS = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9', 'sec-ch-ua': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Linux"', }; async function httpGet(url, headers = {}) { const resp = await realFetch(url, { method: 'GET', headers: { ...DEFAULT_HEADERS, ...headers }, }); const text = await resp.text(); logger.info(`HTTP GET ${url.substring(0, 80)}... → ${resp.status}`); return { status: resp.status, text, headers: resp.headers }; } async function httpPost(url, body, headers = {}) { const isBuffer = body instanceof Uint8Array || Buffer.isBuffer(body); const opts = { method: 'POST', headers: { ...DEFAULT_HEADERS, ...(isBuffer ? { 'Content-Type': 'application/octet-stream' } : { 'Content-Type': 'application/x-www-form-urlencoded' }), ...headers, }, body: isBuffer ? body : body, }; const resp = await realFetch(url, opts); logger.info(`HTTP POST ${url.substring(0, 80)}... → ${resp.status}`); return resp; } // ── 主求解器 ────────────────────────────────────────────────── class HCaptchaSolver { /** * @param {object} opts * @param {string} opts.sitekey * @param {string} opts.host - 嵌入 hCaptcha 的站点域名 * @param {string} [opts.rqdata] - 可选附加数据 * @param {string} [opts.hswPath] - hsw.js 本地路径 */ constructor(opts) { this.sitekey = opts.sitekey; this.host = opts.host; this.rqdata = opts.rqdata || null; this.hswPath = opts.hswPath || join(__dirname, '../asset/hsw.js'); this.bridge = new HswBridge(); this.version = null; } /** 1. 从 api.js 获取最新版本 hash */ async fetchVersion() { const { text } = await httpGet(HCAPTCHA_JS); // 尝试多种模式匹配版本 hash const patterns = [ /captcha\/v1\/([a-f0-9]{40})\//i, // captcha/v1/HASH/ /\/c\/([a-f0-9]{40})\//i, // /c/HASH/ /v=([a-f0-9]{40})/i, // v=HASH /["']([a-f0-9]{40})["']/, // 直接引用的40位hash ]; for (const pat of patterns) { const m = text.match(pat); if (m) { this.version = m[1]; logger.info(`获取到最新 hCaptcha 版本: ${this.version}`); return this.version; } } throw new Error('无法从 api.js 提取版本 hash'); } /** 2. 初始化 Bridge(加载 hsw.js 到沙盒) */ async initBridge() { let hswCode; // 优先用本地 hsw.js try { readFileSync(this.hswPath); hswCode = this.hswPath; logger.info(`使用本地 hsw.js: ${this.hswPath}`); } catch { // 从 CDN 下载 if (!this.version) await this.fetchVersion(); const url = `${HCAPTCHA_CDN}/c/${this.version}/hsw.js`; logger.info(`从 CDN 下载 hsw.js: ${url}`); const { text } = await httpGet(url); // 写入临时文件 const tmp = join(__dirname, '../asset/hsw_latest.js'); require('fs').writeFileSync(tmp, text); hswCode = tmp; logger.info(`hsw.js 已保存: ${tmp}`); } await this.bridge.init(hswCode, { host: this.host, }); } /** 3. checksiteconfig */ async checkSiteConfig() { if (!this.version) await this.fetchVersion(); const params = new URLSearchParams({ v: this.version, host: this.host, sitekey: this.sitekey, sc: '1', swa: '1', spst: '0', }); const t0 = Date.now(); const url = `${HCAPTCHA_API}/checksiteconfig?${params}`; const resp = await realFetch(url, { method: 'POST', headers: { ...DEFAULT_HEADERS, 'Content-Type': 'application/x-www-form-urlencoded', 'Origin': 'https://newassets.hcaptcha.com', 'Referer': 'https://newassets.hcaptcha.com/', }, body: params.toString(), }); const data = await resp.json(); const dur = Date.now() - t0; logger.info(`HTTP POST ${url.substring(0, 80)}... → ${resp.status}`); // 提取 set-cookie(hmt_id 等),后续请求需要带上 const rawCookie = resp.headers.get('set-cookie') || ''; const hmtMatch = rawCookie.match(/hmt_id=[^;]+/); this.cookie = hmtMatch ? hmtMatch[0] : ''; if (this.cookie) logger.info(`获取到 cookie: ${this.cookie}`); const pass = data.pass !== false; const cType = data.c?.type || 'unknown'; logger.info(`checksiteconfig: pass=${pass}, type=${cType}, duration=${dur}ms`); if (!data.c) { throw new Error(`checksiteconfig 缺少 challenge 字段: ${JSON.stringify(data)}`); } return data; } /** 4. 构建并加密请求体 */ async buildEncryptedBody(challenge) { // 计算 PoW n 值 logger.info('计算 PoW n 值...'); const n = await this.bridge.getN(challenge.c.req); logger.info(`n = ${typeof n === 'string' ? n.substring(0, 40) + '...' : n}`); // 构建 payload const motionData = generateMotionData(); const payload = { v: this.version, sitekey: this.sitekey, host: this.host, hl: 'en', motionData: JSON.stringify(motionData), n, c: JSON.stringify(challenge.c), pst: false, }; // 如果有 rqdata,加入 if (this.rqdata) { payload.rqdata = this.rqdata; } logger.info('构建加密请求体...'); // ── 正确的加密流程(与 flow_manager.js / h.html 一致)── // Step 1: 克隆 payload,去掉 c 字段(c 会单独放在外层) const payloadClone = { ...payload }; const cValue = payloadClone.c; delete payloadClone.c; // Step 2: msgpack 编码后再加密: hsw(1, msgpack.encode(payload_without_c)) const msgpackPayload = msgpack.encode(payloadClone); logger.info(`msgpack 编码大小: ${msgpackPayload.length} bytes`); const encrypted = await this.bridge.encrypt(msgpackPayload); // 诊断:encrypted 的类型和内容 const etype = typeof encrypted; const ector = encrypted?.constructor?.name || 'unknown'; logger.info(`encrypt 返回类型: typeof=${etype}, constructor=${ector}`); if (encrypted instanceof Uint8Array || Buffer.isBuffer(encrypted)) { logger.info(`encrypt 返回 Uint8Array/Buffer, length=${encrypted.length}`); } else if (typeof encrypted === 'string') { logger.info(`encrypt 返回 string, length=${encrypted.length}`); } else { logger.info(`encrypt 返回: ${JSON.stringify(encrypted)?.substring(0, 200)}`); } if (!encrypted) { throw new Error('hsw encrypt 返回 null/undefined'); } // Step 3: 二次打包: body = msgpack.encode([JSON.stringify(c), encrypted_bytes]) const cString = typeof cValue === 'string' ? cValue : JSON.stringify(cValue); const body = msgpack.encode([cString, encrypted]); logger.info(`最终 body 大小: ${body.length} bytes, first10=[${Array.from(body.slice(0, 10)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(',')}]`); // 导出加密体供 curl_cffi 测试 require('fs').writeFileSync('body.bin', body); logger.info('已导出加密体到 body.bin'); return body; } /** 5. getcaptcha → 拿 token */ async getCaptcha(encryptedBody) { const url = `${HCAPTCHA_API}/getcaptcha/${this.sitekey}`; const resp = await realFetch(url, { method: 'POST', headers: { ...DEFAULT_HEADERS, 'Accept': 'application/json, application/octet-stream', 'Content-Type': 'application/octet-stream', 'Origin': 'https://newassets.hcaptcha.com', 'Referer': 'https://newassets.hcaptcha.com/', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-site', 'sec-fetch-storage-access': 'none', 'priority': 'u=1, i', ...(this.cookie ? { 'Cookie': this.cookie } : {}), }, body: encryptedBody, }); logger.info(`HTTP POST ${url} → ${resp.status}`); logger.info(`响应 content-type: ${resp.headers.get('content-type')}`); if (!resp.ok) { const errText = await resp.text(); logger.error(`getcaptcha 失败 (${resp.status}): ${errText.substring(0, 200)}`); throw new Error(`getcaptcha HTTP ${resp.status}: ${errText.substring(0, 200)}`); } // 尝试解密响应 const contentType = resp.headers.get('content-type') || ''; let result; if (contentType.includes('json')) { // 明文 JSON 响应 result = await resp.json(); } else { // 二进制加密响应 const rawBuf = await resp.arrayBuffer(); const raw = new Uint8Array(rawBuf); logger.info('解密响应...'); const decrypted = await this.bridge.decrypt(raw); try { result = msgpack.decode(decrypted); } catch { // 可能是 JSON 字符串 result = JSON.parse(new TextDecoder().decode(decrypted)); } } const pass = result.pass || result.generated_pass_UUID; logger.info(`getcaptcha 结果: pass=${!!pass}`); return result; } /** 完整求解流程 */ async solve() { logger.info(`开始求解 sitekey=${this.sitekey.substring(0, 12)}... host=${this.host}`); const t0 = Date.now(); try { // 步骤 1: 获取版本 await this.fetchVersion(); // 步骤 2: 初始化 Bridge await this.initBridge(); // 步骤 3: checksiteconfig const config = await this.checkSiteConfig(); // 步骤 4: 构建加密体 const body = await this.buildEncryptedBody(config); // 步骤 5: getcaptcha const result = await this.getCaptcha(body); const token = result.generated_pass_UUID || result.pass; const dur = ((Date.now() - t0) / 1000).toFixed(2); if (token && typeof token === 'string' && token.startsWith('P1_')) { logger.success(`✅ 求解成功! (${dur}s)`); logger.info(`Token: ${token.substring(0, 50)}...`); return { success: true, token, result }; } else if (result.pass === true) { logger.success(`✅ pass=true (${dur}s)`); return { success: true, token: token || true, result }; } else { logger.error(`❌ 求解失败: ${JSON.stringify(result).substring(0, 200)}`); return { success: false, result }; } } catch (err) { logger.error(`求解异常: ${err.message}`); logger.error(err.stack); return { success: false, error: err.message }; } } } module.exports = { HCaptchaSolver, HswBridge, generateMotionData };