Captcha-Detektor in C#: Typ finden, CapSkip aufrufen

captcha detector in c# - CAPTCHA Detector in C#: Find the Type, Call CapSkip

Ein Captcha-Detektor in C# liest das HTML einer Seite nach dem Skript und dem Container des Widgets, zieht den sitekey heraus und ordnet die Familie der CapSkip-Methode zu, die sie löst. Ein reCAPTCHA-v2-Kasten, ein v3-Skript und ein Turnstile-Widget brauchen drei verschiedene Aufrufe mit unterschiedlichen Optionen, und ein falscher Tipp verschwendet einen ganzen Lösevorgang und gibt Ihnen einen Token, den Sie nicht verwenden können. Dieser Leitfaden baut einen Detektor von etwa 200 Zeilen ohne weitere Pakete außer CapSkip. Er gibt den Typ, die Methode und die Argumente zurück oder sagt offen, dass er es nicht erkennen kann. CapSkip läuft auf Ihrem eigenen Windows-PC ohne Gebühr pro Lösung, daher kostet ein Programm, das hundert Seiten prüft, nichts extra. Denselben Detektor gibt es in Python und Node.js, am Ende verlinkt.

Was Sie brauchen

  • CapSkip läuft auf einem Windows-PC, mit den Lösern, die Sie brauchen, eingeschaltet. Die Einrichtungsanleitung führt durch den ersten Start.
  • Das .NET 8 SDK oder neuer, weil der Code Raw-String-Literale verwendet, die C# 11 brauchen. Getestet wurde mit dem SDK 10.0.100 und dem Paket CapSkip 1.3.0, mit den Zielen net8.0 und net10.0. Auch das Ziel net6.0 funktionierte, nachdem LangVersion 11 in die Projektdatei eingetragen war. .NET Framework wurde nicht ausprobiert.
  • Ein Terminal. Die Befehle funktionieren in PowerShell.
  • Eine Seite, die Ihnen gehört oder die Sie automatisieren dürfen, mit einem Captcha darauf.
  • Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Programm auf einem anderen PC, einem VPS oder einer gehosteten Plattform es über die API aufrufen kann. Eingestellt werden beide Modi in den Verbindungseinstellungen.

So wurde getestet: Der Detektor lief gegen dieselben 66 gespeicherten Seiten wie der Python-Detektor und lieferte für jede Seite dasselbe Ergebnis. Davon wurden 59 von Hand gebaut (etwa 40 folgen der Dokumentation der Anbieter, der Rest sind Randfälle, die ich mir ausgedacht habe) und 7 sind Schnappschüsse öffentlicher Seiten vom 29. Oktober 2026. Die 40 Ergebnisse, die eine Methode nennen, wurden über das echte NuGet-Paket von CapSkip an einen Mock-CapSkip (einen nachgebauten Testersatz) gesendet, der die Eingabeprüfungen der App nachbildet. Die echte App lief nicht, daher ist keine Zahl unten eine Lösezeit.

Warum Sie den Captcha-Typ erkennen, bevor Sie lösen

CapSkip hat für jede Familie eine eigene Methode: RecaptchaAsync, TurnstileAsync, GeetestAsync, AltchaAsync, CapyAsync, CaptchaFoxAsync, FriendlyCaptchaAsync und NormalAsync für Bilder im SDK, dazu hcaptcha über die HTTP API, die das SDK noch nicht abdeckt. Auch innerhalb von reCAPTCHA kommt es auf die Optionen an. Invisible, Enterprise und v3 sind Flags am selben Aufruf. Wenn Sie die falschen senden, ist der beste Fall ein fehlgeschlagener Job, der schlimmste ein Token, den die Website ohne jeden Hinweis ablehnt. Ich konnte nicht prüfen, was die echte App bei einer falschen reCAPTCHA-Version tut. In beiden Fällen ist die Zeit eines Lösevorgangs verschwendet.

Andere Fehler schlagen schnell und laut fehl. Wenn Sie den sitekey von einer Seite lesen, die reCAPTCHA explizit rendert, erhalten Sie das Wort explicit, und CapSkip lehnt es mit ERROR_GOOGLEKEY ab. Die Anleitung zu ERROR_GOOGLEKEY und ERROR_PAGEURL behandelt diesen Fehler von der anderen Seite. Um den Captcha-Typ zuverlässig zu erkennen, muss ein Detektor die Signale in einer festen Reihenfolge prüfen, und diese Reihenfolge vermeidet beide Arten von Fehlern. Ein Detektor ist außerdem das Erste, was man vor einem Programm baut, das auf mehr als eine Website trifft. Wenn Sie den Hintergrund zu den einzelnen Typen möchten: Der Beitrag Was ein Captcha ist erklärt die Familien ohne Code.

Schritt 1: Das Projekt anlegen

Legen Sie ein Konsolenprojekt an und fügen Sie das Paket CapSkip hinzu. Der Detektor selbst verwendet nur, was .NET mitbringt: reguläre Ausdrücke (Regex) und HttpClient.

dotnet new console -o detect-demo
cd detect-demo
dotnet add package CapSkip

Ein Captcha-Detektor in C# arbeitet mit Text und braucht daher nur zwei Eingaben: das HTML der Seite und ihre Adresse. Eine einfache Anfrage zeigt, was der Server sendet, bevor JavaScript läuft. Die reCAPTCHA-Demoseiten von Google, die Turnstile-Demo von Cloudflare und die hCaptcha-Demo tragen ihr Widget alle in diesem HTML. Seiten, die das Widget per JavaScript hinzufügen, tun das nicht, und der Abschnitt “Wenn das HTML nichts hergibt” behandelt diesen Fall. Ein HttpClient hat einen Cookie-Speicher, was für den Schritt mit dem Bild-Captcha wichtig ist: Das Programm unten verwendet denselben Client für die Seite und für das Bild.

Schritt 2: Die Hilfsfunktionen schreiben

