C# ve HttpClient ile AWS WAF CAPTCHA Nasıl Çözülür

aws waf captcha in c# - How to Pass an AWS WAF CAPTCHA in C# With HttpClient

C#’ta bir AWS WAF CAPTCHA’sını çözmek için korumalı sayfayı HttpClient ile çekip, HTTP 405 yanıtından beş değer çıkarıp, bunları CapSkip AWS WAF çözücüsü üzerinden AmazonWafAsync ile gönderip, döndürdüğü voucher çiftini bir aws-waf-token çerezi ile takas edersiniz; CookieContainer’ınız çerezi bundan sonra gönderir. CapSkip kendi Windows bilgisayarınızda çözer, bu yüzden çözüm başına ücret yoktur ve başarısız bir deneme ekstra hiçbir şeye mal olmaz. Kod beş kısa bloktan ve yaklaşık 130 satırdan oluşur.

Neye ihtiyacınız var

  • Windows’ta kurulu CapSkip 3.0.0 veya üzeri. AWS WAF desteği bu sürümle başlar.
  • Kurulu bir .NET 8 SDK veya üzeri ve CapSkip NuGet paketinin 1.4.0 veya üzeri sürümü. 1.3.0 sürümünde AmazonWafAsync yoktur; bunu pakette kontrol ettim. Aşağıdaki her şey .NET SDK 10.0.100 üzerinde, net10.0 ve net8.0 hedeflerinde, CapSkip 1.5.0 ile çalıştı.
  • Sahibi olduğunuz ya da otomatikleştirmenize izin verilen, AWS WAF kuralının CAPTCHA eylemiyle korunan bir sayfa. C#’ta AWS WAF CAPTCHA çözmeyi anlatan bu rehber, HTTP 405 yanıtı veren challenge sayfasını ele alır. Python kılavuzu bu sayfayı daha ayrıntılı açıklar ve widget sürümüne bağlantı verir.
  • Çözücü için bir adres. Local modu, aynı bilgisayarda 127.0.0.1 demektir. Server modu, başka bir makinedeki bir programın CapSkip’i API üzerinden çağırmasını sağlar; bunu sondaki bölüm anlatır. İkisi arasındaki geçişi şuradan yaparsınız: bağlantı ayarları.

Nasıl test edildi: benim yazdığım üç sahte (mock) sunucuya karşı, gerçek CapSkip 1.5.0 paketiyle. Birincisi CapSkip API belgeleri içindeki AWS WAF bölümünde yer alan denetimleri uygular; ikincisi korumalı site, üçüncüsü ise token host’udur. Yalnızca test için yazılmış küçük bir handler awswaf.com host’larını bu token host’una yönlendirdi ve aşağıdaki kodda yoktur. CapSkip uygulaması bilgisayarımda çalışıyordu ve ona yalnızca geçersiz istekler gönderdim; bu yüzden hiçbir bulmaca başlatmadım ve gerçek bir çözüm görmedim. Hiçbir canlı AWS WAF sitesi işin içinde değildi. Bu yazıda çözüm süresi yoktur ve belirli bir canlı sitenin bu akışın ürettiği çerezi kabul edeceğine söz veremem.

1. Adım: projeyi oluşturun ve 405 sayfasını okuyun

Bir konsol projesi oluşturun ve paketi ekleyin. Varsayılan şablon örtük using ifadelerini ve nullable türleri açar ve kod ikisine de dayanır.

dotnet new console -o aws-waf-demo
cd aws-waf-demo
dotnet add package CapSkip

Korumalı bir sayfa, HTTP 405 ve captcha olarak ayarlanmış x-amzn-waf-action başlığıyla yanıt verir. Gövde window.gokuProps tanımlar ve challenge.js ile captcha.js dosyalarını bir awswaf.com host’undan yükler. Kodun ondan beş değere ihtiyacı vardır: gokuProps.key, iv ve context ile iki script’in adresleri. iv ve context her yanıtta yenidir, bu yüzden eski bir çifti asla yeniden kullanmayın.

Program.cs içeriğini 1. ile 4. Adımların C# bloklarıyla değiştirin, art arda, bununla başlayarak. 2. Adım’daki kısa kod parçası bunlardan biri değildir. Page.Read, nesne bir JSON dosyasında değil bir script etiketinin içinde durduğu için iki düzenli ifadeyle (regex) eşleştirir. Yanıt ayrıştırılabilir bir CAPTCHA sayfası değilse null döndürür. Bu; düz bir 200 yanıtını, gövdesi boş bir 405 yanıtını ve beş değerden birini içermeyen bir 405 sayfasını kapsar.

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;
    }
}

