ReCAPTCHA Solver

Automatically solve ReCAPTCHA, adapted from Wikidepia's rektCaptcha

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         ReCAPTCHA Solver
// @namespace    http://tampermonkey.net/
// @version      2026-02-26.1
// @description  Automatically solve ReCAPTCHA, adapted from Wikidepia's rektCaptcha
// @author       tigeryu8900
// @match        *://*/*
// @icon         https://upload.wikimedia.org/wikipedia/commons/a/ad/RecaptchaLogo.svg
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.min.js

// @grant        GM_getResourceURL
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      cdn.jsdelivr.net
// ==/UserScript==

/* global ort, Jimp */

'use strict';

if (self === top) {
  function registerBooleanSetting(key, description, defaultValue) {
    const onText = `${description}: on`;
    const offText = `${description}: off`;
    let id;

    function enable() {
      GM_setValue(key, true);
      id = GM_registerMenuCommand(onText, disable);
    }

    function disable() {
      GM_setValue(key, false);
      id = GM_registerMenuCommand(offText, enable);
    }

    id = GM_getValue(key, defaultValue)
      ? GM_registerMenuCommand(onText, disable)
      : GM_registerMenuCommand(offText, enable);
  }

  function registerIntSetting(key, description, defaultValue) {
    let id;

    function set() {
      const value = parseInt(prompt(description), 10);
      if (!isNaN(value)) {
        GM_setValue(key, value);
        id = GM_registerMenuCommand(`${description}: ${value}`, set);
      }
    }

    id = GM_registerMenuCommand(`${description}: ${GM_getValue(key, defaultValue)}`, set);
  }

  registerBooleanSetting('recaptcha_auto_open', 'Auto Open', false);
  registerBooleanSetting('recaptcha_auto_solve', 'Auto Solve', true);
  registerIntSetting('recaptcha_click_delay_time', 'Click Delay Time (ms)', 0);
  registerIntSetting('recaptcha_solve_delay_time', 'Solve Delay Time (ms)', 0);
}

window.addEventListener('message', e => {
  if (e.data === '__RECAPTCHA_SOLVER__') {
    const port = e.ports && e.ports[0];
    if (!port) return;

    port.onmessage = e => {
      switch (e.data.q) {
        case 'recaptcha_image_visible':
          port.postMessage({
            id: e.data.id,
            value: Array.prototype.some.call(
              document.querySelectorAll(
                'iframe[src*="/recaptcha/api2/bframe"], iframe[src*="/recaptcha/enterprise/bframe"]'
              ),
              div => getComputedStyle(div).visibility === 'visible'
            ),
          });
          break;

        case 'recaptcha_widget_visible':
          port.postMessage({
            id: e.data.id,
            value: Array.prototype.some.call(
              document.querySelectorAll(
                'iframe[src*="/recaptcha/api2/anchor"], iframe[src*="/recaptcha/enterprise/anchor"]'
              ),
              div => getComputedStyle(div).visibility === 'visible'
            ),
          });
          break;
      }
    };

    port.postMessage('__RECAPTCHA_SOLVER__');
  }
});

