How to Solve AWS WAF CAPTCHA in Python (405 Pages)

aws waf captcha in python - How to Solve AWS WAF CAPTCHA in Python (405 Pages)

To solve an AWS WAF CAPTCHA in Python, you read three values from the page that answers with HTTP 405, send them to the CapSkip AWS WAF solver with amazon_waf, and trade the answer for an aws-waf-token cookie. The answer is not a token you paste into a form. It is a voucher pair that AWS exchanges for the cookie, so this flow has one step more than reCAPTCHA. This guide runs the whole loop with the CapSkip Python SDK and requests in about 70 lines. CapSkip runs on your own Windows PC with no per-solve fee, so a retry costs nothing.

What you need

  • CapSkip 3.0.0 or later running on a Windows PC. That release added AWS WAF solving. The setup guide covers the first start.
  • Python 3.10 or later and the capskip package at version 1.4.0 or later. Version 1.3.0 has no amazon_waf method. The code below ran on Python 3.11.7 and 3.14.0 with capskip 1.4.0 and requests 2.34.2. Python 3.10 was not tried here.
  • A PowerShell prompt. The commands use PowerShell syntax.
  • A page that you own or may automate, behind a rule with the AWS WAF CAPTCHA action.
  • An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a script on another PC, a VPS or a hosted platform can reach it over the API. Both are set under connection settings.

How this was tested: the code ran against three mock servers that I wrote, not against a real AWS WAF site. One copies the AWS WAF checks listed in the CapSkip API docs, one plays the protected site and one plays AWS’s token host. The SDK was the real capskip 1.4.0. I also ran the script once against the CapSkip app on my PC, with the mock site as the target. The app cannot solve a fake page, so it answered ERROR_CAPTCHA_UNSOLVABLE three times and the script ended with its RuntimeError, which shows that error reaching the loop as an ApiException. No live AWS WAF site was involved, so no number below is a solve time, and I cannot tell you that a given live site accepts the cookie this flow produces.

Step 1: read the challenge page

When a rule with the CAPTCHA action matches a request that has no valid token, AWS WAF answers with HTTP 405 and the header x-amzn-waf-action set to captcha. According to the AWS WAF documentation, the puzzle page is included only when the request sends an Accept header that asks for text/html. A plain requests call sends a generic Accept, so the script sets one. Without it you can get a 405 with nothing in it to read.

That page defines a small object called gokuProps with three values: key, iv and context. It also loads challenge.js and captcha.js from an awswaf.com host. The iv and context values change with every challenge page, so read them fresh each time. A 202 response with the header set to challenge is a different thing: that is AWS WAF’s silent Challenge action, which has no puzzle. The CapSkip docs describe the 405 page and the widget only, so this guide does not cover it.

# pip install capskip requests
import re
import requests

PAGE_URL = "https://www.example.com/"  # the page that answers HTTP 405
HEADERS = {  # AWS WAF sends its puzzle page only when the request accepts HTML
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0 Safari/537.36",
    "Accept": "text/html",
}


def read_challenge(session):
    resp = session.get(PAGE_URL, timeout=30)
    if resp.status_code != 405 or "gokuProps" not in resp.text:
        return None  # no AWS WAF challenge on this response
    block = re.search(r"gokuProps\s*=\s*\{(.*?)\}", resp.text, re.S).group(1)
    field = lambda name: re.search(rf'"{name}"\s*:\s*"([^"]+)"', block).group(1)
    script = lambda name: re.search(rf'<script[^>]+src="([^"]+/{name}\.js)"', resp.text).group(1)
    return {
        "sitekey": field("key"),
        "iv": field("iv"),
        "context": field("context"),
        "challenge_script": script("challenge"),
        "captcha_script": script("captcha"),
    }

The function returns None when the response is not a CAPTCHA page, so a script can tell “nothing to solve” from “solved”. It reads the values with two small regular expressions instead of a JSON parser, because the page holds JavaScript, not JSON. If your target quotes the keys differently, adjust the expression and nothing else.

Step 2: solve it with amazon_waf