İkinci blok HttpClient tarafıdır. AWS, istek text/html değerli bir Accept başlığı taşıdığında bulmaca sayfasının yanıta dahil edildiğini belgeler (bkz. AWS WAF belgeleri). HttpClient siz eklemedikçe Accept başlığı göndermez, bu yüzden Web.Get onu ayarlar. Bu başlık olmadan mock’um boş bir 405 gönderir. Gerçek AWS’nin ne gönderdiğini test etmedim.

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());
    }
}

2. Adım: AmazonWafAsync ile çözün

İmza AmazonWafAsync(sitekey, url, options, cancellationToken) biçimindedir. Yalnızca sitekey ve url zorunludur ve seçeneksiz bir çağrı mock çalıştırmasında çalıştı. API belgeleri, atladığınız her değerin çözüm anında sayfadan okunduğunu söyler. Bu kod yine de beşini de gönderir, çünkü sayfayı az önce çekti ve 3. Adım challenge.js adresine her durumda ihtiyaç duyar. iv ve context değerleri birlikte gider. Birini diğeri olmadan göndermek, herhangi bir istekten önce ValidationException fırlatır. https olmayan, awswaf.com host’unda bulunmayan ya da challenge.js, captcha.js veya jsapi.js ile bitmeyen bir script adresi için de aynısı olur. Başka bir host, bir http adresi ve yanlış bir dosya adı denedim ve üçü de reddedildi.

Sonuç bir SolveResult nesnesidir. CaptchaVoucher ve ExistingToken çifti tutar, Code ham yanıtı bir JSON dizesi olarak tutar, CaptchaId ise görev numarasını tutar. Çağrı, 300 saniyelik recaptchaTimeout varsayılanına kadar bekler, çünkü CapSkip bulmacayı yanıtlamadan önce bir token üretmek için gerçek bir tarayıcı açar. Bir çözümü zamanlamadım.

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);
}

Çözümü bir proxy üzerinden yönlendirmek için options sözlüğüne bir proxy girdisi ekleyin. Kendi isteklerinizin de aynı IP adresinden çıkması gerekir, bu yüzden handler’ınıza aynı proxy’yi verin. WebProxy bir user:pass@ dizesini okumaz, bu yüzden giriş bilgisi Credentials içine gider. CapSkip’in kendi kodu da onu aynı şekilde ayıklar. Aşağıdaki ilk satır mock çalıştırmasında in.php’ye bir proxy ve bir proxytype olarak ulaştı. İkincisi Web.Client içindeki handler’ı değiştirir. Derlendi, ama bir proxy üzerinden istek göndermedim; bu yüzden denenmemiş sayın.

// 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") } };

3. Adım: voucher’ı HttpClient ile takas edin

Bu akışta voucher takasını CapSkip değil sizin kodunuz yapar, bu yüzden AWS’nin o aşamada gördüğü IP adresi HttpClient’ınızın adresidir. Voucher endpoint’i challenge.js’in yanında durur: aynı klasör, challenge.js yerine voucher. Web.Exchange çifti JSON olarak POST eder ve yanıtın token alanını okur. API belgeleri örneği bir text/plain;charset=UTF-8 içerik türü ve bir Origin başlığı gönderir. CapSkip uygulamasının kendi takas kodunu okudum; o ayrıca bir Referer ve token’ı üreten tarayıcının User-Agent’ını ekler, null ya da geçersiz bir existing_token için HTTP 400 yanıtını da başarısız takas sayar. Web.Exchange dört başlığı da gönderir. in.php yanıtı o User-Agent’ı döndürmez, bu yüzden bu kod genel bir Chrome User-Agent gönderir ve AWS’nin farkı önemseyip önemsemediğini test edemedim. SDK belgeleri çifti gelir gelmez takas etmenizi söyler. Bir ömür vermez ve ben de bir tane ölçmedim.

İçerik türü C#’ta dikkat ister. text/plain ve Encoding.UTF8 ile StringContent bunu text/plain; charset=utf-8 olarak yazar; noktalı virgülden sonra bir boşluk vardır ve utf-8 küçük harflidir, bunu ölçtüm. Bu yüzden metot bir ByteArrayContent oluşturur ve başlığı elle ayarlar. Mock, belgelenmiş yazımın aynısını kaydetti.

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
    }
}

4. Adım: tam çalışan örnek