Speichern Sie alles von hier bis Schritt 9 in einer Datei namens Detector.cs, Block für Block hintereinander. Die Datei lässt sich erst kompilieren, wenn Schritt 9 die Klasse schließt, und sie stützt sich auf die impliziten usings, die dotnet new console einschaltet. Der erste Block enthält die usings, zwei kleine Records und die ersten Hilfsfunktionen. Jeder Leser gibt null zurück, wenn nichts passt, sodass ein fehlendes Attribut nie eine Exception auslöst.

using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using CapSkip;

public record Info(string Type, string? Method, Dictionary<string, object?> Args,
    string? NeedsBrowser = null, bool Unsupported = false, string[]? Also = null);
record Page(string H, string Srcs, string Js, string Url);

public static class Detector
{
    const RegexOptions F = RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant;
    const string SiteKeyJs = """sitekey['"]?\s*:\s*['"]([^'"]+)""";

    static string? Grab(string pattern, string? text)  // first capture group that matched, or null
    {
        var m = Regex.Match(text ?? "", pattern, F);
        for (var i = 1; i < m.Groups.Count; i++) if (m.Groups[i].Success) return m.Groups[i].Value;
        return null;
    }
    static string? Or(params string?[] v) => v.FirstOrDefault(s => !string.IsNullOrEmpty(s));
    static string Abs(string url, string rel) => Uri.TryCreate(new Uri(url), rel, out var u) ? u.AbsoluteUri : rel;
    static Dictionary<string, object?> Args(params (string, object?)[] kv) =>  // null values are left out
        kv.Where(x => x.Item2 != null).ToDictionary(x => x.Item1, x => x.Item2);

Die Methode Grab gibt die erste Capture-Gruppe zurück, die gepasst hat. Die Methode Tag findet ein öffnendes Tag über die CSS-Klasse und weist eine Klasse zurück, die den Namen nur innerhalb einer längeren enthält, sodass eine Klasse namens cf-turnstile-response kein Turnstile-Widget ist. Die Methode Attr liest ein Attribut, das mit beiden Anführungszeichen-Arten geschrieben sein kann, und dekodiert HTML-Entities, was bei einer Challenge in einem Attribut wichtig ist. Attribute ohne Anführungszeichen werden nicht gelesen.

Der zweite Block macht aus einer Seite etwas, das die Prüfungen gemeinsam nutzen können. Er entfernt Kommentare und Style-Blöcke, sodass eine CSS-Regel, die eine Widget-Klasse erwähnt, nicht passen kann, und er sammelt die Skript-Adressen und das Inline-JavaScript getrennt. Die letzte Hilfsfunktion baut die Antwort für Seiten, bei denen das HTML nicht reicht. Ihr Feld NeedsBrowser enthält den Grund, zum Beispiel dass der sitekey per JavaScript gesetzt wird, und dieser Beitrag nennt das ein Browser-nötig-Ergebnis.

    static string? Attr(string? tag, string name) =>  // attribute value, either quote style, HTML-decoded
        Grab("""\s""" + name + """\s*=\s*(?:"([^"]*)"|'([^']*)')""", tag) is { } v ? WebUtility.HtmlDecode(v) : null;
    static string? Tag(string cls, string h) =>  // opening tag that has CSS class cls
        Grab("""(<\w+[^>]*\sclass\s*=\s*["'][^"']*(?<![\w-])""" + cls + """(?![\w-])[^"']*["'][^>]*>)""", h);
    static string? Opts(string call, string js) => Grab(call + """\([^,]+,\s*\{(.{0,400})""", js);

