Cara Memecahkan CAPTCHA AWS WAF di C# dengan HttpClient

Untuk memecahkan CAPTCHA AWS WAF di C#, Anda mengambil halaman yang dilindungi dengan HttpClient, membaca lima nilai dari jawaban HTTP 405 halaman itu, menyerahkannya ke AmazonWafAsync di pemecah AWS WAF CapSkip, lalu menukar pasangan voucher yang dikembalikan dengan cookie aws-waf-token yang selanjutnya dikirim oleh CookieContainer Anda. CapSkip memecahkan CAPTCHA di PC Windows Anda sendiri, jadi tidak ada biaya per pemecahan dan percobaan yang gagal tidak memakan biaya tambahan. Kodenya terdiri dari lima blok pendek dan sekitar 130 baris.
Apa yang Anda butuhkan
- CapSkip 3.0.0 atau lebih baru, terinstal di Windows. Dukungan AWS WAF dimulai dengan rilis itu.
- SDK .NET 8 atau lebih baru, ditambah paket NuGet CapSkip versi 1.4.0 atau lebih baru. Versi 1.3.0 tidak memiliki AmazonWafAsync, yang saya periksa di dalam paketnya. Semua yang di bawah dijalankan pada .NET SDK 10.0.100, pada net10.0 dan net8.0, dengan CapSkip 1.5.0.
- Halaman milik Anda sendiri, atau yang boleh Anda otomatisasi, yang dilindungi oleh aturan dengan aksi CAPTCHA AWS WAF. Panduan CAPTCHA AWS WAF di C# ini menangani halaman challenge yang menjawab HTTP 405. Halaman itu dijelaskan lebih rinci di panduan Python yang juga menautkan ke versi widget.
- Sebuah alamat untuk pemecah CAPTCHA. Mode Local adalah 127.0.0.1 di PC yang sama. Mode Server memungkinkan program di mesin lain memanggil CapSkip melalui API, seperti dijelaskan di bagian dekat akhir. Anda beralih di antara keduanya di pengaturan koneksi.
Cara pengujiannya: terhadap tiga server mock (server pengganti yang dibuat khusus untuk pengujian) yang saya tulis, dengan paket CapSkip 1.5.0 yang asli. Satu mock menerapkan pemeriksaan CapSkip dari bagian AWS WAF pada Dokumentasi API, satu lagi adalah situs terlindungi, dan satu lagi adalah host token. Handler khusus pengujian yang kecil mengalihkan host awswaf.com ke host token itu, dan handler itu tidak ada di kode di bawah. Aplikasi CapSkip berjalan di PC saya dan saya hanya mengirimkan permintaan yang tidak valid, jadi saya tidak memulai puzzle apa pun dan tidak melihat pemecahan sungguhan. Tidak ada situs AWS WAF yang aktif yang dilibatkan. Tidak ada waktu pemecahan di panduan ini, dan saya tidak bisa menjamin bahwa situs aktif tertentu menerima cookie yang dihasilkan alur ini.
Langkah 1: buat proyek dan baca halaman 405
Buat proyek konsol dan tambahkan paketnya. Template default mengaktifkan implicit using dan tipe nullable, dan kode ini bergantung pada keduanya.
dotnet new console -o aws-waf-demo cd aws-waf-demo dotnet add package CapSkip
Halaman yang dilindungi menjawab dengan HTTP 405 dan header x-amzn-waf-action yang diatur ke captcha. Isinya mendefinisikan window.gokuProps dan memuat challenge.js dan captcha.js dari host awswaf.com. Kode memerlukan lima nilai darinya: gokuProps.key, iv, dan context, ditambah alamat kedua skrip. Baik iv maupun context baru pada setiap respons, jadi jangan pernah memakai ulang pasangan lama.
Ganti isi Program.cs dengan blok C# dari Langkah 1 sampai 4, satu demi satu, mulai dari yang ini. Cuplikan pendek di Langkah 2 bukan salah satunya. Page.Read mencocokkan dengan dua regular expression, karena objeknya berada di dalam tag skrip, bukan di file JSON. Metode ini mengembalikan null jika respons bukan halaman CAPTCHA yang bisa di-parse. Itu mencakup 200 biasa, 405 dengan isi kosong, dan halaman 405 yang tidak memiliki salah satu dari lima nilai.
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;
}
}Blok kedua adalah sisi HttpClient. AWS mendokumentasikan bahwa halaman puzzle disertakan jika permintaan membawa header Accept dengan text/html (lihat Dokumentasi AWS WAF). HttpClient tidak mengirim header Accept kecuali Anda menambahkannya, jadi Web.Get mengaturnya. Tanpa itu, mock saya mengirim 405 kosong. Saya tidak menguji apa yang dikirim AWS yang asli.
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());
}
}Langkah 2: pecahkan dengan AmazonWafAsync
Signature-nya adalah AmazonWafAsync(sitekey, url, options, cancellationToken). Hanya sitekey dan url yang wajib, dan panggilan tanpa options berhasil pada eksekusi mock. Dokumentasi API menyebut bahwa nilai apa pun yang Anda tinggalkan dibaca dari halaman saat pemecahan. Kode ini tetap mengirim kelima nilai, karena baru saja mengambil halaman dan Langkah 3 memerlukan alamat challenge.js bagaimanapun juga. Nilai iv dan context berpasangan. Mengirim salah satu tanpa yang lain melempar ValidationException sebelum permintaan apa pun. Hal yang sama terjadi untuk alamat skrip yang bukan https pada host awswaf.com yang berakhir dengan challenge.js, captcha.js, atau jsapi.js. Saya mencoba host lain, alamat http, dan nama file yang salah, dan ketiganya ditolak.
Hasilnya adalah SolveResult. CaptchaVoucher dan ExistingToken menyimpan pasangannya, Code menyimpan jawaban mentah sebagai string JSON, dan CaptchaId menyimpan nomor task. Panggilan ini menunggu hingga default recaptchaTimeout 300 detik, karena CapSkip membuka browser sungguhan untuk menerbitkan token sebelum menjawab puzzle. Saya tidak mengukur waktu satu pemecahan pun.
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);
}Untuk mengarahkan pemecahan melalui proxy, tambahkan entri proxy ke dictionary options. Permintaan Anda sendiri harus keluar dari alamat IP yang sama, jadi berikan proxy yang sama ke handler Anda. WebProxy tidak membaca string user:pass@, jadi login masuk ke Credentials. Kode CapSkip sendiri mengambilnya dengan cara yang sama. Baris pertama di bawah sampai ke in.php sebagai proxy dan proxytype pada eksekusi mock. Baris kedua menggantikan handler di dalam Web.Client. Baris itu berhasil dikompilasi, tetapi saya tidak mengirim permintaan melalui proxy, jadi anggap belum teruji.
// 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") } };Langkah 3: tukar voucher dengan HttpClient
Dalam alur ini kode Anda yang melakukan penukaran voucher, bukan CapSkip, jadi HttpClient Anda adalah alamat IP yang dilihat AWS pada tahap itu. Endpoint voucher berada di samping challenge.js: folder yang sama, dengan voucher sebagai pengganti challenge.js. Web.Exchange mengirim pasangan itu dengan POST sebagai JSON dan membaca field token dari balasan. Contoh di dokumentasi API mengirim content type text/plain;charset=UTF-8 dan header Origin. Saya membaca kode penukaran milik aplikasi CapSkip sendiri, yang juga menambahkan Referer dan User-Agent dari browser yang menerbitkan token, dan menghitung HTTP 400 untuk existing_token yang null atau tidak valid sebagai penukaran yang gagal. Web.Exchange mengirim keempat header itu. Jawaban in.php tidak mengembalikan User-Agent tersebut, jadi kode ini mengirim User-Agent Chrome generik, dan saya tidak bisa menguji apakah AWS mempermasalahkan perbedaannya. Dokumentasi SDK menyebut agar pasangan ditukar segera setelah diterima. Dokumentasi itu tidak memberi masa berlaku, dan saya tidak mengukurnya.
Content type perlu diperhatikan di C#. StringContent dengan text/plain dan Encoding.UTF8 menuliskannya sebagai text/plain; charset=utf-8, dengan spasi setelah titik koma dan utf-8 huruf kecil, yang saya ukur. Karena itu metode ini membuat ByteArrayContent dan mengatur header secara manual. Mock mencatat ejaan yang persis sesuai dokumentasi.
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
}
}Langkah 4: contoh lengkap yang berfungsi
Ubah PageUrl ke halaman Anda sendiri yang dilindungi terlebih dahulu. Seperti yang ditempel, kode ini meminta example.com, yang tidak punya aturan CAPTCHA, jadi program tidak menemukan apa pun untuk dipecahkan. Lalu jalankan dengan dotnet run. Setiap percobaan mengambil halaman lagi, sehingga tiap percobaan mendapat iv dan context sendiri. Pesan ApiException berisi kode error, jadi filter catch hanya mencoba ulang ERROR_CAPTCHA_UNSOLVABLE. Kode lain berarti percobaan ulang akan gagal dengan cara yang sama, jadi error itu muncul. Jika Anda mengubah teks error pengganti di CapSkip, filter tidak akan cocok. Token masuk ke jar untuk host PageUrl, dan permintaan terakhir membaca halaman lagi. Itulah seluruh loop CAPTCHA AWS WAF di 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;
}
}Pada eksekusi mock, program berperilaku seperti ini. Setiap baris dijalankan pada net10.0 dan net8.0 dengan output yang sama.
| Pengaturan mock | Yang dilakukan program |
|---|---|
| Mock menjawab setiap puzzle | Satu task dan satu penukaran, lalu 200 dan halaman yang asli |
| Mock menolak dua puzzle pertama | Dua baris percobaan, lalu 200 setelah task ketiga |
| Mock menolak tiga puzzle pertama | Tiga baris percobaan, pesan bahwa CAPTCHA tidak berhasil dipecahkan, exit code 1, dan tidak ada permintaan voucher |
| Baris Accept dihapus dari Web.Get | Mock mengirim 405 kosong, Page.Read mengembalikan null, dan program mencetak 405 lalu keluar dengan 1 |
| Mock menjawab ERROR_BAD_PARAMETERS | ApiException muncul pada percobaan pertama, tidak ditangani, tanpa percobaan ulang |
| Pasangan voucher diubah sebelum penukaran | Mock menjawab 400 dan program berakhir dengan HttpRequestException yang tidak ditangani |
Cookie berfungsi selama immunity time CAPTCHA situs itu, yaitu periode saat AWS WAF membiarkan klien yang sudah memecahkan CAPTCHA melewati puzzle. Menurut dokumentasi immunity time AWS WAF defaultnya 300 detik, dan pemilik situs bisa mengaturnya dari 60 detik sampai tiga hari. Pertahankan satu HttpClient dan jar-nya selama seluruh eksekusi, dan pakai ulang token sampai 405 muncul lagi.
Kesalahan umum
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| CapSkip.ValidationException | Dilempar oleh SDK sebelum permintaan apa pun: iv dikirim tanpa context, alamat skrip di luar https dan awswaf.com, url yang bukan http atau https, atau sitekey kosong. Pesannya menyebut penyebabnya | Perbaiki argumen yang disebutkan |
| ApiException: ERROR_CAPTCHA_UNSOLVABLE | CapSkip tidak bisa menjawab puzzle itu. Menurut Dokumentasi API setiap percobaan baru mendapat puzzle yang berbeda | Loop mencobanya lagi dengan halaman baru |
| ApiException dengan kode lain | ERROR_BAD_PARAMETERS, ERROR_PROXY_FORMAT, ERROR_TASK_NOT_SUPPORTED (dokumentasi API menyebut tipe task tidak didukung atau pengaturannya dimatikan di CapSkip) atau ERROR_NO_SLOT_AVAILABLE (key yang melebihi rate limit permintaan yang diatur di CapSkip, yang hanya berlaku untuk key yang memilikinya) | Error muncul tanpa percobaan ulang. Baca pesannya. Untuk rate limit, tunggu beberapa detik dan jalankan lagi |
| CapSkip.TimeoutException | Tidak ada jawaban dalam waktu tunggu. Ini turunan CapSkipError, bukan ApiException, jadi filter percobaan ulang tidak menangkapnya | Pastikan CapSkip berjalan dan memecahkan, lalu jalankan lagi |
| CapSkip.NetworkException | CapSkip tidak berjalan, host atau port-nya salah, atau cancellation token Anda terpicu | Jalankan CapSkip, periksa pengaturan koneksi, dan baca pesannya |
| HttpRequestException dari Web.Exchange | EnsureSuccessStatusCode menemukan balasan yang tidak berhasil. Pasangan yang tidak cocok atau diubah memberikan 400 pada mock. Saya tidak bisa mereproduksi kasus yang nyata, dan pasangan yang ditukar terlalu terlambat mungkin gagal dengan cara yang sama | Tukar segera dan kirim task baru |
| Error kompiler CS0104 | TimeoutException tanpa kualifikasi bersifat ambigu antara System dan CapSkip | Tulis CapSkip.TimeoutException |
| 405 meskipun sudah memakai cookie | Token kedaluwarsa, cookie ditambahkan untuk host lain, atau situs membaca token dari header | Pecahkan lagi, lalu bandingkan dengan yang dikirim permintaan milik halaman itu sendiri |
Menjalankan CapSkip di server
CapSkipClient menerima host dan port, ditambah apiKey jika Anda mengaktifkan API Key Validation, jadi memindahkan pemecah hanyalah perubahan satu baris: berikan alamat mesin lain sebagai ganti 127.0.0.1. Mode Server membuat CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga program .NET di PC lain, VPS, atau platform hosting dapat memanggil API yang sama. Mesin yang menjalankan CapSkip harus Windows, berupa PC atau server Windows, dan tetap milik Anda serta tanpa meteran. CapSkip menyarankan IP publik statis agar akses jarak jauh tetap stabil. Port itu lalu menerima koneksi dari alamat mana pun, jadi tambahkan aturan blokir Windows Firewall untuk alamat yang tidak boleh masuk. Pemecahan lalu berjalan di mesin itu, sedangkan penukaran voucher di Langkah 3 berjalan di mana pun program Anda berjalan. Saya tidak menguji apakah AWS mempermasalahkan perbedaan itu. Jika ya, berikan proxy yang sama ke kedua tahap.
FAQ
Mengapa catch (TimeoutException) tidak bisa dikompilasi dengan paket CapSkip?
System dan CapSkip sama-sama mendefinisikan TimeoutException. Dengan implicit usings aktif dan using CapSkip di bagian atas file, nama pendeknya ambigu dan kompiler berhenti dengan error CS0104. Tulis CapSkip.TimeoutException, atau tangkap CapSkipError, kelas dasar dari semua yang dilempar SDK.
Mengapa tidak memakai StringContent untuk POST voucher?
StringContent menulis ulang content type. Dengan text/plain dan Encoding.UTF8 ia mengirim text/plain; charset=utf-8, sedangkan dokumentasi API menunjukkan text/plain;charset=UTF-8. ByteArrayContent dengan header yang ditulis manual mengirim ejaan sesuai dokumentasi. Saya tidak bisa memeriksa apakah AWS yang asli memperlakukan keduanya secara berbeda, jadi saya menyalin yang sesuai dokumentasi.
Apakah cookie berfungsi di host atau subdomain lain?
Tidak dengan cara contoh menambahkannya. Cookie yang ditambahkan ke CookieContainer untuk alamat PageUrl bersifat host-only: pada sebuah uji coba, cookie itu dikirim ke www.example.com dan tidak ke api.example.com atau example.com. Jika situs Anda memanggil host lain, tambahkan cookie untuk host itu juga. Dokumentasi API juga menyebut bahwa sebagian halaman mengirim token di header permintaan x-aws-waf-token alih-alih cookie, jadi periksa apa yang dikirim permintaan milik halaman itu sendiri.
Bagaimana cara membatalkan pemecahan yang memakan waktu terlalu lama?
Berikan CancellationToken ke AmazonWafAsync, seperti yang dilakukan contoh dengan sumber sepuluh menit. Token yang sudah dibatalkan menghentikan panggilan sebelum apa pun dikirim, tetapi muncul sebagai CapSkip.NetworkException dengan pesan A task was canceled, bukan sebagai OperationCanceledException. Tipe yang sama juga melaporkan bahwa CapSkip tidak berjalan, jadi baca pesannya.
Sebuah CAPTCHA AWS WAF di C# tidak dikenai biaya untuk dicoba ulang karena pemecahnya berjalan di PC Anda sendiri, itulah sebabnya bypass captcha lokal cocok untuk situs yang memberi Anda puzzle baru pada setiap percobaan. Alur yang sama di Python ada di panduan Python, dan bagian SDK lainnya ada di Halaman pemecah CAPTCHA C#.
