如何用 Python 识别 AWS WAF 验证码(405 页面)

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

要在 Python 中识别 AWS WAF 验证码,你需要从返回 HTTP 405 的页面读取三个值,并用 amazon_waf 方法把它们发送给 CapSkip AWS WAF 识别工具 来识别,再把得到的答案换成 aws-waf-token cookie。这个答案不是可以粘贴到表单里的 token,而是一对 voucher,由 AWS 把它兑换成 cookie,所以这个流程比 reCAPTCHA 多一步。本指南用 CapSkip Python SDK 和 requests,用大约 70 行代码跑完整个流程。CapSkip 运行在你自己的 Windows PC 上,没有按次识别的费用,所以重试不花一分钱。

你需要什么

  • 在 Windows PC 上运行的 CapSkip 3.0.0 或更高版本。该版本加入了 AWS WAF 识别功能。 设置指南 详细介绍了首次启动的步骤。
  • Python 3.10 或更高版本,以及版本为 1.4.0 或更高的 capskip 包。1.3.0 版本没有 amazon_waf 方法。下面的代码在 Python 3.11.7 和 3.14.0 上运行过,使用 capskip 1.4.0 和 requests 2.34.2。Python 3.10 在这里没有试过。
  • 一个 PowerShell 提示符。这些命令使用 PowerShell 语法。
  • 一个你拥有或被允许自动化操作的页面,受带有 AWS WAF CAPTCHA 动作的规则保护。
  • 识别工具的地址。在 Local 模式下,CapSkip 只在 127.0.0.1 上响应,仅供本机使用。在 Server 模式下,它监听你的网络地址或公网 IP,这样另一台 PC、VPS 或托管平台上的脚本就能通过 API 访问它。两种模式的设置位置都是 连接设置.

测试方式:代码是针对我自己写的三个模拟服务器运行的,而不是针对真实的 AWS WAF 站点。其中一个照搬了 CapSkip API 文档中列出的 AWS WAF 检查,一个扮演受保护的站点,一个扮演 AWS 的 token 主机。SDK 是真实的 capskip 1.4.0。我还在我的 PC 上针对 CapSkip 应用运行了一次脚本,目标是那个模拟站点。应用无法识别一个伪造的页面,所以它三次都返回了 ERROR_CAPTCHA_UNSOLVABLE,脚本以它的 RuntimeError 结束,这说明该错误是以 ApiException 的形式传到循环里的。这里没有涉及任何线上的 AWS WAF 站点,所以下面的任何数字都不是识别耗时,我也不能向你保证某个线上站点会接受这个流程产生的 cookie。

第 1 步:读取挑战页面

当带有 CAPTCHA 动作的规则匹配到没有有效 token 的请求时,AWS WAF 会返回 HTTP 405,并把响应头 x-amzn-waf-action 设为 captcha。根据 AWS WAF 文档,只有当请求发送了要求 text/html 的 Accept 请求头时,才会包含拼图页面。普通的 requests 调用发送的是通用的 Accept,所以脚本自己设置了一个。没有它,你可能得到一个 405,里面却没有任何可读的内容。

该页面定义了一个名为 gokuProps 的小对象,包含三个值:key、iv 和 context。它还会从 awswaf.com 主机加载 challenge.js 和 captcha.js。iv 和 context 的值每个挑战页面都不同,所以每次都要重新读取。返回 202、且该响应头设为 challenge 的情况是另一回事:那是 AWS WAF 的静默 Challenge 动作,没有拼图。CapSkip 文档只介绍 405 页面和小组件,所以本指南不涉及它。

# 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"),
    }

当响应不是 CAPTCHA 页面时,这个函数返回 None,这样脚本就能区分“无需识别”和“已识别”。它用两个简单的正则表达式读取这些值,而不用 JSON 解析器,因为页面里是 JavaScript,不是 JSON。如果你的目标站点引用键名的方式不同,只需调整表达式,其他都不用改。

第 2 步:用 amazon_waf 识别

SDK 的方法是 amazon_waf。只有 sitekey 和 url 是必需的。对于挑战页面,sitekey 就是 gokuProps.key,而 iv 和 context 是另外两个值。它们要一起发送:要么两个都发,要么都不发。如果两个都省略,CapSkip 会在识别时从页面读取它们,文档称,当你手里的页面副本已有几分钟以上时,这是更稳妥的选择。本指南会发送它们,因为页面是刚刚抓取的。

两个脚本地址是选项。每个都必须是 awswaf.com 主机上的 https 地址,SDK 会在发出请求之前拒绝其他任何地址。答案以字典形式返回,包含 captcha_voucher 和 existing_token,另外原始答案在 code 下,任务编号在 captchaId 下。该方法使用与 reCAPTCHA 相同的、更长的 300 秒超时来等待,因为 CapSkip 是在真实浏览器里识别拼图的。我没有给它计时。

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"],
    )

要让识别经过代理,在调用中添加一个 proxy 参数:一个字典,其中 type 设为 HTTP,uri 设为 user:[email protected]:8080。在模拟运行中,它到达 in.php 时是一个 proxy 和一个 proxytype。测试时一个地址就够了。AWS WAF 保护的站点常常把 CAPTCHA 和速率限制搭配使用,所以从同一个地址持续发出大量识别请求会受到更严格的对待,CapSkip 文档建议在 Settings 里使用代理列表,或者当 token 必须来自特定网络时,对每次调用单独设置代理。CapSkip 自己的识别程序会把页面加载、拼图和 voucher 兑换都通过同一个代理发出,所以 AWS 看到的是同一个地址。如果你给识别传了代理,你自己的请求也要通过同一个代理发送。这一部分我没能测试。 代理轮换指南 介绍了如何轮换代理。