    static Page Parse(string html, string url)
    {
        var h = Regex.Replace(html, """<!--.*?-->|<style\b.*?</style>""", "", F);
        var srcs = Regex.Matches(h, """<script\b[^>]*?\ssrc\s*=\s*["']([^"']+)""", RegexOptions.IgnoreCase);
        var js = Regex.Matches(h, """<script\b[^>]*>(.*?)</script>""", F);
        return new(h, WebUtility.HtmlDecode(string.Join(" ", srcs.Select(m => m.Groups[1].Value))),
                   string.Join("\n", js.Select(m => m.Groups[1].Value)), url);
    }
    static Info Browser(string type, string method, Page p, string why) =>  // HTML is not enough: do not guess
        new(type, method, new() { ["url"] = p.Url }, why);
    static readonly (string, string)[] Vendors = { ("arkose", """arkoselabs\.com/v2/|funcaptcha\.com/fc/"""),
        ("geetest_v4", """gt4\.js|initGeetest4""") };
    static Info? Unsup(Page p) => Vendors.Where(v => Regex.IsMatch(p.Srcs + " " + p.Js, v.Item2))  // CapSkip cannot solve these
        .Select(v => new Info(v.Item1, null, new(), Unsupported: true)).FirstOrDefault();
    static Info? Cf(Page p) => Regex.IsMatch(p.Js, """_cf_chl_opt\s*=\s*\{""")  // the "Just a moment..." page, not a widget
        ? Browser("turnstile_challenge", "turnstile", p, "data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time") : null;
    static Info? Hcaptcha(Page p)  // before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
    {
        if (!Regex.IsMatch(p.Srcs + " " + p.Js, """hcaptcha\.com/1/api\.js""")) return null;
        if (Regex.IsMatch(p.Js, """\.setData\s*\(|rqdata"""))  // Enterprise rqdata is issued per challenge
            return Browser("hcaptcha_enterprise", "hcaptcha", p, "rqdata is issued for one challenge at run time");
        var t = Tag("h-captcha", p.H) ?? Tag("g-recaptcha", p.H);
        var key = Or(Attr(t, "data-sitekey"), Grab(SiteKeyJs, Opts("""hcaptcha\.render""", p.Js)));
        if (key == null) return Browser("hcaptcha", "hcaptcha", p, "no sitekey in the static HTML");
        var invisible = Attr(t, "data-size") == "invisible" ? 1 : (int?)null;
        return new("hcaptcha", "hcaptcha", Args(("sitekey", key), ("url", p.Url), ("invisible", invisible)));
    }

Schritt 3: hCaptcha und was CapSkip nicht lösen kann

Diese drei Prüfungen stehen am Ende des zweiten Blocks oben, in diesem Schritt gibt es also nichts Neues einzufügen. Beginnen Sie mit den Seiten, bei denen die richtige Antwort Nein lautet. CapSkip löst weder FunCaptcha noch Arkose, und GeeTest v4 liegt außerhalb der GeeTest-Methode, die für v3 gedacht ist. Würde der Detektor diese überspringen, würde eine Seite mit einem Arkose-Kasten zur nächsten Prüfung durchfallen und könnte als etwas zurückkommen, das sie nicht ist. Deshalb läuft diese Prüfung zuerst und gibt ein Ergebnis ohne Methode zurück.

Derselbe Block behandelt die Vollseiten-Prüfung von Cloudflare, die Zwischenseite “Just a moment”. Diese Seite ist kein Turnstile-Widget. Die Methode turnstile braucht von ihr zwei Werte, data und pagedata, und eine einfache Anfrage enthält sie nicht. Der Detektor sucht nach den Inline-Challenge-Optionen der Seite und antwortet, dass ein Browser nötig ist. Die cf_clearance-Anleitung behandelt, was folgt, nachdem eine Challenge-Seite bestanden ist.

Als Nächstes kommt hCaptcha, noch vor reCAPTCHA. CapSkip löst es, ob das Widget eine Checkbox, unsichtbar oder Enterprise ist, aber eine Website, die von reCAPTCHA zu hCaptcha gewechselt ist, behält an ihrem Widget oft die Klasse g-recaptcha, und nur das hCaptcha-Skript unterscheidet die beiden. Die Prüfung liest den sitekey aus dem Widget oder aus einem Aufruf von hcaptcha.render und fügt invisible hinzu, wenn das Widget data-size auf invisible gesetzt hat. Eine Enterprise-Seite, die dem Widget über hcaptcha.setData einen rqdata-Wert übergibt, bekommt stattdessen ein Ergebnis, das einen Browser braucht, weil dieser Wert für eine einzige Challenge ausgestellt wird und schnell abläuft.

Schritt 4: reCAPTCHA, in der richtigen Reihenfolge

reCAPTCHA braucht die meiste Sorgfalt, weil mehrere CapSkip-Varianten zwei Skriptnamen teilen. Die Reihenfolge der Fragen hält sie auseinander. Zuerst der Skriptname: enterprise.js bedeutet das Enterprise-Flag, api.js nicht. Dann der Parameter render in der Skript-Adresse. Ein Schlüssel nach render= bedeutet v3, aber das Wort explicit nach render= bedeutet eine v2-Seite, die ihr Widget aus Code rendert, und es darf nie als Schlüssel verwendet werden. Dann das Markup. Ein Button mit der Klasse g-recaptcha und einem Attribut data-action ist die v3-Button-Bindung von Google. Ein Button ohne data-action oder ein beliebiges Element mit data-size auf invisible ist das unsichtbare v2-Widget. Alles andere mit dieser Klasse ist die Checkbox.

    static Info? Recaptcha(Page p)  // Enterprise flag, then v3 (render=KEY), then v2
    {
        var m = Regex.Match(p.Srcs, """/recaptcha/(api|enterprise)\.js(\?\S*)?""");
        if (!m.Success) return null;
        var render = Grab("""[?&]render=([^&\s]+)""", m.Groups[2].Value);  // a v3 key, or "explicit" (a v2 page)
        var t = Tag("g-recaptcha", p.H);
        var o = Opts("""\.render""", p.Js);
        var button = (t ?? "").StartsWith("<button", StringComparison.OrdinalIgnoreCase);
        var action = Attr(t, "data-action");
        var v3 = (render != null && render != "explicit") || (button && !string.IsNullOrEmpty(action));
        var key = Or(render == "explicit" ? null : render, Attr(t, "data-sitekey"), Grab(SiteKeyJs, o));
        if (key == null) return Browser("recaptcha", "recaptcha", p, "no sitekey in the static HTML (set by JavaScript)");
        var small = Attr(t, "data-size") == "invisible" || Grab("""size['"]?\s*:\s*['"](invisible)""", o) != null;
        var invisible = !v3 && (button || small);  // a <button> is invisible v2 unless it has data-action (v3)
        action = Or(action, Grab("""execute\(\s*[^,)]+,\s*\{\s*['"]?action['"]?\s*:\s*['"]([^'"]+)""", p.Js));
        var ent = m.Groups[1].Value == "enterprise";
        var a = Args(("sitekey", key), ("url", p.Url), ("enterprise", ent ? 1 : null), ("version", v3 ? "v3" : null),
                     ("action", v3 ? action : null), ("invisible", invisible ? 1 : null));
        return new("recaptcha_" + (v3 ? "v3" : invisible ? "v2_invisible" : "v2") + (ent ? "_enterprise" : ""), "recaptcha", a);
    }

Jedes Ergebnis ist einem Aufruf von RecaptchaAsync zugeordnet. Eine Checkbox ist RecaptchaAsync mit einem sitekey, der Seiten-URL und einem optionalen Options-Dictionary. Invisible fügt "invisible" hinzu, Enterprise fügt "enterprise" hinzu, und v3 fügt "version" und die Action hinzu. Der Name der Action stammt aus dem Attribut data-action oder aus dem execute-Aufruf der Seite; steht keins von beiden im HTML, wird der Eintrag weggelassen, und CapSkip verwendet seinen Standard verify. Siehe die reCAPTCHA-Solver-Seite für jede Variante, die sie abdeckt.

Schritt 5: Turnstile und GeeTest

Ein Turnstile-Widget braucht auf der Seite zwei Dinge: die api.js von Cloudflare und einen sitekey. Der Schlüssel steht in einem Attribut data-sitekey an einem div mit der Klasse cf-turnstile oder in einem Aufruf turnstile.render. Die Prüfung verlangt das Skript, sodass ein verirrter Klassenname auf einer fremden Seite sie nicht auslösen kann. Sie liest außerdem data-action und data-cdata, wenn sie vorhanden sind, und reicht sie als action und data weiter. Die Turnstile-Solver-Seite listet die Optionen auf.

GeeTest v3 liefert zwei Werte, und einer davon ist eine Falle. Der Wert gt ist für die Website fest und kann in der Seite stehen. Der Wert challenge ist einmalig, und Websites holen ihn zur Laufzeit von einem Register-Endpunkt. Trägt die Seite beide, gibt der Detektor sie zurück. Trägt sie nur den Aufruf initGeetest, antwortet er, dass das HTML nicht reicht, denn ein challenge aus einer gespeicherten Seite wäre schon tot.

