Outsmarting Cloudflare Turnstile CAPTCHA: A Bold Expedition Through Varying Levels of Complexity

In my relentless quest for innovation and business insights, I decided to dive into the perplexing world of CAPTCHA systems. And no, this wasn’t a casual experiment—I set out to verify that my digital assistant could churn out code while I uncovered the secrets behind these tricky puzzles. Sure, there were plenty of ethical disclaimers (the usual corporate mumbo-jumbo), but at the end of the day, I was doing this purely for research, and everyone was in the loop. I focused my attention on Cloudflare’s Turnstile CAPTCHA because I had never encountered this particular beast before. For those who aren’t up to speed, let’s break down what Turnstile is and why bypassing it can be an absolute nightmare: What Exactly is Turnstile CAPTCHA and Why Is It So Damn Tricky to Crack? Cloudflare’s Turnstile is a cutting-edge CAPTCHA system crafted to shield websites from bots without disrupting the user journey. Its genius lies in offering robust security while staying practically invisible—users often aren’t even aware a check is happening in the background. However, in my case, both versions of the Turnstile CAPTCHA were glaringly obvious. There are two versions: The Basic Model: Echoes the familiar style of reCAPTCHA, where all the necessary clues (like the sitekey) are right there in the HTML. Just pop open developer tools and Ctrl + F “sitekey.” The Advanced Challenge: This kicks in when the basic tests don’t quite cut it, layering extra verification steps to double down on security without overwhelming every visitor. With this version, crucial data is hidden in JavaScript, meaning you must intercept the calls—a whole new level of complexity! To test these out, I played with two websites: https://privacy.deepsync.com/ – featuring the straightforward Turnstile CAPTCHA https://crash.chicagopolice.org/ – showcasing the more complex variant The No-Nonsense Approach to the Simple Turnstile CAPTCHA: Python Style Let’s start with the easy part. I scoured the web for “solve Turnstile CAPTCHA” and stumbled upon a renowned CAPTCHA-solving service with a detailed API. Rather than manually writing every line of code, I outsourced the task to my neural network sidekick. Through a series of trial and error, we put together the following solution: import argparse import requests import time from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait, Select from selenium.webdriver.support import expected_conditions as EC def get_turnstile_solution(api_key, site_key, page_url): """ Sends a task to solve the Turnstile CAPTCHA via 2captcha and polls for the result. Returns the solution token (str) or None if something goes wrong. """ in_url = 'http://2captcha.com/in.php' payload = { 'key': api_key, 'method': 'turnstile', 'sitekey': site_key, 'pageurl': page_url, 'json': 1 } try: response = requests.post(in_url, data=payload) result = response.json() except Exception as e: print("Error while sending request to 2captcha:", e) return None if result.get('status') != 1: print("Error submitting task:", result.get('request')) return None captcha_id = result.get('request') print("CAPTCHA solving task submitted, ID:", captcha_id) # Poll the result from 2captcha every 5 seconds res_url = 'http://2captcha.com/res.php' params = { 'key': api_key, 'action': 'get', 'id': captcha_id, 'json': 1 } while True: time.sleep(5) try: res_response = requests.get(res_url, params=params) res_result = res_response.json() except Exception as e: print("Error while retrieving result:", e) return None if res_result.get('status') == 1: solution_token = res_result.get('request') print("CAPTCHA solution received:", solution_token) return solution_token elif res_result.get('request') == "CAPCHA_NOT_READY": print("Solution not ready yet, retrying...") continue else: print("Error retrieving solution:", res_result.get('request')) return None def main(): parser = argparse.ArgumentParser( description='Demonstration of form auto-fill and solving Turnstile CAPTCHA via 2captcha.' ) parser.add_argument('api_key', type=str, nargs='?', help='Your 2captcha API key') parser.add_argument('url', type=str, nargs='?', help='URL of the page with the form and Turnstile CAPTCHA') args = parser.parse_args() if not args.api_key: args.api_key = input("Enter your 2captcha API key: ") if not args.url: args.url = input("Enter the URL of the page with the CAPTCHA: ") # 1) Launch Se