The SDK method is amazon_waf. Only sitekey and url are required. For a challenge page, sitekey is gokuProps.key, and iv and context are the other two values. They travel together: send both or neither. If you leave both out, CapSkip reads them from the page at solve time, which the docs call the safer choice when your copy of the page is more than a few minutes old. This guide sends them because it fetched the page a moment ago.

The two script addresses are options. Each must be an https address on an awswaf.com host, and the SDK refuses anything else before a request is sent. The answer comes back as a dictionary with captcha_voucher and existing_token, plus the raw answer under code and the task number under captchaId. The method waits with the longer 300 second timeout that reCAPTCHA uses, because CapSkip solves the puzzle in a real browser. I did not time it.

from capskip import CapSkip

# Local mode. In Server mode, use your server's address instead.
solver = CapSkip(host="127.0.0.1", port=8080)


def solve(challenge):
    # The answer is a voucher pair, not a token.
    return solver.amazon_waf(
        sitekey=challenge["sitekey"],
        iv=challenge["iv"],
        context=challenge["context"],
        url=PAGE_URL,
        challenge_script=challenge["challenge_script"],
        captcha_script=challenge["captcha_script"],
    )

To route the solve through a proxy, add a proxy argument to the call: a dictionary with type set to HTTP and uri set to user:[email protected]:8080. In the mock run that arrived at in.php as a proxy and a proxytype. One address is fine for testing. Sites behind AWS WAF often pair the CAPTCHA with rate limits, so a steady stream of solves from one address draws harder treatment, and the CapSkip docs suggest a proxy list in Settings or a per-call proxy when the token must come from a particular network. CapSkip’s own solver sends its page loads, the puzzle and the voucher exchange through one proxy, so AWS sees one address. If you pass a proxy to the solve, send your own requests through the same one. I could not test that part. The proxy rotation guide covers rotating them.

Step 3: trade the voucher for the aws-waf-token cookie

The voucher pair is useless on its own. AWS trades it for the finished cookie on a voucher endpoint that sits beside challenge.js: the same folder, with voucher in place of challenge.js. POST the pair as JSON, and the reply has a token field. The CapSkip docs send the body as text/plain with an Origin header that matches the site. The CapSkip app’s own code also sends a Referer, so the function sends both. The SDK documentation says the pair is short-lived, so exchange it the moment it arrives and never store it.

import json
from urllib.parse import urlsplit


def exchange(result, challenge):
    # The voucher endpoint sits beside challenge.js, same folder.
    voucher_url = challenge["challenge_script"].rsplit("/", 1)[0] + "/voucher"
    site = urlsplit(PAGE_URL)
    pair = {
        "captcha_voucher": result["captcha_voucher"],
        "existing_token": result["existing_token"],
    }
    reply = requests.post(
        voucher_url,
        data=json.dumps(pair),
        headers={
            "Content-Type": "text/plain;charset=UTF-8",
            "Origin": f"{site.scheme}://{site.netloc}",
            "Referer": f"{site.scheme}://{site.netloc}/",
        },
        timeout=30,
    )
    reply.raise_for_status()
    return reply.json()["token"]  # the aws-waf-token cookie value

Step 4: the full working example

The last block puts the AWS WAF CAPTCHA in Python flow together and adds the retry. Save all four blocks in one file, one after another, and run it. Each attempt fetches the page again, so every try carries a fresh iv and context. A failed puzzle raises ApiException, and the loop tries again with a new puzzle. After the token arrives, the script requests the page with the cookie set.

from capskip import ApiException


def fetch_protected():
    session = requests.Session()
    session.headers.update(HEADERS)
    for attempt in range(1, 4):
        challenge = read_challenge(session)  # fresh page, fresh iv and context
        if challenge is None:
            return session.get(PAGE_URL, timeout=30)  # no challenge, real page
        try:
            result = solve(challenge)
        except ApiException as err:
            print(f"attempt {attempt}: {err}")  # each retry draws a new puzzle
            continue
        token = exchange(result, challenge)  # exchange at once, never store it
        return session.get(PAGE_URL, cookies={"aws-waf-token": token}, timeout=30)
    raise RuntimeError("AWS WAF CAPTCHA not solved in 3 attempts")