    static Info? Turnstile(Page p)  // widget only: div.cf-turnstile or turnstile.render
    {
        if (!p.Srcs.Contains("challenges.cloudflare.com/turnstile/v0/api.js")) return null;
        var t = Tag("cf-turnstile", p.H);
        var o = Opts("""turnstile\.render""", p.Js);
        var key = Or(Attr(t, "data-sitekey"), Grab(SiteKeyJs, o));
        if (key == null) return Browser("turnstile", "turnstile", p, "no sitekey in the static HTML");
        var action = Or(Attr(t, "data-action"), Grab("""\baction['"]?\s*:\s*['"]([^'"]+)""", o));
        var cdata = Or(Attr(t, "data-cdata"), Grab("""\bcData['"]?\s*:\s*['"]([^'"]+)""", o));
        return new("turnstile", "turnstile", Args(("sitekey", key), ("url", p.Url), ("action", action), ("data", cdata)));
    }
    static Info? Geetest(Page p)  // v3 only: gt is static, challenge comes from the register endpoint
    {
        if (!Regex.IsMatch(p.Js, """initGeetest\s*\(""") && !Regex.IsMatch(p.Srcs, """/gt(\.[\d.]+)?\.js""")) return null;
        var gt = Grab("""\bgt['"]?\s*:\s*['"]([0-9a-f]{32})""", p.Js);
        var ch = Grab("""\bchallenge['"]?\s*:\s*['"]([0-9a-f]{32,34})""", p.Js);
        return gt == null || ch == null ? Browser("geetest", "geetest", p, "gt and challenge come from the register endpoint")
            : new("geetest", "geetest", Args(("gt", gt), ("challenge", ch), ("url", p.Url)));
    }

Schritt 6: ALTCHA und Capy

ALTCHA hat seine Attributnamen zwischen den Generationen geändert. Die Versionen 1 und 2 verwenden challengeurl oder challengejson für eine eingebettete Challenge. Version 3 und später verwenden ein einziges Attribut namens challenge, das entweder eine URL oder die Challenge-Daten annimmt. Die Prüfung liest alle drei und verbindet eine relative URL mit der Seitenadresse. In C# lauten die Options-Schlüssel challenge_url und challenge_json.

Capy braucht für diese Prüfung kein Widget-div. Sein Schlüssel beginnt mit PUZZLE_ und steht im Parameter k der Adresse des Widget-Skripts, und ein anderer Host als https://jp.api.capy.me wird als api_server übergeben.

    static Info? Altcha(Page p)  // challengeurl / challengejson (v1, v2) or challenge (v3+)
    {
        var t = Grab("""(<altcha-widget\b[^>]*>)""", p.H);
        if (t == null) return null;
        var c = Attr(t, "challenge");
        var json = Or(Attr(t, "challengejson"), (c ?? "").TrimStart().StartsWith('{') ? c : null);
        var url = Or(Attr(t, "challengeurl"), json != null ? null : c);
        return json != null ? new("altcha", "altcha", Args(("url", p.Url), ("challenge_json", json)))
            : url != null ? new("altcha", "altcha", Args(("url", p.Url), ("challenge_url", Abs(p.Url, url))))
            : Browser("altcha", "altcha", p, "no challenge attribute");
    }
    static Info? Capy(Page p)  // the widget script is .../puzzle/get_js/?k=PUZZLE_...
    {
        var m = Regex.Match(p.Srcs, """((?:https?:)?//[^\s"']+?)/puzzle/get_js/\S*?(PUZZLE_[\w-]+)""");
        if (!m.Success) return null;
        var full = m.Groups[1].Value.StartsWith("http") ? m.Groups[1].Value : "https:" + m.Groups[1].Value;
        var host = full == "https://jp.api.capy.me" ? null : full;
        return new("capy", "capy", Args(("sitekey", m.Groups[2].Value), ("url", p.Url), ("api_server", host)));
    }

Schritt 7: CaptchaFox und Friendly Captcha

Das Widget von CaptchaFox erreicht Seiten auf zwei Wegen. Das einfache lädt von cdn.captchafox.com. Das andere wird von einer Hosting-Plattform unter einem gemeinsamen CDN-Pfad gebündelt, daher sucht der Detektor in diesem Pfad nach dem Paketnamen von CaptchaFox und nicht nur nach dem Pfad allein, den auch andere Widgets verwenden.

Friendly Captcha ist das Kniffligste. Version 1 und Version 2 verwenden dasselbe div und denselben Namensraum für den sitekey, aber unterschiedliche Protokolle, und CapSkip gibt für das falsche einen wohlgeformten Token zurück, den die Website dann ablehnt. Die Skripte unterscheiden sie: Version 2 lädt das Paket @friendlycaptcha/sdk, Version 1 lädt friendly-challenge. Gibt die Seite keinen Hinweis, weigert sich der Detektor zu raten und sagt, dass das HTML nicht reicht.