Mar 21, 2025 - 14:52
 0
Outsmarting Cloudflare Turnstile CAPTCHA: A Bold Expedition Through Varying Levels of Complexity

In my relentless quest for innovation and business insights, I decided to dive into the perplexing world of CAPTCHA systems. And no, this wasn’t a casual experiment—I set out to verify that my digital assistant could churn out code while I uncovered the secrets behind these tricky puzzles. Sure, there were plenty of ethical disclaimers (the usual corporate mumbo-jumbo), but at the end of the day, I was doing this purely for research, and everyone was in the loop.

Image description

I focused my attention on Cloudflare’s Turnstile CAPTCHA because I had never encountered this particular beast before. For those who aren’t up to speed, let’s break down what Turnstile is and why bypassing it can be an absolute nightmare:

What Exactly is Turnstile CAPTCHA and Why Is It So Damn Tricky to Crack?

Cloudflare’s Turnstile is a cutting-edge CAPTCHA system crafted to shield websites from bots without disrupting the user journey. Its genius lies in offering robust security while staying practically invisible—users often aren’t even aware a check is happening in the background. However, in my case, both versions of the Turnstile CAPTCHA were glaringly obvious.

There are two versions:

  • The Basic Model: Echoes the familiar style of reCAPTCHA, where all the necessary clues (like the sitekey) are right there in the HTML. Just pop open developer tools and Ctrl + F “sitekey.”

  • The Advanced Challenge: This kicks in when the basic tests don’t quite cut it, layering extra verification steps to double down on security without overwhelming every visitor. With this version, crucial data is hidden in JavaScript, meaning you must intercept the calls—a whole new level of complexity!

To test these out, I played with two websites:

The No-Nonsense Approach to the Simple Turnstile CAPTCHA: Python Style

Let’s start with the easy part. I scoured the web for “solve Turnstile CAPTCHA” and stumbled upon a renowned CAPTCHA-solving service with a detailed API. Rather than manually writing every line of code, I outsourced the task to my neural network sidekick. Through a series of trial and error, we put together the following solution:

import argparse
import requests
import time




from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC




def get_turnstile_solution(api_key, site_key, page_url):
    """
    Sends a task to solve the Turnstile CAPTCHA via 2captcha and polls for the result.
    Returns the solution token (str) or None if something goes wrong.
    """
    in_url = 'http://2captcha.com/in.php'
    payload = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': site_key,
        'pageurl': page_url,
        'json': 1
    }

    try:
        response = requests.post(in_url, data=payload)
        result = response.json()
    except Exception as e:
        print("Error while sending request to 2captcha:", e)
        return None




    if result.get('status') != 1:
        print("Error submitting task:", result.get('request'))
        return None




    captcha_id = result.get('request')
    print("CAPTCHA solving task submitted, ID:", captcha_id)




    # Poll the result from 2captcha every 5 seconds
    res_url = 'http://2captcha.com/res.php'
    params = {
        'key': api_key,
        'action': 'get',
        'id': captcha_id,
        'json': 1
    }




    while True:
        time.sleep(5)
        try:
            res_response = requests.get(res_url, params=params)
            res_result = res_response.json()
        except Exception as e:
            print("Error while retrieving result:", e)
            return None




        if res_result.get('status') == 1:
            solution_token = res_result.get('request')
            print("CAPTCHA solution received:", solution_token)
            return solution_token
        elif res_result.get('request') == "CAPCHA_NOT_READY":
            print("Solution not ready yet, retrying...")
            continue
        else:
            print("Error retrieving solution:", res_result.get('request'))
            return None




