如何用 Node.js 识别 AWS WAF 小组件验证码

aws waf captcha in node.js - How to Solve an AWS WAF Widget CAPTCHA With Node.js

要在小组件页面上用 Node.js 识别 AWS WAF 验证码,你需要从页面读取小组件的 apiKey 和 jsapi.js 地址,并用 amazonWaf 方法把它们发送给 CapSkip AWS WAF 识别工具 来识别,把它返回的一对 voucher 兑换成 token,再把这个 token 随下一个请求发出去。小组件是 AWS WAF 使用的第二种形态。表单或登录页面会自己绘制拼图,所以没有 HTTP 405 页面,也没有需要读取的 gokuProps。本指南用 CapSkip Node.js SDK 和 Node.js 自带的 fetch 完成整个流程。405 页面有专门的 Python 指南.

你需要什么

  • 在 Windows PC 上运行的 CapSkip 3.0.0 或更高版本。该版本加入了 AWS WAF 识别功能。 设置指南 详细介绍了首次启动的步骤。
  • Node.js 18 或更高版本(这是该包所要求的),以及版本为 1.4.0 或更高的 capskip 包。更早的版本没有 amazonWaf 方法。下面的代码在 Node.js 24.22.0 上用 capskip 1.4.0 运行过。这里没有试过其他 Node.js 版本。
  • 一个 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。这段代码没有针对真实的 CapSkip 应用运行,也没有涉及任何线上的 AWS WAF 站点,所以下面的任何数字都不是识别耗时,我也不能向你保证某个线上站点会接受这个流程产生的 token。

第 1 步:搭建项目并找到小组件的值

创建一个文件夹,初始化项目,并安装 capskip:

npm init -y
npm install capskip

小组件页面会加载一个名为 jsapi.js 的脚本,并用一个选项对象调用 AwsWafCaptcha.renderCaptcha。根据 AWS WAF 文档,该对象中的 apiKey 是一个加密密钥,列出了允许使用该小组件的客户端域名,因此它属于某一个站点。CapSkip 需要从页面获得两样东西:那个 apiKey(它称之为 sitekey)和 jsapi.js 的完整地址。小组件页面没有 gokuProps,所以你不发送 iv 和 context。只发送 jsapi.js 地址而不带它们,就是把这个请求标记为小组件识别的方式。

第一个代码块从页面的 HTML 中同时读取这两个值。有些站点把 apiKey 放在单独的 JavaScript 包里。遇到这种情况,函数返回 null,解决办法是在页面加载的脚本中搜索 apiKey。

'use strict';
const { CapSkip, ApiException } = require('capskip');

const PAGE_URL = 'https://www.example.com/contact'; // the page that draws the widget
// The folder that the page's challenge.js loads from (see the Network tab).
const TOKEN_BASE = 'https://3f2a9c7d41b0.edge.sdk.awswaf.com/3f2a9c7d41b0/8e51d0c7a2f4/';

async function readWidget() {
  const res = await fetch(PAGE_URL, { headers: { Accept: 'text/html' } });
  const html = await res.text();
  const src = /<script[^>]+src="([^"]*\/jsapi\.js)"/i.exec(html);
  const key = /apiKey\s*:\s*["']([^"']+)["']/.exec(html);
  if (!src || !key) return null; // not in the HTML: look in the page's scripts
  return { sitekey: key[1], jsapiScript: new URL(src[1], PAGE_URL).href };
}

页面上的脚本地址可能是相对地址,也可能以两个斜杠开头,所以函数会用页面 URL 把它变成完整地址。代码块顶部附近的 TOKEN_BASE 是一个需要你手动填写的常量,原因在下一步说明。

第 2 步:找出 token 主机(只需一次)

这一对 voucher 要在 AWS 自己的主机上兑换成 token。在小组件页面上,这个主机不在你抓取的 HTML 里。在浏览器中打开页面,打开 Network tab,重新加载,找到 jsapi.js 替你发出的、指向 challenge.js 的请求。要复制的是它所在的文件夹,以斜杠结尾。CapSkip 文档说要把 challenge.js 替换成 voucher,下一步的代码用 TOKEN_BASE 做的就是这件事。

每个站点只需查找一次,所以把它放进你的配置里。我不能说在真实站点上它会保持多久不变。如果下一步里的兑换开始失败,先重新检查 Network tab,再怀疑其他原因。

第 3 步:识别并兑换 voucher

SDK 的方法是 amazonWaf。只有 sitekey 和页面 URL 是必需的,jsapi.js 地址放在选项里。脚本地址必须是 awswaf.com 主机上的 https 地址,SDK 会在发出请求之前拒绝其他任何地址。答案是一个对象,包含 captcha_voucher 和 existing_token,另外原始答案在 code 下,任务编号在 captchaId 下。该方法使用与 reCAPTCHA 相同的、更长的 300 秒超时来等待,因为 CapSkip 是在真实浏览器里识别拼图的。我没有给它计时。

兑换就是向 voucher 端点发出一个 POST,请求体是 JSON 形式的这一对值。CapSkip 文档是以 text/plain 发送的,并带有与站点匹配的 Origin 请求头。CapSkip 应用自己的代码还会发送 Referer,所以这段代码两者都发。Node.js 的 fetch 允许你设置 Origin,而浏览器不允许。SDK 文档说这一对 voucher 的有效期很短,所以立刻兑换,永远不要存起来。