    const string Mam = "mampkg/@mamdev/core.frontend.libs.captchafox";
    static Info? CaptchaFox(Page p)  // plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
    {
        var t = Tag("captchafox", p.H);
        var key = Grab("""captchafox[^"']*/api\.js\?key=([^"'&]+)""", p.Srcs);
        if (t == null && key == null) return null;
        var mam = p.Srcs.Contains(Mam) ? "https://s.uicdn.com/" + Mam + "/" : null;
        var sitekey = Or(Attr(t, "data-sitekey"), key);
        if (sitekey == null) return Browser("captchafox", "captchafox", p, "no sitekey in the static HTML");
        return new("captchafox", "captchafox", Args(("sitekey", sitekey), ("url", p.Url), ("api_server", mam)));
    }
    static Info? Friendly(Page p)  // v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
    {
        var t = Tag("frc-captcha", p.H);
        if (t == null) return null;
        var e2 = Attr(t, "data-api-endpoint");
        var e1 = Attr(t, "data-puzzle-endpoint");
        if (Attr(t, "data-sitekey") == null) return Browser("friendly_captcha", "friendly_captcha", p, "no sitekey in the static HTML");
        var v2 = e2 != null || Regex.IsMatch(p.Srcs, """friendlycaptcha/sdk|site(\.compat)?\.min\.js""");
        var v1 = e1 != null || Regex.IsMatch(p.Srcs, """friendly-challenge|widget(\.module)?\.min\.js""");
        if (v2 == v1) return Browser("friendly_captcha", "friendly_captcha", p, "cannot tell v1 from v2 in the HTML");
        return new(v2 ? "friendly_captcha_v2" : "friendly_captcha_v1", "friendly_captcha", Args(("sitekey", Attr(t, "data-sitekey")),
            ("url", p.Url), ("version", v2 ? "v2" : "v1"), ("api_server", Or(e2, e1))));
    }

Jedes davon hat eine eigene Solver-Seite mit den Optionen, die die Methode annimmt:

Schritt 8: das Bild-Captcha, ein Tipp, und die Prüfungen

Ein Bild-Captcha hat kein Anbieter-Skript, das sich finden ließe, daher ist diese Prüfung ein Tipp und läuft nur, wenn nichts anderes gepasst hat. Die Prüfung sucht ein Bild, dessen Tag captcha enthält, und ein Textfeld an anderer Stelle der Seite, dessen Tag captcha oder security code enthält. Sie hat in jedem Fall danebengelegen, in dem das Markup diese Wörter nicht verwendete, und ein gezeichnetes Canvas ist für sie unsichtbar. Behandeln Sie einen Treffer als wahrscheinlich, nicht als sicher.

Derselbe Block listet die Prüfungen der Reihe nach auf und stellt Detect bereit. Die Reihenfolge ist die Logik: zuerst nicht unterstützte Anbieter, dann die Cloudflare-Seite, dann hCaptcha, dann jede andere Familie, und der Tipp für das Bild nur, wenn die Liste nichts gefunden hat. Die Methode Detect gibt das erste Ergebnis zurück und fügt eine Liste Also hinzu, wenn die Seite mehr als eine Familie trägt, zum Beispiel ein v3-Skript plus ein Turnstile-Kasten. Die Liste nennt Familien, nicht ein zweites reCAPTCHA-Widget mit einem anderen Schlüssel.

    static Info? Image(Page p)  // a guess: an <img> that says captcha, next to a text box that says captcha
    {
        const string box = """<input\b(?![^>]*type\s*=\s*["']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)""";
        if (!Regex.IsMatch(p.H, box, RegexOptions.IgnoreCase)) return null;
        foreach (Match m in Regex.Matches(p.H, """<img\b[^>]*>""", RegexOptions.IgnoreCase))
            if (Regex.IsMatch(m.Value, "captcha", RegexOptions.IgnoreCase) && Attr(m.Value, "src") is { Length: > 0 } src)
                return new("image", "normal", Args(("image", Abs(p.Url, src))));
        return null;
    }
    public static Info? Detect(string html, string pageUrl)  // every check runs; the first hit wins, the rest are "also"
    {
        var p = Parse(html, pageUrl);
        var found = new[] { Unsup(p), Cf(p), Hcaptcha(p), Recaptcha(p), Turnstile(p), Geetest(p), Altcha(p), Capy(p), CaptchaFox(p), Friendly(p) }
            .Where(r => r != null).ToList();
        return found.Count == 0 ? Image(p) : found.Count == 1 ? found[0]
            : found[0]! with { Also = found.Skip(1).Select(r => r!.Type).ToArray() };
    }

Schritt 9: Lösen

Der letzte Block leitet das Ergebnis an die richtige CapSkip-Methode weiter. Er lehnt alles ab, was unsupported oder als Browser nötig markiert ist, mit einer InvalidOperationException. Die CapSkip-Methoden nehmen ihre Hauptwerte per Position und den Rest als Options-Dictionary, daher zieht der Block die Positionswerte aus den Argumenten heraus und reicht alles andere unter den rohen API-Namen weiter. Ein Bild wird mit Ihrem eigenen HttpClient heruntergeladen, damit sein Cookie-Speicher mit der Anfrage mitgeht, denn der eigene Download einer URL durch das SDK sendet keine Cookies. Ein schlechter Status bei diesem Download löst eine HttpRequestException aus.

Das SDK hat noch keinen hCaptcha-Aufruf, daher geht ein hCaptcha-Ergebnis stattdessen an SolveHcaptcha. Die Methode sendet den sitekey und die Seitenadresse mit method=hcaptcha an in.php und fragt dann alle 5 Sekunden res.php ab, bis der Token bereit ist, wie der hCaptcha-Abschnitt der API-Dokumentation beschreibt. Sie liest die Adresse und den API key aus dem CapSkipClient, den Sie übergeben, es gibt also nichts zusätzlich zu konfigurieren. Ein abgelehnter Job wirft die eigene ApiException des SDK mit dem Fehlercode, sodass der catch-Block für CapSkipError im Programm unten ihn behandelt. Das Ergebnis trägt den Token in Code und den User-Agent, mit dem er erzielt wurde, in UserAgent.