def main():
    parser = argparse.ArgumentParser(
        description='Demonstration of form auto-fill and solving Turnstile CAPTCHA via 2captcha.'
    )
    parser.add_argument('api_key', type=str, nargs='?', help='Your 2captcha API key')
    parser.add_argument('url', type=str, nargs='?', help='URL of the page with the form and Turnstile CAPTCHA')
    args = parser.parse_args()




    if not args.api_key:
        args.api_key = input("Enter your 2captcha API key: ")
    if not args.url:
        args.url = input("Enter the URL of the page with the CAPTCHA: ")




    # 1) Launch Selenium in visual mode (without headless) so you can observe the process
    chrome_options = Options()
    driver = webdriver.Chrome(options=chrome_options)
    driver.get(args.url)




    wait = WebDriverWait(driver, 30)




    try:
        # 2) Wait for the element with the class .cf-turnstile to appear
        turnstile_div = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".cf-turnstile"))
        )
    except Exception as e:
        print("Error: element with class .cf-turnstile not found:", e)
        driver.quit()
        return




    # Extract the sitekey from the data-sitekey attribute
    site_key = turnstile_div.get_attribute("data-sitekey")
    print("Sitekey found:", site_key)




    # ----- Step 1: Automatically fill in the form fields -----
    try:
        # Example: select "A deceased individual"
        select_request_type = Select(driver.find_element(By.ID, "request_type"))
        select_request_type.select_by_value("deceased")  

        # First name, last name
        driver.find_element(By.ID, "first_name").send_keys("John")
        driver.find_element(By.ID, "last_name").send_keys("Doe")

        # Email, phone
        driver.find_element(By.ID, "email").send_keys("test@example.com")
        driver.find_element(By.ID, "phone").send_keys("1234567890")

        # Address
        driver.find_element(By.ID, "who_address").send_keys("123 Test Street")
        driver.find_element(By.ID, "who_address2").send_keys("Apt 4")
        driver.find_element(By.ID, "who_city").send_keys("Test City")

        select_state = Select(driver.find_element(By.ID, "who_state"))
        select_state.select_by_value("CA")  # California

        driver.find_element(By.ID, "who_zip").send_keys("90001")

        # Check the "Requests" checkboxes
        driver.find_element(By.ID, "request_type_1").click()  # Do not sell/share my personal information
        driver.find_element(By.ID, "request_type_2").click()  # Do not use my personal data for targeted advertising
        # ... you can select the others if necessary

        print("Form fields have been filled with test data.")
    except Exception as e:
        print("Error auto-filling form fields:", e)
        driver.quit()
        return




    # ----- Step 2: Solve the CAPTCHA via 2captcha -----
    token = get_turnstile_solution(args.api_key, site_key, args.url)
    if not token:
        print("Failed to obtain CAPTCHA solution.")
        driver.quit()
        return




    # ----- Step 3: Insert the solution into the hidden field and call the callback -----
    try:
        # Locate the hidden field that Turnstile uses to store the response
        input_field = wait.until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, 'input#cf-chl-widget-yi26c_response, input[name="cf-turnstile-response"]')
            )
        )
        # Insert the obtained token
        driver.execute_script("arguments[0].value = arguments[1];", input_field, token)
        print("CAPTCHA solution token inserted into the hidden field.")




        # Generate the 'change' event
        driver.execute_script("""
            var event = new Event('change', { bubbles: true });
            arguments[0].dispatchEvent(event);
        """, input_field)




        # If the site uses a callback function for Turnstile, attempt to call it
        driver.execute_script("""
            if (window.tsCallback) {
                window.tsCallback(arguments[0]);
            }
        """, token)
        print("Callback invoked (if it was defined).")




    except Exception as e:
        print("Error inserting CAPTCHA token:", e)
        driver.quit()
        return




    # ----- Step 4: Submit the form to proceed to the next stage -----
    try:
        submit_button = wait.until(
            EC.element_to_be_clickable((By.ID, "submit_button"))
        )
        submit_button.click()
        print("Click on the 'Submit' button executed.")




        # Wait for the URL to change after submission
        wait.until(EC.url_changes(args.url))
        print("Transition to the next stage detected. Current URL:", driver.current_url)




    except Exception as e:
        print("Failed to click 'Submit' or wait for the next stage:", e)




    print("Automation completed. The browser window remains open for verification.")
    input("Press Enter to close the browser...")
    driver.quit()