// Local mode. In Server mode, use your server's address instead.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

async function getToken(widget) {
  // The answer is a voucher pair, not a token.
  const result = await solver.amazonWaf(widget.sitekey, PAGE_URL, {
    jsapiScript: widget.jsapiScript,
  });
  const reply = await fetch(new URL('voucher', TOKEN_BASE), {
    method: 'POST',
    headers: {
      'Content-Type': 'text/plain;charset=UTF-8',
      Origin: new URL(PAGE_URL).origin,
      Referer: new URL(PAGE_URL).origin + '/',
    },
    body: JSON.stringify({
      captcha_voucher: result.captcha_voucher,
      existing_token: result.existing_token,
    }),
  });
  if (!reply.ok) throw new Error(`voucher exchange failed: HTTP ${reply.status}`);
  return (await reply.json()).token; // the aws-waf-token value
}

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

第 4 步:像页面那样发送 token

在小组件页面上,这个 token 与页面自己的 onSuccess 回调收到的是同一个。各个页面传递它的方式不同。许多页面把它作为 aws-waf-token cookie 发送,另一些则作为 x-aws-waf-token 请求头发送,所以打开 Network tab,手动提交一次表单,并复制页面的请求所携带的内容。下面的函数发送的是 cookie,并把请求头作为替代写法展示出来。表单字段只是占位符,请换成你自己的。

function submitForm(token) {
  return fetch(PAGE_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Cookie: `aws-waf-token=${token}`, // or 'x-aws-waf-token': token
    },
    body: new URLSearchParams({ email: '[email protected]', message: 'Hello' }),
  });
}

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

最后一个代码块把各个部分组合成一个完整的 Node.js 版 AWS WAF CAPTCHA 识别程序,带有重试和入口点。把四个代码块按顺序保存到同一个文件里,用 node 运行。拼图识别失败会抛出 ApiException,循环会再试一次,每次都会得到一个新的拼图。其他任何错误都会直接抛出,所以错误的主机或被拒绝的兑换不会被悄悄地重试。

async function main() {
  const widget = await readWidget();
  if (!widget) throw new Error('no jsapi.js script or apiKey in the page HTML');
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const token = await getToken(widget); // exchange at once, never store it
      const res = await submitForm(token);
      console.log(res.status, await res.text());
      return;
    } catch (err) {
      if (!(err instanceof ApiException)) throw err;
      console.log(`attempt ${attempt}: ${err.message}`); // each retry draws a new puzzle
    }
  }
  throw new Error('AWS WAF CAPTCHA not solved in 3 attempts');
}

main().catch((err) => {
  console.error(`${err.constructor.name}: ${err.message}`);
  process.exitCode = 1;
});

在模拟运行中,脚本的表现如下:

模拟设置脚本的表现
每个拼图都得到解答一个任务,一次 voucher 兑换,然后返回状态 200 和文本 Message sent
前两个拼图被拒绝两行 attempt 输出,然后第三个任务返回状态 200
前三个拼图被拒绝三行 attempt 输出,然后是错误行和退出码 1

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

常见错误

错误原因修复
no jsapi.js script or apiKey in the page HTML页面是用一个包来构建小组件的,所以 HTML 里没有它们在已加载的脚本中搜索 apiKey 和 jsapi.js,然后手动传入这些值
ValidationExceptioniv 没有 context,或者脚本地址不是 awswaf.com 主机上的完整 https 地址对小组件不要发送 iv 或 context,并按页面上的原样复制 jsapi.js 地址
带 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 exchange failed: HTTP 4xx很可能是一对过期或不匹配的 voucher,或者 TOKEN_BASE 有误。CapSkip 应用的代码预期,对无效的 existing_token,AWS 会返回 HTTP 400。我没能对真实的 AWS 复现这一情况立刻兑换,在 Network tab 里检查文件夹,并提交一个新任务
表单提交仍然被拒绝token 已过期,或者页面是通过请求头传递它的,而你发送的是 cookie复制页面自己的请求所携带的内容

把识别工具跑在服务器上

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

常见问题

如何区分小组件页面和 405 挑战页面?

挑战页面以 HTTP 405 响应请求,并定义 gokuProps。小组件页面是普通页面,加载 jsapi.js 并调用 renderCaptcha,通常在表单上。同一个 amazonWaf 方法能处理这两种页面,只是传入的值不同。

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

你的脚本不需要浏览器。它只向 CapSkip 和站点发送普通的 HTTP 请求,而 CapSkip 会在它所运行的那台机器上,用真实浏览器识别拼图,SDK 文档就是这样描述的。你只需要自己打开一次浏览器,用来读取页面上的值。

SDK 会直接返回 token 吗?

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

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

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

简短版结论

Node.js 中的 AWS WAF CAPTCHA 与 405 页面上的是同一个拼图,只是出现的位置不同,放在了小组件里:读取 apiKey 和 jsapi.js 地址,调用 amazonWaf,在 voucher 端点兑换这一对值,再像页面那样发送 token。CapSkip 是一款 验证码绕过 工具,运行在你自己的机器上,所以它无法解答的拼图只会让你重试一次,而不会产生费用。关于 SDK 的其余部分,请参阅 Node.js 验证码识别页面.