    public static async Task<SolveResult> Solve(Info i, CapSkipClient s, HttpClient http)
    {
        if (i.Unsupported || i.NeedsBrowser != null) throw new InvalidOperationException(i.NeedsBrowser ?? i.Type + " is not supported by CapSkip");
        if (i.Method == "hcaptcha") return await SolveHcaptcha(i.Args, s, http);
        string K(string k) => (string)i.Args[k]!;  // the positional arguments
        var o = i.Args.Where(kv => kv.Key is not ("sitekey" or "url" or "gt" or "challenge" or "image"))
                      .ToDictionary(kv => kv.Key, kv => kv.Value);  // everything else is an options entry
        return i.Method switch
        {
            "recaptcha" => await s.RecaptchaAsync(K("sitekey"), K("url"), o),
            "turnstile" => await s.TurnstileAsync(K("sitekey"), K("url"), o),
            "geetest" => await s.GeetestAsync(K("gt"), K("challenge"), K("url"), o),
            "altcha" => await s.AltchaAsync(K("url"), o),
            "capy" => await s.CapyAsync(K("sitekey"), K("url"), o),
            "captchafox" => await s.CaptchaFoxAsync(K("sitekey"), K("url"), o),
            "friendly_captcha" => await s.FriendlyCaptchaAsync(K("sitekey"), K("url"), o),
            "normal" => await s.NormalAsync(await ImageData(K("image"), http)),  // fetched with YOUR cookies
            _ => throw new InvalidOperationException("unknown method " + i.Method),
        };
    }
    static async Task<string> ImageData(string src, HttpClient http) => src.StartsWith("data:") ? src
        : "data:image/png;base64," + Convert.ToBase64String(await http.GetByteArrayAsync(src));
    static async Task<SolveResult> SolveHcaptcha(Dictionary<string, object?> a, CapSkipClient s, HttpClient http)
    {   // no SDK call yet: post to in.php and poll res.php at the address the client already uses
        var form = new Dictionary<string, string> { ["method"] = "hcaptcha", ["sitekey"] = (string)a["sitekey"]!,
            ["pageurl"] = (string)a["url"]!, ["invisible"] = a.ContainsKey("invisible") ? "1" : "0", ["json"] = "1" };
        if (a.TryGetValue("data", out var d) && d != null) form["data"] = d.ToString()!;
        var key = string.IsNullOrEmpty(s.ApiKey) ? "" : "&key=" + Uri.EscapeDataString(s.ApiKey);
        if (key != "") form["key"] = s.ApiKey;
        using var sent = await http.PostAsync(s.ApiClient.BaseUrl + "/in.php", new FormUrlEncodedContent(form));
        var job = JsonDocument.Parse(await sent.Content.ReadAsStringAsync()).RootElement;
        var id = job.GetProperty("request").GetString()!;
        if (job.GetProperty("status").GetInt32() != 1) throw new ApiException(id);
        for (var n = 0; n < 36; n++)  // a solve takes 10 to 30 seconds, so give up after 3 minutes
        {
            await Task.Delay(5000);
            var r = JsonDocument.Parse(await http.GetStringAsync($"{s.ApiClient.BaseUrl}/res.php?action=get&id={id}&json=1{key}")).RootElement;
            var value = r.GetProperty("request").GetString()!;
            if (value == "CAPCHA_NOT_READY") continue;
            if (r.GetProperty("status").GetInt32() != 1) throw new ApiException(value);
            return new SolveResult { CaptchaId = id, Code = value, UserAgent = r.TryGetProperty("useragent", out var ua) ? ua.GetString() : null };
        }
        throw new CapSkip.TimeoutException("hCaptcha was not solved within 3 minutes");
    }
}

Sobald ein Bild gelöst ist, senden Sie die Antwort mit demselben Objekt http als Formular-POST zurück, damit die Website das Cookie sieht, das sie gesetzt hat, als sie Ihnen das Bild zeigte. Das Ergebnis eines Turnstile- oder hCaptcha-Lösevorgangs trägt außerdem einen UserAgent, der bei einer Challenge-Seite mit dem Token mitgehen muss. Das Programm unten gibt nur das Feld Code aus, sodass ein Ergebnis von GeeTest oder Capy als rohes JSON erscheint.

Den Captcha-Detektor in C# ausführen

Ersetzen Sie den Inhalt von Program.cs durch diesen Block. Er ruft Ihre Seite ab, gibt den HTTP-Status aus, gibt aus, was der Detektor gefunden hat, und ruft CapSkip nur auf, wenn es eine Methode gibt und nichts fehlt. Er fängt CapSkipError ab, die Basis der Exceptions des SDK, statt sie einzeln zu nennen. Das vermeidet auch eine Falle: Bei eingeschalteten impliziten usings ist ein einfaches TimeoutException zwischen System und CapSkip mehrdeutig und lässt sich nicht kompilieren. Führen Sie es mit dotnet run, zwei Bindestrichen und der vollständigen Seitenadresse einschließlich https:// aus, wie im Befehl unten. Der Port 8080 ist der Standard; verwenden Sie den Port aus den Verbindungseinstellungen von CapSkip, falls Ihrer abweicht.

dotnet run -- "https://example.com/login"

Das ist das ganze Programm:

using CapSkip;

var pageUrl = args.Length > 0 ? args[0] : "https://example.com/login";
var http = new HttpClient();  // one client keeps one cookie jar for the page and the image
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0 Safari/537.36");
Info? info;
try
{
    var page = await http.GetAsync(pageUrl);
    Console.WriteLine((int)page.StatusCode);  // a Cloudflare challenge page arrives as a 403
    var finalUrl = page.RequestMessage!.RequestUri!.ToString();  // the URL after redirects
    info = Detector.Detect(await page.Content.ReadAsStringAsync(), finalUrl);
}
catch (Exception e) when (e is HttpRequestException or InvalidOperationException or TaskCanceledException or NotSupportedException)
{
    Console.WriteLine("Could not fetch the page: " + e.Message);
    return;
}
if (info is null) { Console.WriteLine("No CAPTCHA found."); return; }
Console.WriteLine($"{info.Type} via {info.Method ?? "nothing"}"
    + (info.Also is null ? "" : " (also " + string.Join(", ", info.Also) + ")"));