if __name__ == '__main__':
    main()

What’s brilliant here is that the script is self-contained—just save it in one file, install the required dependencies, and you’re ready to go. All you need to do is install Selenium and the requests library:

pip install selenium requests

Keep in mind: this code is fine-tuned for a specific website mentioned above and not only bypasses the CAPTCHA but also auto-fills the necessary form data.

A Deep Dive into How the Turnstile Bypass Script Operates

Here’s the lowdown:

  1. Gathering Inputs: Using the argparse module, the script prompts for your 2captcha API key and the target URL.

  2. Launching the Browser: It then fires up a browser (not in headless mode—I had to capture every moment on video) and waits for the .cf-turnstile element to appear.

  3. Extracting the Secret: Once the element is detected, the script pulls the data-sitekey attribute—a unique key vital for engaging with the CAPTCHA.

  4. Auto-Filling the Form: Meanwhile, the form fields are automatically populated (this is mainly to ensure the script runs end-to-end).

  5. Token Acquisition: The sitekey is sent to the 2captcha server, which works its magic and returns a solution token. This token is then inserted into a hidden field on the page via an execute_script call.

  6. Triggering the Callback: A change event is dispatched, and if there’s a callback (say, window.tsCallback), it gets triggered. Finally, the script clicks the submit button as soon as it becomes active.

And there you have it—watch the entire process in action:

Not to be boxed in, I later retooled the script to integrate with SolveCaptcha. The mechanics remain unchanged—simply swap your API key for SolveCaptcha’s:

import argparse
import requests
import time


from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC


def get_turnstile_solution(api_key, site_key, page_url):
    """
    Sends a task to solve the Turnstile CAPTCHA via solvecaptcha and polls for the result.
    Returns the solution token (str) or None if something goes wrong.
    """
    # URL to send the task to solvecaptcha
    in_url = 'https://api.solvecaptcha.com/in.php'
    payload = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': site_key,
        'pageurl': page_url,
        'json': 1
    }

    try:
        response = requests.post(in_url, data=payload)
        result = response.json()
    except Exception as e:
        print("Error while sending request to solvecaptcha:", e)
        return None


    if result.get('status') != 1:
        print("Error submitting task:", result.get('request'))
        return None


    captcha_id = result.get('request')
    print("CAPTCHA solving task submitted, ID:", captcha_id)


    # Poll the result from solvecaptcha every 5 seconds
    res_url = 'https://api.solvecaptcha.com/res.php'
    params = {
        'key': api_key,
        'action': 'get',
        'id': captcha_id,
        'json': 1
    }


    while True:
        time.sleep(5)
        try:
            res_response = requests.get(res_url, params=params)
            res_result = res_response.json()
        except Exception as e:
            print("Error while retrieving result:", e)
            return None


        if res_result.get('status') == 1:
            solution_token = res_result.get('request')
            print("CAPTCHA solution received:", solution_token)
            return solution_token
        elif res_result.get('request') == "CAPCHA_NOT_READY":
            print("Solution not ready yet, retrying...")
            continue
        else:
            print("Error retrieving solution:", res_result.get('request'))
            return None


def main():
    parser = argparse.ArgumentParser(
        description='Demonstration of form auto-fill and solving Turnstile CAPTCHA via solvecaptcha.'
    )
    parser.add_argument('api_key', type=str, nargs='?', help='Your solvecaptcha API key')
    parser.add_argument('url', type=str, nargs='?', help='URL of the page with the form and Turnstile CAPTCHA')
    args = parser.parse_args()


    if not args.api_key:
        args.api_key = input("Enter your solvecaptcha API key: ")
    if not args.url:
        args.url = input("Enter the URL of the page with the CAPTCHA: ")


    # 1) Launch Selenium in visual mode (without headless) so you can observe the process
    chrome_options = Options()
    driver = webdriver.Chrome(options=chrome_options)
    driver.get(args.url)


    wait = WebDriverWait(driver, 30)


    try:
        # 2) Wait for the element with the class .cf-turnstile to appear
        turnstile_div = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".cf-turnstile"))
        )
    except Exception as e:
        print("Error: element with class .cf-turnstile not found:", e)
        driver.quit()
        return


    # Extract the sitekey from the data-sitekey attribute
    site_key = turnstile_div.get_attribute("data-sitekey")
    print("Sitekey found:", site_key)


    # ----- Step 1: Automatically fill in the form fields -----
    try:
        # Example: select "A deceased individual"
        select_request_type = Select(driver.find_element(By.ID, "request_type"))
        select_request_type.select_by_value("deceased")  

        # First name, last name
        driver.find_element(By.ID, "first_name").send_keys("John")
        driver.find_element(By.ID, "last_name").send_keys("Doe")

        # Email, phone
        driver.find_element(By.ID, "email").send_keys("test@example.com")
        driver.find_element(By.ID, "phone").send_keys("1234567890")

        # Address
        driver.find_element(By.ID, "who_address").send_keys("123 Test Street")
        driver.find_element(By.ID, "who_address2").send_keys("Apt 4")
        driver.find_element(By.ID, "who_city").send_keys("Test City")

        select_state = Select(driver.find_element(By.ID, "who_state"))
        select_state.select_by_value("CA")  # California

        driver.find_element(By.ID, "who_zip").send_keys("90001")

        # Check the "Requests" checkboxes
        driver.find_element(By.ID, "request_type_1").click()  # Do not sell/share my personal information
        driver.find_element(By.ID, "request_type_2").click()  # Do not use my personal data for targeted advertising
        # ... you can select the others if necessary

        print("Form fields have been filled with test data.")
    except Exception as e:
        print("Error auto-filling form fields:", e)
        driver.quit()
        return


    # ----- Step 2: Solve the CAPTCHA via solvecaptcha -----
    token = get_turnstile_solution(args.api_key, site_key, args.url)
    if not token:
        print("Failed to obtain CAPTCHA solution.")
        driver.quit()
        return


    # ----- Step 3: Insert the solution into the hidden field and call the callback -----
    try:
        # Locate the hidden field that Turnstile uses to store the response
        input_field = wait.until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, 'input#cf-chl-widget-yi26c_response, input[name="cf-turnstile-response"]')
            )
        )
        # Insert the obtained token
        driver.execute_script("arguments[0].value = arguments[1];", input_field, token)
        print("CAPTCHA solution token inserted into the hidden field.")


        # Generate the 'change' event
        driver.execute_script("""
            var event = new Event('change', { bubbles: true });
            arguments[0].dispatchEvent(event);
        """, input_field)


        # If the site uses a callback function for Turnstile, attempt to call it
        driver.execute_script("""
            if (window.tsCallback) {
                window.tsCallback(arguments[0]);
            }
        """, token)
        print("Callback invoked (if it was defined).")


    except Exception as e:
        print("Error inserting CAPTCHA token:", e)
        driver.quit()
        return


    # ----- Step 4: Submit the form to proceed to the next stage -----
    try:
        submit_button = wait.until(
            EC.element_to_be_clickable((By.ID, "submit_button"))
        )
        submit_button.click()
        print("Click on the 'Submit' button executed.")


        # Wait for the URL to change after submission
        wait.until(EC.url_changes(args.url))
        print("Transition to the next stage detected. Current URL:", driver.current_url)


    except Exception as e:
        print("Failed to click 'Submit' or wait for the next stage:", e)


    print("Automation completed. The browser window remains open for verification.")
    input("Press Enter to close the browser...")
    driver.quit()