if __name__ == "__main__":
    page = fetch_protected()
    print(page.status_code, page.text[:60])

In the mock run the script behaved like this on both Python versions:

Mock settingWhat the script did
Every puzzle answeredOne task, one voucher exchange, then status 200 with the real page
First two puzzles refusedTwo attempt lines, then status 200 on the third task
First three puzzles refusedThree attempt lines, then RuntimeError
Request without an Accept headerThe mock sent a 405 with no page, read_challenge returned None and the script printed 405, so keep the header

The cookie works for as long as the site’s immunity time. AWS sets 300 seconds by default and lets the site owner choose from 60 seconds to three days, as the AWS WAF documentation explains. When the 405 comes back, run the loop again. If you make many requests, keep the token and reuse it until then, instead of solving once per request.

Common errors

ErrorCauseFix
ValidationExceptioniv without context, a script address that is not https on an awswaf.com host, or a url that is not a full addressSend iv and context together, and copy each script address exactly as the page has it
ApiException with ERROR_CAPTCHA_UNSOLVABLEThe puzzle could not be answeredSubmit again. The API docs note that every attempt draws a fresh puzzle
ERROR_TASK_NOT_SUPPORTEDThe API docs list this for a task type that is switched off in CapSkipCheck the AWS WAF settings in the CapSkip app
ERROR_PROXY_FORMATThe proxy string could not be parsedUse user:pass@ip:port
ERROR_NO_SLOT_AVAILABLEThe API key has a request rate limit and you went over itWait a few seconds and submit again
NetworkExceptionCapSkip is not running, or the host or port is wrongStart CapSkip and check the connection settings
The voucher POST is refusedMost likely a stale or mismatched pair. The CapSkip app’s code expects HTTP 400 from AWS for an invalid existing_token. I could not reproduce this one against real AWSExchange at once, and submit a new task
Still a 405 with the cookieThe token expired, or the site reads it from somewhere elseSolve again, and check how the page’s own requests carry the token

Running the solver on a server

The script needs two things from CapSkip: a host and a port. In Local mode that is 127.0.0.1 and your own PC. In Server mode CapSkip listens on your network address or a public IP, so a script on another PC, a VPS or a hosted platform can call it over the same API. Change the host and port in the CapSkip constructor and nothing else in the code changes. A static public IP is recommended for stable remote access. Server mode is still your own hardware and still unmetered. It does open the port to any address, so add a Windows Firewall block rule for the addresses that should not reach it. The connection settings show how to switch modes.

FAQ

Can I solve an AWS WAF CAPTCHA in Python without a browser?

Your script needs no browser. It sends plain HTTP requests to CapSkip and to the site, and CapSkip solves the puzzle in a real browser on the machine where it runs, as the SDK documentation describes. The 405 status is simply how AWS WAF marks its CAPTCHA action.

Does the SDK return the aws-waf-token directly?

No. It returns captcha_voucher and existing_token, and you trade them at the voucher endpoint for the token. The JSON createTask API accepts AWS WAF too, and several of its formats return the token directly. The API docs list them.

What if the CAPTCHA is a widget inside a form?

That is the second shape AWS WAF uses. The values come from the widget’s apiKey and its jsapi.js address instead of gokuProps. The Node.js widget guide walks through it, and the call is the same method.

Can a hosted platform run this against my CapSkip?

Yes, through Server mode. Set CapSkip to listen on your network or public IP, point the CapSkip constructor at that host and port, and the loop works the same as on one PC. The solver stays on your hardware.

The short version

An AWS WAF CAPTCHA in Python takes four moves: read the 405 page with an HTML Accept header, call amazon_waf, trade the voucher pair for the cookie, and repeat the request with it. Retrying is free on your own machine, which is the point of an unlimited captcha solver for a protected site that sometimes hands you a puzzle that cannot be answered. For the rest of the SDK, see the Python CAPTCHA solver page.