try
{
    var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
    var result = await Detector.Solve(info, solver, http);
    Console.WriteLine(result.Code.Length > 80 ? result.Code[..80] + "..." : result.Code);
}
catch (InvalidOperationException e) { Console.WriteLine("Not solvable from this HTML: " + e.Message); }
catch (HttpRequestException e) { Console.WriteLine("Image download failed: " + e.Message); }
catch (CapSkipError e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }

Auf einer Seite mit einer reCAPTCHA-v2-Checkbox zeigt die erste Zeile den Typ und die Methode, und die zweite zeigt den Token. Im Mock-Lauf sah es so aus:

SeiteDas Programm gibt aus
reCAPTCHA v2 checkbox200, dann recaptcha_v2 über recaptcha, dann der Token
hCaptcha-Widget200, dann hcaptcha über hcaptcha, dann der Token
Cloudflare-Seite “Just a moment”403, dann turnstile_challenge über turnstile, dann eine Meldung, dass data und pagedata nur zur Laufzeit existieren
Seite ohne Captcha200, dann No CAPTCHA found.
Adresse, die 404 zurückgibt404, dann No CAPTCHA found.

Wenn das HTML nichts hergibt: die Browser-Konsole

Manche Seiten fügen ihr Widget nach dem Laden mit JavaScript hinzu. Eine einfache Anfrage findet dann gar kein Captcha, und der Detektor gibt für eine Seite, die eines hat, null zurück. Zwei Fälle sind bekannt und zu erwarten: ein Skript von Google oder Cloudflare, das per Code hinzugefügt wird, und ein sitekey, der per Code gesetzt wird. Öffnen Sie dafür die Seite in Chrome, drücken Sie F12 und fügen Sie dies in die Console ein. Blockiert Chrome das Einfügen, tippen Sie zuerst allow pasting und drücken Sie Enter. Es liest die sitekeys aus den Ressourcen, die die Seite bereits geladen hat, und sieht daher, was der Browser gebaut hat.

(() => {
  const iframes = [...document.querySelectorAll('iframe')].map((f) => f.src);
  const urls = performance.getEntriesByType('resource').map((r) => r.name).concat(iframes);
  const found = new Map(), add = (o) => found.set(JSON.stringify(o), o);
  for (const u of urls) {
    let m;
    if ((m = u.match(/recaptcha\/(?:api2|(enterprise))\/anchor\?.*?\bk=([\w-]+)/)))
      add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: /size=invisible/.test(u) });
    else if ((m = u.match(/recaptcha\/(?:api|enterprise)\.js\?.*?\brender=([\w-]+)/)) && m[1] !== 'explicit')
      add({ type: 'recaptcha v3', sitekey: m[1], enterprise: /enterprise\.js/.test(u) });
    else if ((m = u.match(/challenges\.cloudflare\.com\/.*turnstile\/.*\/(0x4[\w-]+|[123]x0+[A-Za-z]+)\//)))
      add({ type: 'turnstile', sitekey: m[1] });
    else if ((m = u.match(/hcaptcha\.com\/captcha\/.*[#&?]sitekey=([\w-]+)/))) add({ type: 'hcaptcha', sitekey: m[1] });
    else if (/arkoselabs\.com\/fc\//.test(u)) add({ type: 'arkose: CapSkip cannot solve' });
  }
  if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });
  document.querySelectorAll('altcha-widget').forEach((e) => add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));
  document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) => add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));
  document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) => add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));
  const v3 = [...found.values()].filter((o) => o.type === 'recaptcha v3').map((o) => o.sitekey);  // a v3 badge also adds an invisible anchor frame
  const res = [...found.values()].filter((o) => !(o.type === 'recaptcha' && v3.includes(o.sitekey)));
  return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';
})();

Es hat den sitekey und die Flags auf einer reCAPTCHA-v2-Seite, einer unsichtbaren Seite, einer v3-Seite, einer Turnstile-Seite und der hCaptcha-Demo gefunden und auf einer Seite ohne Captcha eine Meldung ausgegeben. Drei Grenzen: Der Browser behält standardmäßig nur etwa 250 Ressourceneinträge, sodass eine lange laufende Single-Page-App frühe verlieren kann, und das Snippet kann Friendly Captcha Version 1 nicht von Version 2 unterscheiden und sucht nicht nach Skripten von GeeTest, Capy oder CaptchaFox. Bei der Vollseiten-Prüfung von Cloudflare kann es den sitekey aus einer Ressourcenadresse zeigen, aber nicht die Werte data und pagedata, die einen Hook auf den render-Aufruf brauchen. Dieser Hook wurde hier nicht getestet.

Wo die Erkennung scheitert

SituationWas Detect zurückgibtWas zu tun ist
Widget-Skript, das per JavaScript hinzugefügt wirdnullLaden Sie die Seite in einem Browser und führen Sie das Konsolen-Snippet aus
sitekey, der per JavaScript gesetzt wirdein Ergebnis, das einen Browser brauchtLesen Sie den Schlüssel aus der aktiven Seite
Vollseiten-Prüfung von Cloudflareein Ergebnis, das einen Browser brauchtLesen Sie die Werte in einem Browser, siehe die cf_clearance-Anleitung
Friendly-Captcha-Skript ohne div frc-captcha im HTMLnullFühren Sie das Konsolen-Snippet aus oder lesen Sie den Seitenquelltext
CaptchaFox- oder Capy-Skript, das per JavaScript hinzugefügt wirdnullFühren Sie das Konsolen-Snippet aus oder lesen Sie den Seitenquelltext
Eine 403- oder 503-Seite der Website selbstnull, außer es ist eine Cloudflare-ChallengeDas Programm gibt zuerst den Status aus: Eine Sperrseite enthält kein Captcha, das sich finden ließe
Arkose, GeeTest v4unsupportedCapSkip kann diese nicht lösen
hCaptcha Enterprise, das rqdata setztbraucht einen BrowserLesen Sie für jede Aufgabe ein frisches rqdata von der Live-Seite und übergeben Sie es als data an SolveHcaptcha
Friendly Captcha ohne Skript-Hinweisein Ergebnis, das einen Browser brauchtPrüfen Sie, welches Skript die Seite lädt
Bild-Captcha ohne das Wort captchanullFügen Sie eine eigene Regel für diese Website hinzu