if __name__ == '__main__':
    main()

When the Going Gets Tough: The Advanced Turnstile CAPTCHA and the Node.js Conundrum

Then came the real test—the advanced Turnstile CAPTCHA. My “iron colleague” (read: my computer) faltered repeatedly in Python. Parameters were missing, data wasn’t intercepted—it was chaos! I had to dig deep into old-school methods and scour the internet once more.

Image description

I unearthed this GitHub repository:

Yes, it’s the same service making waves everywhere! The repository works like a charm—just tweak this line:

page.goto('https://2captcha.com/demo/cloudflare-turnstile-challenge')

Swap in your URL and API key, and it runs smoothly. The catch? I needed a Python solution, not a Node.js one.

The Situation

Image description

I loaded all the files into my trusty system and had it convert the Node.js solution to Python. Initially, we tried a Puppeteer-based approach (as they say – “It’s liquid stool and we’re not showing it”), but it flopped—not once, but five times.

Then we pivoted and modernized the script using Playwright. It wasn’t a slam dunk immediately, but eventually, the wheels started turning.

Here’s the final script:

import asyncio
import json
import re
import os
import time
import requests
from playwright.async_api import async_playwright




# Set the API key directly in the file
API_KEY = "Ваш ключ АПИ"




async def normalize_user_agent(playwright):
    """
    Retrieves the user agent using a temporary headless browser and normalizes it.
    """
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context()
    page = await context.new_page()
    user_agent = await page.evaluate("() => navigator.userAgent")
    normalized = re.sub(r'Headless', '', user_agent)
    normalized = re.sub(r'Chromium', 'Chrome', normalized)
    await browser.close()
    return normalized.strip()




def solve_turnstile(params, api_key):
    """
    Sends the CAPTCHA parameters to 2Captcha and polls for the result until solved.
    """
    base_url = 'http://2captcha.com'
    in_params = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': params.get('sitekey'),
        'pageurl': params.get('pageurl'),
        'data': params.get('data'),
        'action': params.get('action'),
        'userAgent': params.get('userAgent'),
        'json': 1
    }
    if 'pagedata' in params:
        in_params['pagedata'] = params.get('pagedata')

    print("Sending CAPTCHA for solving...")
    r = requests.get(f"{base_url}/in.php", params=in_params)
    res_json = r.json()
    if res_json.get('status') != 1:
        raise Exception("Error submitting CAPTCHA: " + res_json.get('request'))
    captcha_id = res_json.get('request')
    print(f"Task submitted, ID: {captcha_id}")

    # Initial delay before polling
    time.sleep(10)
    while True:
        r = requests.get(f"{base_url}/res.php", params={
            'key': api_key,
            'action': 'get',
            'id': captcha_id,
            'json': 1
        })
        res_json = r.json()
        if res_json.get('status') == 1:
            token = res_json.get('request')
            print(f"CAPTCHA solved, token: {token}")
            return token
        elif res_json.get('request') == 'CAPCHA_NOT_READY':
            print("Solution not ready, waiting 5 seconds...")
            time.sleep(5)
        else:
            raise Exception("Error solving CAPTCHA: " + res_json.get('request'))




async def handle_console(msg, page, captcha_future):
    """
    Console message handler. Upon receiving a string with the prefix "intercepted-params:",
    parses the JSON, sends the parameters to 2Captcha, and returns the obtained token.
    """
    text = msg.text
    if "intercepted-params:" in text:
        json_str = text.split("intercepted-params:", 1)[1]
        try:
            params = json.loads(json_str)
        except json.JSONDecodeError:
            print("Error parsing JSON from intercepted-params")
            return
        print("Intercepted parameters:", params)
        api_key = API_KEY
        if not api_key:
            print("APIKEY environment variable is not set.")
            await page.context.browser.close()
            return
        try:
            token = solve_turnstile(params, api_key)
            # Send the token back to the page via callback call
            await page.evaluate("""(token) => {
                window.cfCallback(token);
            }""", token)
            if not captcha_future.done():
                captcha_future.set_result(token)
        except Exception as e:
            print("Error solving CAPTCHA:", e)
            if not captcha_future.done():
                captcha_future.set_exception(e)




