Как решить AWS WAF CAPTCHA в Python (страницы с кодом 405)

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

Чтобы решить AWS WAF CAPTCHA в Python, считайте три значения со страницы, которая отвечает HTTP 405, и отправьте их в решатель CapSkip для AWS WAF с методом amazon_waf, а ответ обменяйте на cookie aws-waf-token. Ответ не является токеном, который вставляют в форму. Это пара voucher, которую AWS обменивает на cookie, поэтому в этой схеме на один шаг больше, чем в reCAPTCHA. В этом руководстве весь цикл выполняется с помощью CapSkip Python SDK и requests примерно в 70 строк. CapSkip работает на вашем собственном ПК с Windows без платы за каждое решение, поэтому повтор ничего не стоит.

Что понадобится

  • CapSkip 3.0.0 или новее, запущенный на ПК с Windows. В этом выпуске появилось решение AWS WAF. Пошаговое руководство по настройке проведёт вас через первый запуск.
  • Python 3.10 или новее и пакет capskip версии 1.4.0 или новее. В версии 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.
  • Страница, которой вы владеете или которую вам разрешено автоматизировать, за правилом с действием CAPTCHA в AWS WAF.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другом ПК, VPS или размещённой платформе мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.

Как это проверялось: код запускался на трёх серверах-имитациях, которые я написал, а не на настоящем сайте с AWS WAF. Один копирует проверки AWS WAF, перечисленные в разделе Документация API, второй изображает защищённый сайт, а третий изображает хост токенов AWS. Использовался настоящий SDK capskip 1.4.0. Один раз я также запустил скрипт на приложении CapSkip на своём ПК, взяв имитацию сайта в качестве цели. Приложение не умеет решать поддельную страницу, поэтому три раза ответило ERROR_CAPTCHA_UNSOLVABLE, и скрипт завершился своим RuntimeError, что показывает, как эта ошибка доходит до цикла в виде ApiException. Настоящий сайт с AWS WAF в проверке не участвовал, поэтому ни одно число ниже не является временем решения, и я не могу сказать, что конкретный живой сайт принимает cookie, которую даёт эта схема.

Шаг 1: читаем страницу проверки

Когда правило с действием CAPTCHA срабатывает на запрос без действительного токена, AWS WAF отвечает HTTP 405 и заголовком x-amzn-waf-action, равным captcha. Согласно документации AWS WAF, страница с пазлом включается в ответ, только когда запрос отправляет заголовок Accept, запрашивающий text/html. Обычный вызов requests отправляет общий Accept, поэтому скрипт задаёт свой. Без него можно получить 405 без содержимого, которое можно прочитать.

Эта страница определяет небольшой объект gokuProps с тремя значениями: key, iv и context. Она также загружает challenge.js и captcha.js с хоста awswaf.com. Значения iv и context меняются с каждой страницей проверки, поэтому читайте их заново каждый раз. Ответ 202 с этим заголовком, равным challenge, означает другое: это тихое действие Challenge в AWS WAF, у которого нет пазла. В документации 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"),
    }

Функция возвращает None, если ответ не является страницей CAPTCHA, поэтому скрипт может отличить “нечего решать” от “решено”. Значения она читает двумя небольшими регулярными выражениями, а не парсером JSON, потому что на странице лежит JavaScript, а не JSON. Если на вашей цели ключи записаны в других кавычках, поправьте выражение, и больше ничего менять не нужно.

Шаг 2: решаем её через amazon_waf

Метод SDK называется amazon_waf. Обязательны только sitekey и url. Для страницы проверки sitekey равен gokuProps.key, а iv и context являются двумя другими значениями. Они передаются вместе: отправляйте оба или ни одного. Если опустить оба, CapSkip прочитает их со страницы в момент решения, и в документации это названо более безопасным вариантом, когда ваша копия страницы старше нескольких минут. В этом руководстве значения отправляются, потому что страница была загружена только что.

Два адреса скриптов необязательны. Каждый должен быть https-адресом на хосте awswaf.com, а SDK отклоняет любой другой ещё до отправки запроса. Ответ приходит как словарь с captcha_voucher и existing_token, а также с необработанным ответом в code и номером задания в captchaId. Метод ждёт с более длинным таймаутом в 300 секунд, как и для reCAPTCHA, потому что 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 или прокси на отдельный вызов, когда токен должен прийти из определённой сети. Собственный решатель CapSkip отправляет загрузки страниц, пазл и обмен voucher через один прокси, поэтому AWS видит один адрес. Если вы передаёте прокси в решение, отправляйте собственные запросы через тот же. Эту часть я проверить не смог. Отдельное руководство по ротации прокси рассказывает, как их менять.

Шаг 3: обмениваем voucher на cookie aws-waf-token

Пара voucher сама по себе бесполезна. AWS обменивает её на готовый cookie на эндпоинте voucher, который находится рядом с challenge.js: в той же папке, но с voucher вместо challenge.js. Отправьте пару POST-запросом в виде JSON, и в ответе будет поле token. В документации CapSkip тело отправляется как text/plain с заголовком Origin, который соответствует сайту. Собственный код приложения CapSkip отправляет ещё и Referer, поэтому функция отправляет оба. В документации SDK сказано, что пара живёт недолго, поэтому обменивайте её сразу, как только она пришла, и никогда не храните.

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: полный рабочий пример

