ReCAPTCHA Solver

Automatically solve ReCAPTCHA, adapted from Wikidepia's rektCaptcha

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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();
      }
    }
  })();
}