async def main():
    async with async_playwright() as p:
        # Retrieve normalized user agent
        user_agent = await normalize_user_agent(p)
        print("Normalized user agent:", user_agent)

        # Launch browser with visible window
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(user_agent=user_agent)

        # Read the contents of the inject.js file
        with open("inject.js", "r", encoding="utf-8") as f:
            inject_script = f.read()
        # Add the script for injection into every page
        await context.add_init_script(script=inject_script)

        page = await context.new_page()

        # Create a future to wait for the CAPTCHA solution result
        captcha_future = asyncio.Future()

        # Register console message handler
        page.on("console", lambda msg: asyncio.create_task(handle_console(msg, page, captcha_future)))

        # Navigate to the target page
        await page.goto("https://crash.chicagopolice.org/")

        try:
            token = await asyncio.wait_for(captcha_future, timeout=120)
            print("CAPTCHA token received:", token)
        except asyncio.TimeoutError:
            print("CAPTCHA wait time expired")
        await browser.close()




if __name__ == "__main__":
    asyncio.run(main())

This script works hand in hand with an injection script (yes, that’s its name and it does exactly what you’d expect):

console.log('inject.js loaded');
console.clear = () => console.log('Console was cleared');
const i = setInterval(() => {
    if (window.turnstile) {
        console.log('window.turnstile detected');
        clearInterval(i);
        const originalRender = window.turnstile.render;
        window.turnstile.render = (a, b) => {
            console.log('Overriding window.turnstile.render');
            let params = {
                sitekey: b.sitekey,
                pageurl: window.location.href,
                data: b.cData,
                pagedata: b.chlPageData,
                action: b.action,
                userAgent: navigator.userAgent,
                json: 1
            };
            console.log('intercepted-params:' + JSON.stringify(params));
            window.cfCallback = b.callback;
            // If you need to call the original function, uncomment the following line:
            // return originalRender ? originalRender(a, b) : null;
            return;
        };
    }
}, 50);

The injection is loaded on every page via the context.add_init_script method. Its mission? To intercept the call to window.turnstile.render, the function that normally displays and processes the CAPTCHA. Every 50 milliseconds, it checks for the presence of window.turnstile. Once it appears, it constructs a parameter object containing:

  • sitekey: passed via parameter b.

  • pageurl: the current page URL.

  • data: additional data from b.cData.

  • pagedata: page data from b.chlPageData.

  • action: the intended action.

  • userAgent: the browser’s user agent.

  • json: a flag for JSON requests.

These parameters are then sent to our main script, where the same magic occurs as with the simple CAPTCHA—the challenge is solved, the token is returned, and it’s seamlessly inserted into the page.

For a closer look, check out this video:

I also reworked the intermediary script for SolveCaptcha—here’s that version:

import asyncio
import json
import re
import os
import time
import requests
from playwright.async_api import async_playwright




# Set the API key directly in the file
API_KEY = "Your API key"




async def normalize_user_agent(playwright):
    """
    Retrieves the user agent using a temporary headless browser and normalizes it.
    """
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context()
    page = await context.new_page()
    user_agent = await page.evaluate("() => navigator.userAgent")
    normalized = re.sub(r'Headless', '', user_agent)
    normalized = re.sub(r'Chromium', 'Chrome', normalized)
    await browser.close()
    return normalized.strip()