Последний блок собирает схему AWS WAF CAPTCHA в Python воедино и добавляет повтор. Сохраните все четыре блока в один файл, один за другим, и запустите его. Каждая попытка снова загружает страницу, поэтому в каждой попытке свежие iv и context. Неудачный пазл вызывает ApiException, и цикл пробует снова с новым пазлом. Когда токен получен, скрипт запрашивает страницу с установленным 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 с настоящей страницей
Первые два пазла отклоненыДве строки попыток, затем статус 200 на третьем задании
Первые три пазла отклоненыТри строки попыток, затем RuntimeError
Запрос без заголовка AcceptИмитация отправила 405 без страницы, функция read_challenge вернула None, и скрипт напечатал 405, поэтому оставляйте заголовок

Cookie действует столько же, сколько время иммунитета сайта. По умолчанию AWS задаёт 300 секунд, а владелец сайта может выбрать от 60 секунд до трёх суток. Это описано в документации AWS WAF подробно. Когда снова приходит 405, запустите цикл заново. Если вы отправляете много запросов, сохраните токен и используйте его до этого момента, а не решайте капчу на каждый запрос.

Частые ошибки

ОшибкаПричинаИсправить
ValidationExceptioniv без context, адрес скрипта, который не является https-адресом на хосте awswaf.com, или url, который не является полным адресомОтправляйте iv и context вместе и копируйте каждый адрес скрипта точно так, как он записан на странице
ApiException с ERROR_CAPTCHA_UNSOLVABLEПазл не удалось решитьОтправьте снова. В разделе Документация API сказано, что каждая попытка получает новый пазл
ERROR_TASK_NOT_SUPPORTEDДокументация API указывает это для типа задачи, который отключён в CapSkipПроверьте настройки AWS WAF в приложении CapSkip
ERROR_PROXY_FORMATСтроку прокси не удалось разобратьИспользуйте user:pass@ip:port
ERROR_NO_SLOT_AVAILABLEУ API-ключа есть ограничение частоты запросов, и вы его превысилиПодождите несколько секунд и отправьте снова
NetworkExceptionCapSkip не запущен либо неверно указаны хост или портЗапустите CapSkip и проверьте настройки подключения
POST-запрос на voucher отклонёнСкорее всего, пара устарела или не совпадает. Код приложения CapSkip ожидает от AWS HTTP 400 для недействительного existing_token. Воспроизвести это на настоящем AWS я не смогОбменивайте сразу и отправляйте новое задание
Всё ещё 405 с cookieТокен истёк, или сайт читает его из другого местаРешите заново и проверьте, как собственные запросы страницы передают токен

Запуск решателя на сервере

Скрипту от CapSkip нужны две вещи: хост и порт. В режиме Local это 127.0.0.1 и ваш собственный ПК. В режиме Server CapSkip слушает ваш сетевой адрес или публичный IP, поэтому скрипт на другом ПК, VPS или размещённой платформе может обращаться к нему через тот же API. Смените хост и порт в конструкторе CapSkip, и больше в коде ничего не меняется. Для стабильного удалённого доступа рекомендуется статический публичный IP. Режим Server всё равно работает на вашем оборудовании и всё равно без счётчика. Но он открывает порт для любого адреса, поэтому добавьте в Windows Firewall блокирующее правило для адресов, которым доступ к нему не нужен. Смотрите раздел Настройки подключения с описанием переключения режимов.

FAQ

Можно ли решить AWS WAF CAPTCHA в Python без браузера?

Скрипту браузер не нужен. Он отправляет обычные HTTP-запросы к CapSkip и к сайту, а пазл решает CapSkip в настоящем браузере на той машине, где он запущен, как описано в документации SDK. Статус 405 лишь отмечает, что сработало действие CAPTCHA в AWS WAF.

Возвращает ли SDK aws-waf-token напрямую?

Нет. Он возвращает captcha_voucher и existing_token, а обменять их на токен нужно на эндпоинте voucher. JSON API createTask тоже принимает AWS WAF, и несколько его форматов возвращают токен сразу. В разделе Документация API они перечислены.

А если CAPTCHA представляет собой виджет внутри формы?

Это вторая форма, которую использует AWS WAF. Значения берутся из apiKey виджета и адреса его jsapi.js, а не из gokuProps. Пошаговое руководство по виджету на Node.js разбирает её, а вызов делается тем же методом.

Может ли размещённая платформа запускать это через мой CapSkip?

Да, через режим Server. Настройте CapSkip на прослушивание вашей сети или публичного IP, направьте конструктор CapSkip на этот хост и порт, и цикл работает так же, как на одном ПК. Решатель остаётся на вашем оборудовании.

Коротко

AWS WAF CAPTCHA в Python сводится к четырём действиям: прочитать страницу 405 с HTML-заголовком Accept, вызвать amazon_waf, обменять пару voucher на cookie и повторить запрос с ним. Повторы на вашей машине бесплатны, и именно ради этого нужен безлимитный сервис распознавания капчи для защищённого сайта, который иногда подсовывает пазл, на который нельзя ответить. Об остальных возможностях SDK читайте на странице сервиса распознавания капчи для Python.