Häufige Fehler

FehlerUrsacheBeheben
ERROR_GOOGLEKEYDer Schlüssel war das Wort explicit oder ein Schlüssel, der kein reCAPTCHA-Schlüssel istLesen Sie den Schlüssel aus dem Widget, nicht aus dem Parameter render
ERROR_CAPTCHA_UNSOLVABLEJeder fehlgeschlagene Job sieht so aus, und der Code nennt die Ursache nicht. Eine falsche reCAPTCHA-Version ist eine mögliche UrsachePrüfen Sie zuerst den erkannten Typ, dann lesen Sie die API-Dokumentation
ValidationExceptionEine Option, die die Methode nicht annimmt, etwa eine Action bei einem v2-AufrufVerwenden Sie die Argumente, die der Detektor zurückgegeben hat
NetworkExceptionCapSkip läuft nicht, oder Host oder Port stimmen nicht; unter Windows kommt der Fehler nach etwa 2 SekundenStarten Sie CapSkip und prüfen Sie die Verbindungseinstellungen
InvalidOperationException von SolveDas Ergebnis ist unsupported oder braucht einen BrowserBehandeln Sie diese beiden Fälle, bevor Sie Solve aufrufen

Den Löser auf einem Server betreiben

Der Detektor braucht von CapSkip nur zwei Dinge: einen Host und einen Port. Im Local-Modus sind das 127.0.0.1 und Ihr eigener PC. Im Server-Modus lauscht CapSkip auf Ihrer Netzwerkadresse oder einer öffentlichen IP, sodass ein Detektor auf einem anderen PC, einem VPS oder einer gehosteten Plattform es über dieselbe API aufrufen kann. Ändern Sie Host und Port im Konstruktor von CapSkipClient, und nichts anderes im Code ändert sich. Für einen stabilen Fernzugriff wird eine statische öffentliche IP empfohlen. Der Server-Modus läuft weiterhin auf Ihrer eigenen Hardware, und es wird weiterhin nichts pro Lösung abgerechnet. Beachten Sie, dass der Server-Modus den Port für jede Adresse öffnet, fügen Sie daher in der Windows Firewall eine Blockierregel für die Adressen hinzu, die ihn nicht erreichen sollen. Wie Sie den Modus wechseln, finden Sie in den Verbindungseinstellungen beschrieben.

FAQ

Kann ich ohne Code herausfinden, welches Captcha eine Website verwendet?

Ja. Öffnen Sie DevTools, wechseln Sie zum Tab Network, laden Sie die Seite neu und sehen Sie sich die Skript-Adressen an. Ein Skript von google.com/recaptcha oder recaptcha.net ist reCAPTCHA, challenges.cloudflare.com ist Turnstile und js.hcaptcha.com ist hCaptcha. Das Console-Snippet oben erledigt dieselbe Suche für Sie.

Warum gibt Detect ein Ergebnis zurück, das einen Browser braucht, statt zu raten?

Weil ein falscher Tipp einen ganzen Lösevorgang verschwendet und einen Token zurückgibt, den die Website ohne jeden Hinweis ablehnt. Enthält das statische HTML den sitekey, eine GeeTest-Challenge oder eine Version von Friendly Captcha nicht, geht es zügiger, das zu sagen, als einen Job scheitern zu lassen.

Löst CapSkip hCaptcha?

Ja. CapSkip löst hCaptcha-Widgets als Checkbox, unsichtbar und Enterprise über seine HTTP API mit method=hcaptcha, wie es die API-Dokumentation beschreibt. Das .NET-SDK hat noch keinen hCaptcha-Aufruf, daher sendet SolveHcaptcha oben den Job selbst an in.php. FunCaptcha, Arkose und GeeTest v4 werden weiterhin nicht unterstützt, und der Detektor meldet sie als unsupported, damit ein Programm die Seite überspringen kann, statt an ihr zu scheitern.

Kann eine gehostete Plattform den Detektor mit meinem CapSkip verwenden?

Ja, über den Server-Modus. Stellen Sie CapSkip so ein, dass es auf Ihrer Netzwerkadresse oder öffentlichen IP lauscht, richten Sie den Konstruktor von CapSkipClient auf diesen Host und Port, und der Detektor und der Aufruf Solve funktionieren wie auf einem einzigen PC. Der Solver bleibt auf Ihrer Hardware.

Die Kurzfassung

Ein Captcha-Detektor in C# verdient sich seinen Platz dadurch, dass er zuerst läuft: Captcha-Typ erkennen, dann lösen. Lesen Sie das Skript und das Markup in fester Reihenfolge, übergeben Sie die Argumente an die richtige Methode und behandeln Sie null, ein Ergebnis, das einen Browser braucht, und unsupported als Antworten, nicht als Abstürze. Mit CapSkip auf Ihrem eigenen Rechner läuft beim Prüfen einer Seite kein Zähler. Genau deshalb ist ein Captcha-Löser praktisch für einen Crawl, der alle paar Minuten auf eine neue Website trifft. Zum .NET-SDK selbst siehe die C#-Captcha-Solver-Seite.

Derselbe Detektor in anderen Sprachen: