So lösen Sie AWS WAF CAPTCHA in C# mit HttpClient

Um ein AWS WAF CAPTCHA in C# zu lösen, rufen Sie die geschützte Seite mit HttpClient ab und lesen fünf Werte aus ihrer HTTP-405-Antwort. Diese übergeben Sie an AmazonWafAsync im CapSkip-Solver für AWS WAF. Das zurückgegebene Voucher-Paar tauschen Sie gegen ein Cookie aws-waf-token ein, das Ihr CookieContainer von da an sendet. CapSkip löst auf Ihrem eigenen Windows-PC, daher gibt es keine Gebühr pro Lösung, und ein fehlgeschlagener Versuch kostet nichts extra. Der Code besteht aus fünf kurzen Blöcken und etwa 130 Zeilen.
Was Sie brauchen
- CapSkip 3.0.0 oder neuer, unter Windows installiert. Die Unterstützung für AWS WAF beginnt mit dieser Version.
- Das .NET 8 SDK oder neuer sowie das NuGet-Paket CapSkip ab Version 1.4.0. Version 1.3.0 hat kein AmazonWafAsync, was ich im Paket geprüft habe. Alles unten lief auf dem .NET SDK 10.0.100, auf net10.0 und net8.0, mit CapSkip 1.5.0.
- Eine eigene Seite oder eine, die Sie automatisieren dürfen, geschützt durch eine Regel mit der Aktion AWS WAF CAPTCHA. Dieser Leitfaden zum AWS WAF CAPTCHA in C# behandelt die Challenge-Seite, die mit HTTP 405 antwortet. Der Python-Leitfaden erklärt diese Seite genauer und verweist auf die Widget-Version.
- Eine Adresse für den Solver. Der Local-Modus ist 127.0.0.1 auf demselben PC. Mit dem Server-Modus kann ein Programm auf einem anderen Rechner CapSkip über die API aufrufen, wie der Abschnitt gegen Ende erklärt. Zwischen beiden wechseln Sie in den Verbindungseinstellungen.
So wurde getestet: gegen drei Mock-Server, die ich geschrieben habe, mit dem echten Paket CapSkip 1.5.0. Einer übernimmt die Prüfungen aus dem AWS-WAF-Abschnitt der CapSkip API-Dokumentation, einer ist die geschützte Website und einer ist der Token-Host. Ein kleiner Handler, nur für den Test, leitete die awswaf.com-Hosts auf diesen Token-Host um, und er steht nicht im Code unten. Die CapSkip-App lief auf meinem PC, und ich habe ihr nur ungültige Anfragen gesendet, daher habe ich kein Puzzle gestartet und keine echte Lösung gesehen. Es war keine echte AWS-WAF-Website beteiligt. In diesem Beitrag gibt es keine Lösezeit, und ich kann nicht versprechen, dass eine bestimmte echte Website das Cookie akzeptiert, das dieser Ablauf erzeugt.
Schritt 1: Das Projekt anlegen und die 405-Seite lesen
Legen Sie ein Konsolenprojekt an und fügen Sie das Paket hinzu. Die Standardvorlage schaltet implizite usings und Nullable-Typen ein, und der Code verlässt sich auf beides.
dotnet new console -o aws-waf-demo cd aws-waf-demo dotnet add package CapSkip
Eine geschützte Seite antwortet mit HTTP 405 und dem Header x-amzn-waf-action mit dem Wert captcha. Der Body definiert window.gokuProps und lädt challenge.js und captcha.js von einem awswaf.com-Host. Der Code braucht daraus fünf Werte: gokuProps.key, iv und context sowie die Adressen beider Skripte. Sowohl iv als auch context sind bei jeder Antwort neu, verwenden Sie also nie ein altes Paar wieder.
Ersetzen Sie den Inhalt von Program.cs durch die C#-Blöcke der Schritte 1 bis 4, einen nach dem anderen, beginnend mit diesem. Das kurze Snippet in Schritt 2 gehört nicht dazu. Page.Read arbeitet mit zwei regulären Ausdrücken, da das Objekt in einem Script-Tag steht und nicht in einer JSON-Datei. Es gibt null zurück, wenn die Antwort keine auswertbare Captcha-Seite ist. Das umfasst ein einfaches 200, eine 405 mit leerem Body und eine 405-Seite, der einer der fünf Werte fehlt.
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using CapSkip;
record Challenge(string SiteKey, string Iv, string Context, string ChallengeScript, string CaptchaScript);
static class Page
{
// Read the values from an AWS WAF CAPTCHA page (HTTP 405), or return null.
public static Challenge? Read(HttpStatusCode status, string html)
{
if (status != HttpStatusCode.MethodNotAllowed || !html.Contains("gokuProps")) return null;
var block = Regex.Match(html, @"gokuProps\s*=\s*\{(.*?)\}", RegexOptions.Singleline).Groups[1].Value;
string Field(string name) =>
Regex.Match(block, "\"" + name + "\"\\s*:\\s*\"([^\"]+)\"").Groups[1].Value;
string Script(string name) =>
Regex.Match(html, "<script[^>]+src=\"([^\"]+/" + name + "\\.js)\"").Groups[1].Value;
var found = new Challenge(Field("key"), Field("iv"), Field("context"),
Script("challenge"), Script("captcha"));
var values = new[] { found.SiteKey, found.Iv, found.Context, found.ChallengeScript, found.CaptchaScript };
return values.Any(string.IsNullOrEmpty) ? null : found;
}
}Der zweite Block ist die HttpClient-Seite. AWS dokumentiert, dass die Puzzle-Seite enthalten ist, wenn die Anfrage einen Accept-Header mit text/html trägt (siehe die AWS-WAF-Dokumentation). HttpClient sendet keinen Accept-Header, wenn Sie keinen hinzufügen, daher setzt Web.Get ihn. Ohne ihn sendet mein Mock eine leere 405. Ich habe nicht getestet, was das echte AWS sendet.
static partial class Web
{
const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
// The jar lives in the handler, so a cookie you add later goes out with every request to its host.
public static HttpClient Client(CookieContainer jar) =>
new(new HttpClientHandler { CookieContainer = jar }) { Timeout = TimeSpan.FromSeconds(30) };
public static async Task<(HttpStatusCode Status, string Body)> Get(HttpClient http, string url)
{
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
request.Headers.TryAddWithoutValidation("Accept", "text/html"); // HttpClient sends none
using var response = await http.SendAsync(request);
return (response.StatusCode, await response.Content.ReadAsStringAsync());
}
}Schritt 2: Mit AmazonWafAsync lösen
Die Signatur lautet AmazonWafAsync(sitekey, url, options, cancellationToken). Nur sitekey und url sind Pflicht, und ein Aufruf ohne Optionen funktionierte im Mock-Lauf. Laut der API-Dokumentation wird jeder weggelassene Wert zur Lösezeit aus der Seite gelesen. Dieser Code sendet trotzdem alle fünf, weil er die Seite gerade erst abgerufen hat und Schritt 3 die Adresse von challenge.js ohnehin braucht. Die Werte iv und context gehören zusammen. Wenn Sie einen ohne den anderen senden, wird vor jeder Anfrage eine ValidationException ausgelöst. Dasselbe passiert bei einer Skript-Adresse, die nicht https auf einem awswaf.com-Host ist, der auf challenge.js, captcha.js oder jsapi.js endet. Ich habe einen anderen Host, eine http-Adresse und einen falschen Dateinamen ausprobiert, und alle drei wurden abgelehnt.
Das Ergebnis ist ein SolveResult. CaptchaVoucher und ExistingToken enthalten das Paar, Code enthält die rohe Antwort als JSON-String, und CaptchaId enthält die Aufgabennummer. Der Aufruf wartet bis zum Standardwert von 300 Sekunden für recaptchaTimeout, weil CapSkip einen echten Browser öffnet, um einen Token zu erzeugen, bevor es das Puzzle beantwortet. Ich habe keine Lösung gemessen.
static class Solve
{
// Local mode. In Server mode, use your server's address instead.
static readonly CapSkipClient Solver = new(host: "127.0.0.1", port: 8080);
public static Task<SolveResult> Run(Challenge c, string pageUrl, CancellationToken ct) =>
Solver.AmazonWafAsync(c.SiteKey, pageUrl, new Dictionary<string, object?>
{
["iv"] = c.Iv,
["context"] = c.Context,
["challenge_script"] = c.ChallengeScript,
["captcha_script"] = c.CaptchaScript,
}, ct);
}Um die Lösung über einen Proxy zu leiten, fügen Sie dem Dictionary options einen Eintrag proxy hinzu. Ihre eigenen Anfragen sollten von derselben IP-Adresse ausgehen, geben Sie Ihrem Handler also denselben Proxy. WebProxy liest keinen String user:pass@, daher kommt der Login in Credentials. Der eigene Code von CapSkip holt ihn auf dieselbe Weise heraus. Die erste Zeile unten kam im Mock-Lauf als proxy und proxytype bei in.php an. Die zweite ersetzt den Handler in Web.Client. Sie ließ sich kompilieren, aber ich habe keine Anfrage über einen Proxy gesendet, betrachten Sie sie also als ungetestet.
// Snippet, not part of Program.cs. The first line is checked at in.php, the second is not tested.
["proxy"] = new Proxy("HTTP", "user:[email protected]:8080"), // add to the options in Solve.Run
var handler = new HttpClientHandler { UseProxy = true, CookieContainer = jar, Proxy =
new WebProxy("http://1.2.3.4:8080") { Credentials = new NetworkCredential("user", "pass") } };Schritt 3: Den Voucher mit HttpClient tauschen
In diesem Ablauf führt Ihr Code den Voucher-Tausch durch, nicht CapSkip, daher sieht AWS auf dieser Teilstrecke die IP-Adresse Ihres HttpClient. Der Voucher-Endpunkt liegt neben challenge.js: im selben Ordner, mit voucher statt challenge.js. Web.Exchange sendet das Paar per POST als JSON und liest das Feld token der Antwort. Das Beispiel in der API-Dokumentation sendet den Inhaltstyp text/plain;charset=UTF-8 und einen Origin-Header. Ich habe den eigenen Tauschcode der CapSkip-App gelesen, der zusätzlich einen Referer und den User-Agent des Browsers ergänzt, der den Token ausgestellt hat, und HTTP 400 bei einem null oder ungültigen existing_token als gescheiterten Tausch zählt. Web.Exchange sendet alle vier Header. Die Antwort von in.php liefert diesen User-Agent nicht, daher sendet dieser Code einen allgemeinen Chrome-User-Agent, und ich konnte nicht testen, ob AWS sich an dem Unterschied stört. Die SDK-Dokumentation sagt, dass Sie das Paar tauschen sollen, sobald es eintrifft. Eine Lebensdauer nennt sie nicht, und ich habe keine gemessen.
Der Inhaltstyp braucht in C# Sorgfalt. StringContent mit text/plain und Encoding.UTF8 schreibt ihn als text/plain; charset=utf-8, mit einem Leerzeichen nach dem Semikolon und einem kleingeschriebenen utf-8, was ich gemessen habe. Daher baut die Methode ein ByteArrayContent und setzt den Header von Hand. Der Mock hat die dokumentierte Schreibweise genau aufgezeichnet.
static partial class Web
{
// Trade the voucher pair for the aws-waf-token. Your code makes this call, not CapSkip.
public static async Task<string> Exchange(HttpClient http, SolveResult result, Challenge c, string pageUrl)
{
// The voucher endpoint sits beside challenge.js, same folder.
var voucherUrl = c.ChallengeScript[..c.ChallengeScript.LastIndexOf('/')] + "/voucher";
var origin = new Uri(pageUrl).GetLeftPart(UriPartial.Authority);
var pair = JsonSerializer.Serialize(new
{
captcha_voucher = result.CaptchaVoucher,
existing_token = result.ExistingToken,
});
using var request = new HttpRequestMessage(HttpMethod.Post, voucherUrl)
{
Content = new ByteArrayContent(Encoding.UTF8.GetBytes(pair)),
};
// StringContent would rewrite this header, so write it exactly.
request.Content.Headers.TryAddWithoutValidation("Content-Type", "text/plain;charset=UTF-8");
request.Headers.TryAddWithoutValidation("Origin", origin);
request.Headers.TryAddWithoutValidation("Referer", origin + "/");
request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("token").GetString()!; // the aws-waf-token cookie value
}
}Schritt 4: Das vollständige funktionierende Beispiel
Ersetzen Sie PageUrl zuerst durch Ihre eigene geschützte Seite. So eingefügt ruft das Programm example.com ab, das keine CAPTCHA-Regel hat, daher findet es nichts zu lösen. Führen Sie es dann mit dotnet run aus. Jeder Versuch ruft die Seite erneut ab, wodurch jeder Versuch sein eigenes iv und context bekommt. Die Meldung der ApiException enthält den Fehlercode, daher wiederholt der Catch-Filter nur ERROR_CAPTCHA_UNSOLVABLE. Jeder andere Code bedeutet, dass eine Wiederholung genauso scheitern würde, daher wird er bis zu Ihnen durchgereicht. Wenn Sie in CapSkip den Ersatztext für Fehlermeldungen geändert haben, passt der Filter nicht. Der Token kommt in den Cookie-Jar für den Host von PageUrl, und die letzte Anfrage liest die Seite erneut. Das ist die ganze Schleife für ein AWS WAF CAPTCHA in C#.
static class Program
{
const string PageUrl = "https://www.example.com/"; // change this to your protected page
static async Task<int> Main()
{
var jar = new CookieContainer();
using var http = Web.Client(jar);
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); // a budget for all solves
for (var attempt = 1; attempt <= 3; attempt++)
{
var (status, html) = await Web.Get(http, PageUrl); // fresh page, fresh iv and context
var challenge = Page.Read(status, html);
if (challenge is null)
{
Console.WriteLine($"{(int)status} {html[..Math.Min(60, html.Length)]}");
return status == HttpStatusCode.OK ? 0 : 1; // 200 means there was nothing to solve
}
SolveResult result;
try { result = await Solve.Run(challenge, PageUrl, cts.Token); }
catch (ApiException ex) when (ex.Message.Contains("ERROR_CAPTCHA_UNSOLVABLE"))
{
Console.WriteLine($"attempt {attempt}: {ex.Message}"); // the message holds the error code
continue; // a fresh page draws a fresh puzzle
}
catch (CapSkip.TimeoutException)
{
Console.WriteLine("CapSkip gave no answer in time");
return 1;
}
var token = await Web.Exchange(http, result, challenge, PageUrl); // never store the pair
jar.Add(new Uri(PageUrl), new Cookie("aws-waf-token", token));
var (code, page) = await Web.Get(http, PageUrl);
Console.WriteLine($"{(int)code} {page[..Math.Min(60, page.Length)]}");
return code == HttpStatusCode.OK ? 0 : 1;
}
Console.WriteLine("AWS WAF CAPTCHA not solved in 3 attempts");
return 1;
}
}Im Mock-Lauf verhielt sich das Programm so. Jede Zeile lief auf net10.0 und net8.0 mit derselben Ausgabe.
| Mock-Einstellung | Was das Programm tat |
|---|---|
| Mock beantwortet jedes Puzzle | Eine einzelne Aufgabe und ein einzelner Tausch, dann 200 und die echte Seite |
| Mock lehnt die ersten zwei Puzzles ab | Zwei Versuchszeilen, dann 200 nach der dritten Aufgabe |
| Mock lehnt die ersten drei Puzzles ab | Drei Versuchszeilen, die Meldung, dass das Captcha nicht gelöst wurde, Exit-Code 1 und keine Voucher-Anfrage |
| Zeile Accept aus Web.Get entfernt | Der Mock sendete eine leere 405, Page.Read gab null zurück, und das Programm gab 405 aus und beendete sich mit 1 |
| Mock antwortet mit ERROR_BAD_PARAMETERS | Die ApiException trat beim ersten Versuch unbehandelt auf, ohne Wiederholung |
| Voucher-Paar vor dem Tausch verändert | Der Mock antwortete mit 400, und das Programm endete mit einer unbehandelten HttpRequestException |
Das Cookie gilt für die CAPTCHA-Immunitätszeit der Website, den Zeitraum, in dem AWS WAF einem Client, der das Puzzle gelöst hat, dessen erneute Anzeige erspart. Die AWS-WAF-Dokumentation zur Immunitätszeit gibt 300 Sekunden als Standard an und lässt den Websitebetreiber den Wert von 60 Sekunden bis zu drei Tagen einstellen. Verwenden Sie einen HttpClient und seinen Cookie-Jar für den ganzen Lauf, und nutzen Sie den Token weiter, bis die 405 wiederkommt.
Häufige Fehler
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| CapSkip.ValidationException | Vom SDK vor jeder Anfrage ausgelöst: iv ohne context gesendet, eine Skript-Adresse außerhalb von https und awswaf.com, eine url, die nicht http oder https ist, oder ein leerer sitekey. Die Meldung nennt den Verursacher | Korrigieren Sie das genannte Argument |
| ApiException: ERROR_CAPTCHA_UNSOLVABLE | CapSkip konnte dieses Puzzle nicht beantworten. Die API-Dokumentation sagt, dass jeder neue Versuch ein anderes Puzzle bekommt | Die Schleife versucht es mit einer neuen Seite erneut |
| ApiException mit einem anderen Code | ERROR_BAD_PARAMETERS, ERROR_PROXY_FORMAT, ERROR_TASK_NOT_SUPPORTED (laut der API-Dokumentation wird der Aufgabentyp nicht unterstützt oder seine Einstellung ist in CapSkip ausgeschaltet) oder ERROR_NO_SLOT_AVAILABLE (ein API-Schlüssel über dem in CapSkip eingestellten Limit für die Anfragerate, das nur für API-Schlüssel gilt, die eines haben) | Sie wird ohne Wiederholung weitergereicht. Lesen Sie die Meldung. Beim Limit für die Anfragerate warten Sie ein paar Sekunden und führen es dann erneut aus |
| CapSkip.TimeoutException | Keine Antwort innerhalb der Wartezeit. Die Exception stammt von CapSkipError ab, nicht von ApiException, daher fängt der Wiederholungsfilter sie nicht ab | Prüfen Sie, ob CapSkip läuft und löst, und führen Sie das Skript dann erneut aus |
| CapSkip.NetworkException | CapSkip läuft nicht, Host oder Port sind falsch, oder Ihr Abbruch-Token wurde ausgelöst | Starten Sie CapSkip, prüfen Sie die Verbindungseinstellungen und lesen Sie die Meldung |
| HttpRequestException von Web.Exchange | EnsureSuccessStatusCode sah eine Antwort ohne Erfolg. Ein nicht passendes oder verändertes Paar ergab im Mock 400. Einen echten Fall konnte ich nicht reproduzieren, und ein zu spät getauschtes Paar könnte genauso scheitern | Tauschen Sie sofort und senden Sie eine neue Aufgabe |
| Compilerfehler CS0104 | Eine bloße TimeoutException ist zwischen System und CapSkip mehrdeutig | Schreiben Sie CapSkip.TimeoutException |
| 405 trotz Cookie | Der Token ist abgelaufen, das Cookie wurde für einen anderen Host hinzugefügt, oder die Website liest den Token aus einem Header | Lösen Sie erneut und vergleichen Sie dann mit dem, was die eigenen Anfragen der Seite senden |
CapSkip auf einem Server betreiben
CapSkipClient nimmt einen Host und einen Port entgegen, dazu einen apiKey, wenn Sie API Key Validation eingeschaltet haben. Den Solver zu verlegen ist also eine Änderung in einer Zeile: Übergeben Sie die Adresse des anderen Rechners statt 127.0.0.1. Der Server-Modus lässt CapSkip auf Ihrer Netzwerkadresse oder einer öffentlichen IP lauschen, sodass ein .NET-Programm auf einem anderen PC, einem VPS oder einer gehosteten Plattform dieselbe API aufrufen kann. Der Rechner, auf dem CapSkip läuft, muss Windows sein, ein PC oder ein Windows-Server, und er bleibt Ihr eigener, ohne Gebühren pro Lösung. CapSkip empfiehlt eine statische öffentliche IP, damit der Fernzugriff stabil bleibt. Der Port nimmt dann Verbindungen von jeder Adresse an, fügen Sie daher eine Blockierregel der Windows Firewall für diejenigen hinzu, die nicht hinein sollen. Die Lösung läuft dann auf diesem Rechner, während der Voucher-Tausch in Schritt 3 dort läuft, wo Ihr Programm läuft. Ich habe nicht getestet, ob AWS sich an diesem Unterschied stört. Falls doch, übergeben Sie beiden Teilstrecken denselben Proxy.
FAQ
Warum lässt sich catch (TimeoutException) mit dem CapSkip-Paket nicht kompilieren?
Sowohl System als auch CapSkip definieren eine TimeoutException. Bei eingeschalteten impliziten Usings und using CapSkip am Anfang der Datei ist der Kurzname mehrdeutig, und der Compiler bricht mit dem Fehler CS0104 ab. Schreiben Sie CapSkip.TimeoutException oder fangen Sie CapSkipError ab, die Basisklasse von allem, was das SDK wirft.
Warum nicht StringContent für den Voucher-POST verwenden?
Es schreibt den Inhaltstyp um. StringContent mit text/plain und Encoding.UTF8 sendet text/plain; charset=utf-8, während die API-Dokumentation text/plain;charset=UTF-8 zeigt. Ein ByteArrayContent mit von Hand geschriebenem Header sendet die dokumentierte Schreibweise. Ich konnte nicht prüfen, ob das echte AWS die beiden unterschiedlich behandelt, daher habe ich die dokumentierte übernommen.
Funktioniert das Cookie auf anderen Hosts oder Subdomains?
Nicht so, wie das Beispiel es hinzufügt. Ein Cookie, das dem CookieContainer für die Adresse PageUrl hinzugefügt wird, gilt nur für genau diesen Host: In einem Test ging es an www.example.com und nicht an api.example.com oder example.com. Wenn Ihre Website einen anderen Host aufruft, fügen Sie das Cookie auch für diesen Host hinzu. Laut der API-Dokumentation geben manche Seiten den Token außerdem in einem Request-Header x-aws-waf-token statt in einem Cookie weiter, prüfen Sie also, was die eigenen Anfragen der Seite senden.
Wie breche ich eine Lösung ab, die zu lange dauert?
Übergeben Sie einen CancellationToken an AmazonWafAsync, wie es das Beispiel mit einer CancellationTokenSource von zehn Minuten tut. Ein bereits abgebrochener Token stoppt den Aufruf, bevor etwas gesendet wird, tritt aber als CapSkip.NetworkException mit der Meldung A task was canceled auf, nicht als OperationCanceledException. Derselbe Typ meldet, dass CapSkip nicht läuft, lesen Sie also die Meldung.
Bei einem AWS WAF CAPTCHA in C# kostet eine Wiederholung nichts, weil der Solver lokal auf Ihrem eigenen PC läuft. Deshalb eignet sich die Captcha-Umgehung gut für Websites, die Ihnen bei jedem Versuch ein neues Puzzle liefern. Denselben Ablauf in Python zeigt der Python-Leitfaden, und den Rest des SDK finden Sie auf der C#-Captcha-Solver-Seite.