if (/^\w+:\/\/\w+\.(google\.com|recaptcha\.net)\/recaptcha\/(api2|enterprise)\//.test(location.href)) {
  const queryQueue = {};

  const portPromise = new Promise(resolve => {
    const { port1, port2 } = new MessageChannel();

    port1.onmessage = e => {
      if (e.data === '__RECAPTCHA_SOLVER__') {
        port1.onmessage = e => {
          if (queryQueue[e.data.id]) {
            queryQueue[e.data.id](e.data.value);
          }
        };
        resolve(port1);
      }
    };

    window.parent.postMessage('__RECAPTCHA_SOLVER__', '*', [port2]);
  });

  async function query(q) {
    const port = await portPromise;
    const id = Math.random().toString().substring(2);

    return new Promise(resolve => {
      queryQueue[id] = resolve;
      port.postMessage({ id, q });
    });
  }

  class Time {
    static time() {
      if (!Date.now) {
        Date.now = () => new Date().getTime();
      }
      return Date.now();
    }

    static sleep(i = 1000) {
      return new Promise(resolve => setTimeout(resolve, i));
    }

    static async random_sleep(min, max) {
      const duration = Math.floor(Math.random() * (max - min) + min);
      return await Time.sleep(duration);
    }
  }

  function SimulateMouseClick(element, clientX = null, clientY = null) {
    if (clientX === null || clientY === null) {
      const box = element.getBoundingClientRect();
      clientX = box.left + box.width / 2;
      clientY = box.top + box.height / 2;
    }

    if (isNaN(clientX) || isNaN(clientY)) {
      return;
    }

    // Send mouseover, mousedown, mouseup, click, mouseout
    [
      'mouseover',
      'mouseenter',
      'mousedown',
      'mouseup',
      'click',
      'mouseout',
    ].forEach(eventName => {
      const event = new MouseEvent(eventName, {
        detail: 1 - (eventName === 'mouseover'),
        bubbles: true,
        cancelable: true,
        clientX: clientX,
        clientY: clientY,
      });
      element.dispatchEvent(event);
    });
  }

  function imageDataToTensor(image, dims, normalize = true) {
    // 1. Get buffer data from image and extract R, G, and B arrays.
    var imageBufferData = image.bitmap.data;
    const [redArray, greenArray, blueArray] = [[], [], []];

    // 2. Loop through the image buffer and extract the R, G, and B channels
    for (let i = 0; i < imageBufferData.length; i += 4) {
      redArray.push(imageBufferData[i]);
      greenArray.push(imageBufferData[i + 1]);
      blueArray.push(imageBufferData[i + 2]);
    }

    // 3. Concatenate RGB to transpose [224, 224, 3] -> [3, 224, 224] to a number array
    const transposedData = redArray.concat(greenArray, blueArray);

    // 4. Convert to float32 and normalize to 1
    const float32Data = new Float32Array(transposedData.map(x => x / 255.0));

    // 5. Normalize the data mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
    if (normalize) {
      const mean = [0.485, 0.456, 0.406];
      const std = [0.229, 0.224, 0.225];
      for (let i = 0; i < float32Data.length; i++) {
        float32Data[i] = (float32Data[i] - mean[i % 3]) / std[i % 3];
      }
    }

    // 6. Create a tensor from the float32 data
    const inputTensor = new ort.Tensor('float32', float32Data, dims);
    return inputTensor;
  }

  function overflowBoxes(box, maxSize) {
    return [
      Math.max(box[0], 0),
      Math.max(box[1], 0),
      Math.min(box[2], maxSize - Math.max(box[0], 0)),
      Math.min(box[3], maxSize - Math.max(box[1], 0)),
    ];
  };

  function is_widget_frame() {
    return document.querySelector('.recaptcha-checkbox');
  }

  function is_image_frame() {
    return document.querySelector('#rc-imageselect');
  }

  function open_image_frame() {
    SimulateMouseClick(document.querySelector('#recaptcha-anchor'));
  }

  function is_invalid_config() {
    return document.querySelector('.rc-anchor-error-message');
  }

  function is_rate_limited() {
    return document.querySelector('.rc-doscaptcha-header');
  }

  function is_solved() {
    // Note: verify button is disabled after clicking and during transition to the next image task
    return document.querySelector('.recaptcha-checkbox[aria-checked="true"], #recaptcha-verify-button[disabled]');
  }

  function on_images_ready(timeout = 15000) {
    return new Promise(async resolve => {
      const start = Time.time();
      while (true) {
        const $tiles = document.querySelectorAll('.rc-imageselect-tile');
        const $loading = document.querySelectorAll('.rc-imageselect-dynamic-selected');
        const is_loaded = $tiles.length && !$loading.length;
        if (is_loaded) {
          return resolve(true);
        }
        if (Time.time() - start > timeout) {
          return resolve(false);
        }
        await Time.sleep(100);
      }
    });
  }

  function get_image_url($e) {
    return $e?.src?.trim();
  }

  async function get_task(task_lines) {
    let task = null;
    if (task_lines.length > 1) {
      task = task_lines.slice(0, 2).join(' ');
      task = task.replace(/\s+/g, ' ')?.trim();
    } else {
      task = task_lines.join('\n');
    }
    if (!task) {
      return null;
    }
    return task;
  }

  let last_urls_hash = null;
  function on_task_ready(interval = 500) {
    return new Promise(resolve => {
      let checking = false;
      const check_interval = setInterval(async () => {
        if (checking) {
          return;
        }
        checking = true;

        const task_lines = document
        .querySelector('.rc-imageselect-instructions')
        ?.innerText?.split('\n');
        let task = await get_task(task_lines);
        if (!task) {
          checking = false;
          return;
        }

        const is_hard = task_lines.length === 3 ? true : false;

        const $cells = document.querySelectorAll('table tr td');
        if ($cells.length !== 9 && $cells.length !== 16) {
          checking = false;
          return;
        }

        const cells = [];
        const urls = Array($cells.length).fill(null);
        let background_url = null;
        let has_secondary_images = false;
        let i = 0;
        for (const $e of $cells) {
          const $img = $e?.querySelector('img');
          if (!$img) {
            checking = false;
            return;
          }

          const url = get_image_url($img);
          if (!url || url === '') {
            checking = false;
            return;
          }

          if ($img.naturalWidth >= 300) {
            background_url = url;
          } else if ($img.naturalWidth === 100) {
            urls[i] = url;
            has_secondary_images = true;
          }

          cells.push($e);
          i++;
        }
        if (has_secondary_images) {
          background_url = null;
        }

        const urls_hash = JSON.stringify([background_url, urls]);
        if (last_urls_hash === urls_hash) {
          checking = false;
          return;
        }
        last_urls_hash = urls_hash;

        clearInterval(check_interval);
        checking = false;
        return resolve({ task, is_hard, cells, background_url, urls });
      }, i);
    });
  }

  function submit() {
    SimulateMouseClick(document.querySelector('#recaptcha-verify-button'));
  }

  function reload() {
    SimulateMouseClick(document.querySelector('#recaptcha-reload-button'));
  }


  function got_solve_incorrect() {
    const errors = [
      '.rc-imageselect-incorrect-response', // try again
    ];
    for (const e of errors) {
      if (document.querySelector(e)?.style.display === '') {
        return true;
      }
    }
    return false;
  }

  function got_solve_error() {
    // <div aria-live="polite">
    //     <div class="rc-imageselect-error-select-more" style="" tabindex="0">Please select all matching images.</div>
    //     <div class="rc-imageselect-error-dynamic-more" style="display:none">Please also check the new images.</div>
    //     <div class="rc-imageselect-error-select-something" style="display:none">Please select around the object, or reload if there are none.</div>
    // </div>

    const errors = [
      '.rc-imageselect-error-select-more', // select all matching images
      '.rc-imageselect-error-dynamic-more', // please also check the new images
      '.rc-imageselect-error-select-something', // select around the object or reload
    ];
    for (const e of errors) {
      const $e = document.querySelector(e);
      if (!$e?.style.display || !$e?.tabIndex) {
        return true;
      }
    }
    return false;
  }

  function is_cell_selected($cell) {
    try {
      return $cell.classList.contains('rc-imageselect-tileselected');
    } catch {}
    return false;
  }

  function softmax(x) {
    const e_x = x.map(Math.exp);
    const sum_e_x = e_x.reduce((a, b) => a + b, 0);
    return e_x.map(v => v / sum_e_x);
  }

  let was_solved = false;
  let was_incorrect = false;
  let solved_urls = [];

  async function on_widget_frame() {
    // Check if parent frame marked this frame as visible on screen
    if (!await query('recaptcha_widget_visible')) {
      return;
    }

    // Wait if already solved
    if (is_solved()) {
      if (!was_solved) {
        was_solved = true;
      }
      return;
    }
    was_solved = false;
    await Time.sleep(500);
    open_image_frame();
  }

  async function on_image_frame() {
    // Check if parent frame marked this frame as visible on screen
    if (!await query('recaptcha_image_visible')) {
      return;
    }

    // Wait if verify button or rate limited or invalid config
    if (is_solved() || is_rate_limited() || is_invalid_config()) {
      return;
    }

    // Incorrect solution
    if (!was_incorrect && got_solve_incorrect()) {
      solved_urls = [];
      was_incorrect = true;
    } else {
      was_incorrect = false;
    }

    // Select more images error
    if (got_solve_error()) {
      solved_urls = [];
      return reload();
    }

    // Wait for images to load
    const is_ready = await on_images_ready();
    if (!is_ready) {
      return;
    }

    // Wait for task to be available
    const { task, is_hard, cells, background_url, urls } =
          await on_task_ready();

    const image_urls = [];
    const n = 4 - (cells.length === 9);
    let clickable_cells = [];
    if (background_url === null) {
      urls.forEach((url, i) => {
        if (url && !solved_urls.includes(url)) {
          image_urls.push(url);
          clickable_cells.push(cells[i]);
        }
      });
    } else {
      image_urls.push(background_url);
      clickable_cells = cells;
    }

    const normalizedLabel = {
      bicycles: 'bicycle',
      bridges: 'bridge',
      buses: 'bus',
      cars: 'car',
      chimneys: 'chimney',
      crosswalks: 'crosswalk',
      fire_hydrants: 'fire_hydrant',
      motorcycles: 'motorcycle',
      mountains: 'mountain_or_hill',
      palm_trees: 'palm_tree',
      taxis: 'taxi',
      stairs: 'stair',
      traffic_lights: 'traffic_light',
      tractors: 'tractor',
      vehicles: 'car',
    };

    const modelLabel = [
      'bicycle',
      'bridge',
      'bus',
      'car',
      'chimney',
      'crosswalk',
      'fire_hydrant',
      'motorcycle',
      'mountain_or_hill',
      'palm_tree',
      'parking_meter',
      'stair',
      'taxi',
      'tractor',
      'traffic_light',
    ];

    const data = Array(16).fill(false);
    let label = task
    .match(/(?<=Select all (?:square|image)s with\s+(?:an?\s+)?)(?!an?\s+).*[^ ]/)[0]
    .replaceAll(' ', '_')
    .toLowerCase();
    label = normalizedLabel[label] || label;

    const subImages = [];
    if (!background_url) {
      for (const url of image_urls) {
        subImages.push(await Jimp.read(url).then(img => img.opaque()));
      }
    } else {
      const image = await Jimp.read(background_url).then(img => img.opaque());

      if (n === 4) {
        subImages.push(image);
      } else {
        const cropSize = image.bitmap.width / n;
        for (let i = 0; i < n; i++) {
          for (let j = 0; j < n; j++) {
            subImages.push(
              image.clone().crop({
                x: j * cropSize,
                y: i * cropSize,
                w: cropSize,
                h: cropSize,
              })
            );
          }
        }
      }
    }
    if (!subImages.length) return;
    if (n === 3) {
      const url = GM_getResourceURL(`${label}.ort`);
      if (!url) {
        return reload();
      }

      // Initialize recaptcha detection model
      const classifierSession = await ort.InferenceSession.create(url);

      const outputs = {};
      for (let i = 0; i < subImages.length; i++) {
        const subImage = subImages[i];

        // Resize image to 224x224 with bilinear interpolation
        subImage.resize({
          w: 224,
          h: 224,
          mode: Jimp.RESIZE_BILINEAR
        });

        // Convert image data to tensor
        const input = imageDataToTensor(subImage, [1, 3, 224, 224]);

        // Feed feats to classifier
        const classifierOutputs = await classifierSession.run({ input });
        const output = classifierOutputs[classifierSession.outputNames[0]].data;

        // Find confidence score of output
        const confidence = softmax(output);
        outputs[i] = confidence[1];
      }

      // Sort outputs by confidence
      const sortedOutputs = Object.keys(outputs).sort(
        (a, b) => outputs[b] - outputs[a]
      );

      let possibleTrue = sortedOutputs.filter(idx => outputs[idx] > 0.7);
      if (![3, 4].includes(possibleTrue.length) && subImages.length === 9) {
        // if confidence between 3rd and 4th is smaller than 0.025, then include 4th
        possibleTrue = sortedOutputs.slice(0, 3 + (
          sortedOutputs.length > 3 &&
          outputs[sortedOutputs[2]] - outputs[sortedOutputs[3]] < 0.025
        ));
      } else if (
        // Logic for 2nd try at hard recaptcha
        [3, 4].includes(subImages.length) &&
        !possibleTrue.length
      ) {
        possibleTrue = sortedOutputs.filter(idx => outputs[idx] > 0.6);
      }
      possibleTrue.forEach(idx => (data[idx] = true));
    } else if (n === 4) {
      const imageSize = 320;

      // Initialize recaptcha detection model
      const nmsConfig = new ort.Tensor(
        'float32',
        new Float32Array([10, 0.25, 0.1])
      );
      const [segmentation, mask, nms] = await Promise.all([
        ort.InferenceSession.create(GM_getResourceURL('yolov5-seg.ort')),
        ort.InferenceSession.create(GM_getResourceURL('mask-yolov5-seg.ort')),
        ort.InferenceSession.create(GM_getResourceURL('nms-yolov5-det.ort')),
      ]);

      const inputImage = subImages[0].resize({w: imageSize, h: imageSize});
      const inputTensor = imageDataToTensor(
        inputImage,
        [1, 3, imageSize, imageSize],
        false
      );
      const { output0, output1 } = await segmentation.run({
        images: inputTensor,
      });

      const nmsOutput = await nms.run({
        detection: output0,
        config: nmsConfig,
      });
      const selectedIdx = nmsOutput[nms.outputNames[0]];

      function hexToRgba(hex, alpha) {
        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? [
          parseInt(result[1], 16),
          parseInt(result[2], 16),
          parseInt(result[3], 16),
          alpha,
        ] : null;
      }

      // looping through output
      const gridWidth = imageSize / 4;
      const gridPixel = gridWidth ** 2;
      for (let i = 0; i < selectedIdx.data.length; i++) {
        const idx = selectedIdx.data[i];
        const numClass = modelLabel.length;
        const selectedData = output0.data.slice(
          idx * output0.dims[2],
          (idx + 1) * output0.dims[2]
        );

        const scores = selectedData.slice(5, 5 + numClass);
        let score = Math.max(...scores);
        const labelName = modelLabel[scores.indexOf(score)];
        if (labelName !== label) continue;

        const color = '#FF37C7';
        let box = overflowBoxes(
          [selectedData[0] - 0.5 * selectedData[2], selectedData[1] - 0.5 * selectedData[3], selectedData[2], selectedData[3]],
          imageSize,
        );

        // Create mask overlay
        const detectionTensor = new ort.Tensor(
          'float32',
          new Float32Array([...box, ...selectedData.slice(5 + numClass)])
        );

        const maskConfig = new ort.Tensor(
          'float32',
          new Float32Array([
            imageSize,
            ...box,
            ...hexToRgba(color, 120),
          ])
        );

        const maskOutput = await mask.run({
          detection: detectionTensor,
          mask: output1,
          config: maskConfig,
        });
        const maskFilter = maskOutput[mask.outputNames[0]];

        // Create mask in JIMP
        const maskImage = new Jimp({
          width: imageSize,
          height: imageSize
        });
        maskImage.bitmap.data = maskFilter.data;

        // Get how much percentage of mask in each grid
        const gridMask = Array(16).fill(0);
        maskImage.scan(0, 0, imageSize, imageSize, function (x, y, idx) {
          const gridX = Math.floor(x / gridWidth);
          const gridY = Math.floor(y / gridWidth);
          if (this.bitmap.data[idx + 3]) {
            gridMask[(gridY << 2) + gridX] += 1;
          }
        });

        // Convert to percentage if higher than 0.15 set to true
        for (let i = 0; i < 16; i++) {
          const maskPercentage = gridMask[i] / gridPixel;
          if (maskPercentage > 0.1) {
            data[i] = true;
          }
        }
      }
    }

    let clicks = 0;
    for (let i = 0; i < data.length; i++) {
      if (data[i]) {
        clicks++;

        // Click if not already selected
        if (!is_cell_selected(clickable_cells[i])) {
          SimulateMouseClick(clickable_cells[i]);
          await Time.sleep(GM_getValue('recaptcha_click_delay_time', 0));
        }
      }
    }

    for (const url of urls) {
      solved_urls.push(url);
      if (solved_urls.length > 9) {
        solved_urls.shift();
      }
    }

    await Time.sleep(GM_getValue('recaptcha_solve_delay_time', 0));
    if (
      (n === 3 && is_hard && !clicks && (await on_images_ready())) ||
      (n === 3 && !is_hard) ||
      (n === 4 && clicks)
    ) {
      await Time.sleep(200);
      return submit();
    } else if (n === 4 && !clicks) {
      return reload();
    }
  }

  (async () => {
    // Modify ort wasm path
    ort.env.wasm.wasmPaths = Object.fromEntries(await Promise.all(Object.entries({
      mjs: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.jsep.mjs',
      wasm: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.jsep.wasm',
    }).map(([key, url]) => new Promise((resolve, reject) => GM_xmlhttpRequest({
      method: 'GET',
      url,
      responseType: 'blob',
      onload({ response }) {
        resolve([key, URL.createObjectURL(response)]);
      },
      onabort: reject,
      onerror: reject,
      ontimeout: reject,
    })))));

    while (true) {
      await Time.sleep(1000);

      if (is_widget_frame() && GM_getValue('recaptcha_auto_open', false)) {
        await on_widget_frame();
      } else if (is_image_frame() && GM_getValue('recaptcha_auto_solve', true)) {
        await on_image_frame();
      }
    }
  })();
}