Önce PageUrl değerini kendi korumalı sayfanızla değiştirin. Yapıştırıldığı hâliyle example.com’u ister ve orada CAPTCHA kuralı yoktur, bu yüzden program çözülecek bir şey bulamaz. Sonra dotnet run ile çalıştırın. Her deneme sayfayı yeniden çeker, bu da her denemeye kendi iv ve context çiftini verir. ApiException iletisi hata kodunu içerir, bu yüzden catch filtresi yalnızca ERROR_CAPTCHA_UNSOLVABLE için yeniden dener. Başka herhangi bir kod, yeniden denemenin aynı şekilde başarısız olacağı anlamına gelir; bu yüzden yakalanmadan çağırana ulaşır. CapSkip’te yerine geçen hata metnini değiştirdiyseniz filtre eşleşmez. Token, PageUrl host’u için çerez deposuna girer ve son istek sayfayı yeniden okur. C#’ta AWS WAF CAPTCHA döngüsünün tamamı budur.

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;
    }
}

Mock çalıştırmasında program şöyle davrandı. Her satır hem net10.0 hem net8.0 üzerinde aynı çıktıyla çalıştı.

Mock ayarıProgram ne yaptı
Mock her bulmacayı yanıtlıyorTek bir görev ve tek bir takas, ardından 200 ve gerçek sayfa
Mock ilk iki bulmacayı reddediyorİki deneme satırı, ardından üçüncü görevden sonra 200
Mock ilk üç bulmacayı reddediyorÜç deneme satırı, CAPTCHA’nın çözülmediğini bildiren ileti, çıkış kodu 1 ve voucher isteği yok
Web.Get içinden Accept satırı kaldırıldıMock boş bir 405 gönderdi, Page.Read null döndürdü ve program 405 yazdırıp 1 koduyla çıktı
Mock ERROR_BAD_PARAMETERS yanıtı veriyorApiException ilk denemede yakalanmamış olarak ortaya çıktı, yeniden deneme olmadı
Takastan önce değiştirilmiş voucher çiftiMock 400 yanıtı verdi ve program yakalanmamış bir HttpRequestException ile bitti

Çerez, sitenin CAPTCHA bağışıklık süresi boyunca çalışır; bu, AWS WAF’ın bulmacayı çözmüş bir istemcinin bulmacayı atlamasına izin verdiği dönemdir. AWS WAF bağışıklık süresi belgeleri varsayılan olarak 300 saniye verir ve site sahibinin bunu 60 saniyeden üç güne kadar ayarlamasına izin verir. Tüm çalıştırma boyunca tek bir HttpClient ve onun çerez deposunu tutun, 405 yeniden gelene kadar token’ı yeniden kullanın.

Yaygın hatalar

GördüğünüzNedenDüzeltme
CapSkip.ValidationExceptionSDK tarafından herhangi bir istekten önce fırlatılır: context olmadan gönderilen iv, https ve awswaf.com dışındaki bir script adresi, http ya da https olmayan bir url veya boş bir sitekey. İleti hangisinin sorun olduğunu söylerİletinin adını verdiği argümanı düzeltin
ApiException: ERROR_CAPTCHA_UNSOLVABLECapSkip o bulmacayı yanıtlayamadı. API belgeleri her yeni denemenin farklı bir bulmaca aldığını söylerDöngü onu yeni bir sayfayla yeniden dener
Başka bir kodla ApiExceptionERROR_BAD_PARAMETERS, ERROR_PROXY_FORMAT, ERROR_TASK_NOT_SUPPORTED (API belgeleri, görev türünün desteklenmediğini ya da ayarının CapSkip’te kapalı olduğunu söyler) veya ERROR_NO_SLOT_AVAILABLE (CapSkip’te ayarlanan istek hızı sınırını aşan bir anahtar; bu yalnızca sınırı olan anahtarlar için geçerlidir)Yeniden denenmeden çağırana ulaşır. İletiyi okuyun. Hız sınırı için birkaç saniye bekleyin ve yeniden çalıştırın
CapSkip.TimeoutExceptionBekleme süresi içinde yanıt yok. ApiException değil CapSkipError sınıfından türer, bu yüzden yeniden deneme filtresi onu yakalamazCapSkip’in çalıştığını ve çözdüğünü kontrol edin, sonra yeniden çalıştırın
CapSkip.NetworkExceptionCapSkip çalışmıyor, host veya port yanlış ya da iptal token’ınız tetiklendiCapSkip’i başlatın, bağlantı ayarlarını kontrol edin ve iletiyi okuyun
Web.Exchange kaynaklı HttpRequestExceptionEnsureSuccessStatusCode başarısız bir yanıt gördü. Eşleşmeyen ya da değiştirilmiş bir çift mock’ta 400 verdi. Gerçek bir örneği yeniden üretemedim ve çok geç takas edilen bir çift de aynı şekilde başarısız olabilirHemen takas edin ve yeni bir görev gönderin
CS0104 derleyici hatasıÇıplak bir TimeoutException, System ile CapSkip arasında belirsizdirCapSkip.TimeoutException yazın
Çerezle bile 405Token’ın süresi doldu, çerez başka bir host için eklendi ya da site token’ı bir başlıktan okuyorYeniden çözün, sonra sayfanın kendi isteklerinin ne gönderdiğiyle karşılaştırın

CapSkip’i bir sunucuda çalıştırmak

CapSkipClient bir host ve bir port alır; API Key Validation’ı açtıysanız bir de apiKey alır. Bu yüzden çözücüyü taşımak tek satırlık bir değişikliktir: 127.0.0.1 yerine diğer makinenin adresini verin. Server modu, CapSkip’in ağ adresinizi veya genel bir IP’yi dinlemesini sağlar; bu da başka bir bilgisayardaki, bir VPS’teki veya barındırılan bir platformdaki bir .NET programının aynı API’yi çağırmasına izin verir. CapSkip’i çalıştıran makine bir Windows makinesi, yani bir bilgisayar ya da bir Windows sunucusu olmalıdır ve sizin kalır, çözüm başına ücretlendirilmez. CapSkip, uzaktan erişimin kararlı kalması için statik bir genel IP önerir. Port o zaman her adresten bağlantı kabul eder, bu yüzden girmemesi gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Çözüm o zaman o makinede çalışır, 3. Adım’daki voucher takası ise programınızın çalıştığı yerde çalışır. AWS’nin bu farkı önemseyip önemsemediğini test etmedim. Önemsiyorsa aynı proxy’yi iki aşamaya da verin.

FAQ

catch (TimeoutException) neden CapSkip paketiyle derlenmiyor?

System de CapSkip de bir TimeoutException tanımlar. Örtük using’ler açıkken ve dosyanın başında using CapSkip varken kısa ad belirsizdir ve derleyici CS0104 hatasıyla durur. CapSkip.TimeoutException yazın ya da SDK’nın fırlattığı her şeyin taban sınıfı olan CapSkipError’ı yakalayın.

Voucher POST’u için neden StringContent kullanılmıyor?

İçerik türünü yeniden yazar. text/plain ve Encoding.UTF8 ile StringContent, text/plain; charset=utf-8 gönderir; API belgeleri ise text/plain;charset=UTF-8 gösterir. Başlığı elle yazılmış bir ByteArrayContent belgelenmiş yazımı gönderir. Gerçek AWS’nin ikisine farklı davranıp davranmadığını kontrol edemedim, bu yüzden belgelenmiş olanı kopyaladım.

Çerez diğer host’larda ya da alt alan adlarında çalışır mı?

Örnekte eklendiği gibi değil. PageUrl adresi için CookieContainer’a eklenen çerez yalnızca o host’a özgüdür (host-only): yaptığım bir testte www.example.com’a gitti, api.example.com’a ya da example.com’a gitmedi. Siteniz başka bir host’u çağırıyorsa çerezi o host için de ekleyin. API belgeleri ayrıca bazı sayfaların token’ı çerez yerine x-aws-waf-token istek başlığında ilettiğini söyler; bu yüzden sayfanın kendi isteklerinin ne gönderdiğini kontrol edin.

Çok uzun süren bir çözümü nasıl iptal ederim?

AmazonWafAsync’e, örnekte on dakikalık bir kaynakla (source) yapıldığı gibi, bir CancellationToken verin. Zaten iptal edilmiş bir token, hiçbir şey gönderilmeden önce çağrıyı durdurur, ama OperationCanceledException olarak değil, A task was canceled iletisiyle CapSkip.NetworkException olarak çağırana ulaşır. Aynı tür CapSkip’in çalışmadığını da bildirir, bu yüzden iletiyi okuyun.

C#’ta bir AWS WAF CAPTCHA’sını yeniden denemek hiçbir şeye mal olmaz, çünkü çözücü kendi bilgisayarınızda çalışır; bu yüzden yerel bir captcha atlatma çözümü, her denemede size yeni bir bulmaca veren sitelere uygundur. Aynı akışın Python sürümü şurada: Python kılavuzu. SDK’nın geri kalanı için şuraya bakın: C# CAPTCHA çözücü sayfası.