def solve_turnstile(params, api_key):
    """
    Sends the CAPTCHA parameters to solvecaptcha and polls for the result until solved.
    """
    base_url = 'https://api.solvecaptcha.com'
    in_params = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': params.get('sitekey'),
        'pageurl': params.get('pageurl'),
        'data': params.get('data'),
        'action': params.get('action'),
        'userAgent': params.get('userAgent'),
        'json': 1
    }
    if 'pagedata' in params:
        in_params['pagedata'] = params.get('pagedata')

    print("Sending CAPTCHA for solving via solvecaptcha...")
    r = requests.get(f"{base_url}/in.php", params=in_params)
    res_json = r.json()
    if res_json.get('status') != 1:
        raise Exception("Error submitting CAPTCHA: " + res_json.get('request'))
    captcha_id = res_json.get('request')
    print(f"Task submitted, ID: {captcha_id}")

    # Initial delay before polling
    time.sleep(10)
    while True:
        r = requests.get(f"{base_url}/res.php", params={
            'key': api_key,
            'action': 'get',
            'id': captcha_id,
            'json': 1
        })
        res_json = r.json()
        if res_json.get('status') == 1:
            token = res_json.get('request')
            print(f"CAPTCHA solved, token: {token}")
            return token
        elif res_json.get('request') == 'CAPCHA_NOT_READY':
            print("Solution not ready, waiting 5 seconds...")
            time.sleep(5)
        else:
            raise Exception("Error solving CAPTCHA: " + res_json.get('request'))




async def handle_console(msg, page, captcha_future):
    """
    Console message handler. Upon receiving a string with the prefix "intercepted-params:",
    parses the JSON, sends the parameters to solvecaptcha, and returns the obtained token.
    """
    text = msg.text
    if "intercepted-params:" in text:
        json_str = text.split("intercepted-params:", 1)[1]
        try:
            params = json.loads(json_str)
        except json.JSONDecodeError:
            print("Error parsing JSON from intercepted-params")
            return
        print("Intercepted parameters:", params)
        api_key = API_KEY
        if not api_key:
            print("API key environment variable is not set.")
            await page.context.browser.close()
            return
        try:
            token = solve_turnstile(params, api_key)
            # Send the token back to the page via callback call
            await page.evaluate("""(token) => {
                window.cfCallback(token);
            }""", token)
            if not captcha_future.done():
                captcha_future.set_result(token)
        except Exception as e:
            print("Error solving CAPTCHA:", e)
            if not captcha_future.done():
                captcha_future.set_exception(e)




async def main():
    async with async_playwright() as p:
        # Retrieve normalized user agent
        user_agent = await normalize_user_agent(p)
        print("Normalized user agent:", user_agent)

        # Launch browser with visible window
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(user_agent=user_agent)

        # Read the contents of the inject.js file
        with open("inject.js", "r", encoding="utf-8") as f:
            inject_script = f.read()
        # Add the injection script to every page
        await context.add_init_script(script=inject_script)

        page = await context.new_page()

        # Create a future to wait for the CAPTCHA solution result
        captcha_future = asyncio.Future()

        # Register console message handler
        page.on("console", lambda msg: asyncio.create_task(handle_console(msg, page, captcha_future)))

        # Navigate to the target page
        await page.goto("https://crash.chicagopolice.org/")

        try:
            token = await asyncio.wait_for(captcha_future, timeout=120)
            print("CAPTCHA token received:", token)
        except asyncio.TimeoutError:
            print("CAPTCHA wait time expired")
        await browser.close()




if __name__ == "__main__":
    asyncio.run(main())

It functions identically—just be sure to update the API key accordingly.

Final Word: Embracing a New Era of Smart Problem-Solving

In our fast-paced digital era, even mundane tasks can morph into epic challenges. With the right strategy and a dash of creative ingenuity, we can finally liberate ourselves from bombarding overstretched developers on tech forums with trivial questions. Sure, forum admins might miss the constant influx of basic queries that drive engagement, but sometimes, disrupting the norm is exactly what’s needed to spark true innovation.