第 3 步:用 voucher 换取 aws-waf-token cookie

这一对 voucher 单独没有用。AWS 会在 challenge.js 旁边的 voucher 端点上把它兑换成最终的 cookie:同一个文件夹,用 voucher 取代 challenge.js。把这一对以 JSON 形式 POST 过去,响应里有一个 token 字段。CapSkip 文档是以 text/plain 发送请求体,并带有与站点匹配的 Origin 请求头。CapSkip 应用自己的代码还会发送 Referer,所以这个函数两者都发。SDK 文档说这一对 voucher 的有效期很短,所以一拿到就立刻兑换,永远不要存起来。

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

第 4 步:完整的可运行示例

最后一个代码块把 Python 中的 AWS WAF CAPTCHA 流程整合起来,并加上重试。把四个代码块按顺序保存到同一个文件里并运行。每次尝试都会重新抓取页面,所以每次都带有新的 iv 和 context。拼图识别失败会抛出 ApiException,循环会用新的拼图再试一次。token 到手之后,脚本带着这个 cookie 请求页面。

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])

在模拟运行中,脚本在两个 Python 版本上的表现如下:

模拟设置脚本的表现
每个拼图都得到解答一个任务,一次 voucher 兑换,然后返回状态 200 和真实页面
前两个拼图被拒绝两行 attempt 输出,然后第三个任务返回状态 200
前三个拼图被拒绝三行 attempt 输出,然后抛出 RuntimeError
请求没有 Accept 请求头模拟服务器返回了没有页面内容的 405,read_challenge 返回了 None,脚本打印了 405,所以请保留这个请求头

cookie 在站点的免疫时间(immunity time)内有效。AWS 默认设为 300 秒,网站所有者可以在 60 秒到三天之间选择,如 AWS WAF 文档 所述。405 再次出现时,重新运行循环。如果你发出很多请求,就保留 token,一直复用到那时,而不是每个请求都识别一次。

常见错误

错误原因修复
ValidationExceptioniv 没有 context、脚本地址不是 awswaf.com 主机上的 https 地址,或者 url 不是完整地址把 iv 和 context 一起发送,并按页面上的原样复制每个脚本地址
带 ERROR_CAPTCHA_UNSOLVABLE 的 ApiException拼图无法解答再提交一次。 API 文档 指出,每次尝试都会得到一个新的拼图
ERROR_TASK_NOT_SUPPORTEDAPI 文档用它表示 CapSkip 中已关闭的任务类型检查 CapSkip 应用中的 AWS WAF 设置
ERROR_PROXY_FORMAT无法解析代理字符串使用 user:pass@ip:port
ERROR_NO_SLOT_AVAILABLEAPI 密钥有请求频率限制,而你超出了限制等几秒钟再提交
NetworkExceptionCapSkip 没有在运行,或者主机或端口不对启动 CapSkip 并检查连接设置
voucher 的 POST 被拒绝很可能是一对过期或不匹配的 voucher。CapSkip 应用的代码预期,对无效的 existing_token,AWS 会返回 HTTP 400。我没能对真实的 AWS 复现这一情况立刻兑换,并提交一个新任务
带着 cookie 仍然返回 405token 已过期,或者站点从别处读取它重新识别,并检查页面自己的请求是如何携带 token 的

把识别工具跑在服务器上

脚本需要从 CapSkip 获得两样东西:一个主机和一个端口。在 Local 模式下,就是 127.0.0.1 和你自己的 PC。在 Server 模式下,CapSkip 监听你的网络地址或公网 IP,因此另一台 PC、VPS 或托管平台上的脚本可以通过同一个 API 调用它。在 CapSkip 构造函数里修改主机和端口,代码的其他部分都不用改。建议使用静态公网 IP,以便稳定地远程访问。Server 模式仍然运行在你自己的硬件上,仍然不按次计费。它确实会对任何地址开放该端口,所以请添加一条 Windows Firewall 阻止规则,拦住不该访问它的地址。 连接设置 说明了如何切换模式。

常见问题

我能在不用浏览器的情况下,用 Python 识别 AWS WAF 验证码吗?

你的脚本不需要浏览器。它只向 CapSkip 和站点发送普通的 HTTP 请求,而 CapSkip 会在它所运行的那台机器上,用真实浏览器识别拼图,SDK 文档就是这样描述的。405 状态码只是 AWS WAF 标记其 CAPTCHA 动作的方式。

SDK 会直接返回 aws-waf-token 吗?

不会。它返回 captcha_voucher 和 existing_token,你要在 voucher 端点把它们兑换成 token。JSON createTask API 也支持 AWS WAF,其中好几种格式会直接返回 token。 API 文档 列出了这些格式。

如果 CAPTCHA 是表单里的小组件怎么办?

这是 AWS WAF 使用的第二种形态。这时的值来自小组件的 apiKey 及其 jsapi.js 地址,而不是 gokuProps。 Node.js 小组件指南 会带你完整走一遍,调用的是同一个方法。

托管平台能针对我的 CapSkip 运行这个流程吗?

可以,通过 Server 模式。把 CapSkip 设置为监听你的网络 IP 或公网 IP,把 CapSkip 构造函数指向该主机和端口,循环的工作方式就和在一台 PC 上一样。识别工具仍然留在你自己的硬件上。

简短版结论

在 Python 中识别 AWS WAF CAPTCHA 分四步:用 HTML 的 Accept 请求头读取 405 页面,调用 amazon_waf,把这一对 voucher 兑换成 cookie,再带着它重复请求。受保护的站点有时会给你一个无法解答的拼图,而在你自己的机器上重试是免费的,这正是 无限量验证码识别工具 的意义所在。关于 SDK 的其余部分,请参阅 Python 验证码识别页面.