{"id":27128,"date":"2026-11-04T20:09:20","date_gmt":"2026-11-04T20:09:20","guid":{"rendered":"https:\/\/capskip.com\/?p=27128"},"modified":"2026-11-04T20:09:20","modified_gmt":"2026-11-04T20:09:20","slug":"aws-waf-captcha-python","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/aws-waf-captcha-python\/","title":{"rendered":"\u5982\u4f55\u7528 Python \u8bc6\u522b AWS WAF \u9a8c\u8bc1\u7801\uff08405 \u9875\u9762\uff09"},"content":{"rendered":"<p>To solve an AWS WAF CAPTCHA in Python, you read three values from the page that answers with HTTP 405, send them to the <a href=\"https:\/\/capskip.com\/aws-waf-captcha-solver\/\">CapSkip AWS WAF solver<\/a> 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.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip 3.0.0 or later running on a Windows PC. That release added AWS WAF solving. The <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a> covers the first start.<\/li>\n<li>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.<\/li>\n<li>A PowerShell prompt. The commands use PowerShell syntax.<\/li>\n<li>A page that you own or may automate, behind a rule with the AWS WAF CAPTCHA action.<\/li>\n<li>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 <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<p>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 <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a>, one plays the protected site and one plays AWS&#8217;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.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: read the challenge page<\/h2>\n<p>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 <a href=\"https:\/\/docs.aws.amazon.com\/waf\/latest\/developerguide\/waf-captcha-and-challenge-actions.html\" rel=\"noopener nofollow\" target=\"_blank\">AWS WAF documentation<\/a>, 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.<\/p>\n<p>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&#8217;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.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip requests\nimport re\nimport requests\n\nPAGE_URL = &quot;https:\/\/www.example.com\/&quot;  # the page that answers HTTP 405\nHEADERS = {  # AWS WAF sends its puzzle page only when the request accepts HTML\n    &quot;User-Agent&quot;: &quot;Mozilla\/5.0 (Windows NT 10.0; Win64; x64) Chrome\/130.0 Safari\/537.36&quot;,\n    &quot;Accept&quot;: &quot;text\/html&quot;,\n}\n\n\ndef read_challenge(session):\n    resp = session.get(PAGE_URL, timeout=30)\n    if resp.status_code != 405 or &quot;gokuProps&quot; not in resp.text:\n        return None  # no AWS WAF challenge on this response\n    block = re.search(r&quot;gokuProps\\s*=\\s*\\{(.*?)\\}&quot;, resp.text, re.S).group(1)\n    field = lambda name: re.search(rf'&quot;{name}&quot;\\s*:\\s*&quot;([^&quot;]+)&quot;', block).group(1)\n    script = lambda name: re.search(rf'&lt;script[^&gt;]+src=&quot;([^&quot;]+\/{name}\\.js)&quot;', resp.text).group(1)\n    return {\n        &quot;sitekey&quot;: field(&quot;key&quot;),\n        &quot;iv&quot;: field(&quot;iv&quot;),\n        &quot;context&quot;: field(&quot;context&quot;),\n        &quot;challenge_script&quot;: script(&quot;challenge&quot;),\n        &quot;captcha_script&quot;: script(&quot;captcha&quot;),\n    }<\/pre>\n<\/div>\n<p>The function returns None when the response is not a CAPTCHA page, so a script can tell &#8220;nothing to solve&#8221; from &#8220;solved&#8221;. 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.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: solve it with amazon_waf<\/h2>\n<p>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.<\/p>\n<p>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.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">from capskip import CapSkip\n\n# Local mode. In Server mode, use your server's address instead.\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n\ndef solve(challenge):\n    # The answer is a voucher pair, not a token.\n    return solver.amazon_waf(\n        sitekey=challenge[&quot;sitekey&quot;],\n        iv=challenge[&quot;iv&quot;],\n        context=challenge[&quot;context&quot;],\n        url=PAGE_URL,\n        challenge_script=challenge[&quot;challenge_script&quot;],\n        captcha_script=challenge[&quot;captcha_script&quot;],\n    )<\/pre>\n<\/div>\n<p>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:pass@1.2.3.4: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&#8217;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 <a href=\"https:\/\/capskip.com\/captcha-proxy-rotation\/\">proxy rotation guide<\/a> covers rotating them.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: trade the voucher for the aws-waf-token cookie<\/h2>\n<p>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&#8217;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.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import json\nfrom urllib.parse import urlsplit\n\n\ndef exchange(result, challenge):\n    # The voucher endpoint sits beside challenge.js, same folder.\n    voucher_url = challenge[&quot;challenge_script&quot;].rsplit(&quot;\/&quot;, 1)[0] + &quot;\/voucher&quot;\n    site = urlsplit(PAGE_URL)\n    pair = {\n        &quot;captcha_voucher&quot;: result[&quot;captcha_voucher&quot;],\n        &quot;existing_token&quot;: result[&quot;existing_token&quot;],\n    }\n    reply = requests.post(\n        voucher_url,\n        data=json.dumps(pair),\n        headers={\n            &quot;Content-Type&quot;: &quot;text\/plain;charset=UTF-8&quot;,\n            &quot;Origin&quot;: f&quot;{site.scheme}:\/\/{site.netloc}&quot;,\n            &quot;Referer&quot;: f&quot;{site.scheme}:\/\/{site.netloc}\/&quot;,\n        },\n        timeout=30,\n    )\n    reply.raise_for_status()\n    return reply.json()[&quot;token&quot;]  # the aws-waf-token cookie value<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: the full working example<\/h2>\n<p>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.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">from capskip import ApiException\n\n\ndef fetch_protected():\n    session = requests.Session()\n    session.headers.update(HEADERS)\n    for attempt in range(1, 4):\n        challenge = read_challenge(session)  # fresh page, fresh iv and context\n        if challenge is None:\n            return session.get(PAGE_URL, timeout=30)  # no challenge, real page\n        try:\n            result = solve(challenge)\n        except ApiException as err:\n            print(f&quot;attempt {attempt}: {err}&quot;)  # each retry draws a new puzzle\n            continue\n        token = exchange(result, challenge)  # exchange at once, never store it\n        return session.get(PAGE_URL, cookies={&quot;aws-waf-token&quot;: token}, timeout=30)\n    raise RuntimeError(&quot;AWS WAF CAPTCHA not solved in 3 attempts&quot;)\n\n\nif __name__ == &quot;__main__&quot;:\n    page = fetch_protected()\n    print(page.status_code, page.text[:60])<\/pre>\n<\/div>\n<p>In the mock run the script behaved like this on both Python versions:<\/p>\n<table>\n<thead>\n<tr>\n<th>Mock setting<\/th>\n<th>What the script did<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Every puzzle answered<\/td>\n<td>One task, one voucher exchange, then status 200 with the real page<\/td>\n<\/tr>\n<tr>\n<td>First two puzzles refused<\/td>\n<td>Two attempt lines, then status 200 on the third task<\/td>\n<\/tr>\n<tr>\n<td>First three puzzles refused<\/td>\n<td>Three attempt lines, then RuntimeError<\/td>\n<\/tr>\n<tr>\n<td>Request without an Accept header<\/td>\n<td>The mock sent a 405 with no page, read_challenge returned None and the script printed 405, so keep the header<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The cookie works for as long as the site&#8217;s immunity time. AWS sets 300 seconds by default and lets the site owner choose from 60 seconds to three days, as the <a href=\"https:\/\/docs.aws.amazon.com\/waf\/latest\/developerguide\/waf-tokens-immunity-times.html\" rel=\"noopener nofollow\" target=\"_blank\">AWS WAF documentation<\/a> 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.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors<\/h2>\n<table>\n<thead>\n<tr>\n<th>Error<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ValidationException<\/td>\n<td>iv without context, a script address that is not https on an awswaf.com host, or a url that is not a full address<\/td>\n<td>Send iv and context together, and copy each script address exactly as the page has it<\/td>\n<\/tr>\n<tr>\n<td>ApiException with ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>The puzzle could not be answered<\/td>\n<td>Submit again. The <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a> note that every attempt draws a fresh puzzle<\/td>\n<\/tr>\n<tr>\n<td>ERROR_TASK_NOT_SUPPORTED<\/td>\n<td>The API docs list this for a task type that is switched off in CapSkip<\/td>\n<td>Check the AWS WAF settings in the CapSkip app<\/td>\n<\/tr>\n<tr>\n<td>ERROR_PROXY_FORMAT<\/td>\n<td>The proxy string could not be parsed<\/td>\n<td>Use user:pass@ip:port<\/td>\n<\/tr>\n<tr>\n<td>ERROR_NO_SLOT_AVAILABLE<\/td>\n<td>The API key has a request rate limit and you went over it<\/td>\n<td>Wait a few seconds and submit again<\/td>\n<\/tr>\n<tr>\n<td>NetworkException<\/td>\n<td>CapSkip is not running, or the host or port is wrong<\/td>\n<td>Start CapSkip and check the connection settings<\/td>\n<\/tr>\n<tr>\n<td>The voucher POST is refused<\/td>\n<td>Most likely a stale or mismatched pair. The CapSkip app&#8217;s code expects HTTP 400 from AWS for an invalid existing_token. I could not reproduce this one against real AWS<\/td>\n<td>Exchange at once, and submit a new task<\/td>\n<\/tr>\n<tr>\n<td>Still a 405 with the cookie<\/td>\n<td>The token expired, or the site reads it from somewhere else<\/td>\n<td>Solve again, and check how the page&#8217;s own requests carry the token<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on a server<\/h2>\n<p>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 <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a> show how to switch modes.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I solve an AWS WAF CAPTCHA in Python without a browser?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">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.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does the SDK return the aws-waf-token directly?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">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 <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a> list them.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">What if the CAPTCHA is a widget inside a form?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">That is the second shape AWS WAF uses. The values come from the widget&#8217;s apiKey and its jsapi.js address instead of gokuProps. The <a href=\"https:\/\/capskip.com\/aws-waf-widget-nodejs\/\">Node.js widget guide<\/a> walks through it, and the call is the same method.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can a hosted platform run this against my CapSkip?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">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.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>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 <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> for a protected site that sometimes hands you a puzzle that cannot be answered. For the rest of the SDK, see the <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">Python CAPTCHA solver page<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>AWS WAF \u4f1a\u7528 HTTP 405 \u548c\u4e00\u9053\u62fc\u56fe\u6765\u5e94\u7b54\u8bf7\u6c42\uff0c\u800c\u7b54\u6848\u662f\u4e00\u5bf9 voucher\uff0c\u4e0d\u662f token\u3002\u4e0b\u9762\u662f\u4e00\u4e2a Python \u5faa\u73af\uff1a\u8bfb\u53d6\u9875\u9762\uff0c\u8c03\u7528 CapSkip \u7684 amazon_waf\uff0c\u7528 voucher \u6362\u53d6 aws-waf-token cookie\uff0c\u7136\u540e\u91cd\u8bd5\u3002<\/p>","protected":false},"author":1,"featured_media":27127,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"AWS WAF CAPTCHA in Python: Solve the 405 Page | CapSkip","rank_math_description":"Solve an AWS WAF CAPTCHA in Python: read gokuProps from the 405 page, call amazon_waf, trade the voucher for the cookie. Tested on a mock. Copy the code.","rank_math_focus_keyword":"aws waf captcha in python","footnotes":""},"categories":[70],"tags":[],"class_list":["post-27128","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27128","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=27128"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27128\/revisions"}],"predecessor-version":[{"id":27133,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27128\/revisions\/27133"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/27127"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=27128"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=27128"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=27128"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}