So lösen Sie ein DataDome-Captcha in PHP (Guzzle CookieJar)

datadome captcha in php - How to Solve DataDome CAPTCHA in PHP (Guzzle CookieJar)

Ein DataDome-Captcha in PHP endet mit einem Cookie, nicht mit einem Token. Dieser Leitfaden liest die 403-Sperrseite, baut die Challenge-Adresse und übergibt sie an die Methode dataDome() des Pakets capskip/capskip. Diese Methode spricht mit dem CapSkip-Solver für DataDome. Das zurückgegebene Cookie behält das Skript anschließend im Cookie-Jar des Guzzle-Clients, der die nächste Anfrage sendet. Laut der CapSkip-Dokumentation funktioniert das Cookie nur mit dem User-Agent und der IP-Adresse, mit denen es ausgestellt wurde, und ein Guzzle-Client legt beides fest.

Was Sie brauchen

  • CapSkip 3.2.0 oder neuer auf einem Windows-PC, die erste Version, die DataDome löst (Slider und Geräteprüfung).
  • PHP 8.0.2 oder neuer mit den Erweiterungen curl und json sowie Composer, beide in Ihrem PATH. Die Plattformprüfung von Composer nannte 8.0.2 bei einer Installation von 8.0.30 und 8.1.0 bei 8.1.33 und 8.4.15.
  • Ein leerer Arbeitsordner. Führen Sie darin composer require capskip/capskip guzzlehttp/guzzle aus und speichern Sie das Skript dort als datadome-demo.php. Releases von capskip vor 1.5.0 haben die Methode dataDome nicht.
  • Ein CA-Bundle. PHP für Windows wird ohne eines ausgeliefert, daher scheitert die erste https-Anfrage mit cURL error 60, bis Sie cacert.pem von curl.se herunterladen und curl.cainfo in der php.ini darauf zeigen lassen.
  • Eine DataDome-geschützte Seite, die Sie automatisieren dürfen.
  • Ein Host und ein Port für CapSkip. Im Local-Modus antwortet CapSkip nur auf 127.0.0.1, für Skripte auf demselben PC. Im Server-Modus kann PHP auf einem anderen Rechner CapSkip über Ihr Netzwerk oder eine öffentliche IP aufrufen. Den Modus wählen Sie in den Verbindungseinstellungen.
  • Für alles über einen Test hinaus: ein Sticky-Proxy pro Aufgabe.

Testmethode: zwei Mock-Server, die ich geschrieben habe, nur unter Windows und ohne echte DataDome-Website. Einer übernimmt die Prüfungen aus dem DataDome-Abschnitt der CapSkip API-Dokumentation. Der andere spielt eine geschützte Website, die mit 403 und einer Sperrseite antwortet, nur ein Cookie akzeptiert, das der erste Mock ausgestellt hat, und Anfragen auch als Proxy weiterleitet. Für https verwendete er eine Wegwerf-CA. Die Skripte berührten die CapSkip-App nie, daher gibt es keine Zeitmessung für eine echte Lösung und kein Urteil einer Live-Website.

Die Blöcke liefen auf PHP 8.0.30, 8.1.33, 8.2.29, 8.3.28 und 8.4.15 mit capskip 1.6.0 und Guzzle 8.3.1 sowie auf 8.0.30 und 8.4.15 mit capskip 1.6.0 und Guzzle 7.16.1. Unter PHP 8.5.0 läuft das Skript durch, aber die Datei Http.php des SDK gibt für jede Anfrage an CapSkip einen Deprecated-Hinweis zu curl_close() aus, in meinem Lauf drei.

Schritt 1: ein Guzzle-Client pro Identität, dann die Sperrseite

Setzen Sie die fünf Codeblöcke in der gezeigten Reihenfolge in datadome-demo.php ein. Der erste enthält jede use-Zeile, die anderen fügen keine hinzu.

Drei Standardeinstellungen von Guzzle arbeiten gegen diese Schleife. Ein Client behält keine Cookies, solange Sie ihm kein Cookie-Jar übergeben. Er nennt sich selbst GuzzleHttp plus seine Hauptversion, was der Mock bei einem unveränderten Client unter Guzzle 7 und 8 sah. Und er wirft bei jeder 4xx-Antwort eine ClientException, was hier am meisten stört, denn die 403-Sperrseite ist die Challenge, die Sie lesen müssen. Mit dieser Voreinstellung starb mein Lauf bei der ersten Anfrage, und das Mock-CapSkip bekam nie eine Aufgabe. Deshalb schaltet session() http_errors aus. Es setzt außerdem Cookie-Jar, User-Agent und Proxy zusammen, damit sie nicht auseinanderlaufen. Eine neue Identität bedeutet einen neuen Client.

<?php
// composer require capskip/capskip guzzlehttp/guzzle
require __DIR__ . '/vendor/autoload.php';

use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\CapSkipError;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\SetCookie;
use Psr\Http\Message\ResponseInterface;

const PAGE_URL = 'https://www.example.com/'; // a page behind DataDome
// For the first request only. The cookie comes back with its own User-Agent.
const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
    . '(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36';

// Guzzle fixes its options when the Client is built, so one Client is one
// identity: a cookie jar, a User-Agent and a proxy.
function session(CookieJar $jar, string $userAgent, ?string $proxyUrl = null): Client
{
    return new Client([
        'cookies' => $jar,
        'headers' => ['User-Agent' => $userAgent],
        'proxy' => $proxyUrl,
        'http_errors' => false, // a 403 block page is an answer, not an exception
        'timeout' => 30,
    ]);
}

Eine Anfrage, der DataDome misstraut, bekommt ein 403, manchmal ein 401, und eine kurze HTML-Seite. Die Challenge-Adresse, die die Dokumentation captcha_url nennt, ist entweder das src eines iframes von geo.captcha-delivery.com oder etwas, das ein Skript aus einem dd-Objekt zusammensetzt. Die folgende Funktion kommt mit beidem zurecht. Der Eintrag rt des dd-Objekts wählt die Challenge-Seite: Der Buchstabe c wählt den Slider unter /captcha/, der Buchstabe i die Geräteprüfung unter /interstitial/. Die Dokumentation definiert einen t-Wert von bv als gesperrte Adresse und zeigt fe bei gewöhnlichen Challenges; diese Deutung von fe stammt von ihr.

Die erste Anfrage geht mit einem schlichten Chrome-User-Agent hinaus, der nie an CapSkip gesendet wird, seine Version muss also zu nichts passen. Jede Sperrseite ist eine neue Challenge, verwenden Sie ihre Adresse daher bald.

// Build the challenge address from a DataDome block page, or null.
function captchaUrlFrom(ResponseInterface $resp): ?string
{
    if (!in_array($resp->getStatusCode(), [401, 403], true)) {
        return null;
    }
    $html = (string) $resp->getBody();
    // Some block pages carry the challenge iframe: its src is the address.
    if (preg_match('/<iframe[^>]+src="(https:\/\/[^"]*captcha-delivery\.com\/[^"]+)"/', $html, $found)) {
        return $found[1]; // an HTML-escaped &amp; is fine, CapSkip decodes it
    }
    // Most build it from the dd object in a script instead.
    if (!preg_match('/var\s+dd\s*=\s*(\{[^}]*\})/', $html, $found)) {
        return null;
    }
    $dd = json_decode(str_replace("'", '"', $found[1]), true);
    if (!is_array($dd)) {
        return null;
    }
    $query = ['initialCid' => $dd['cid'] ?? '', 'hash' => $dd['hsh'] ?? '', 'cid' => $dd['cookie'] ?? ''];
    if (!empty($dd['t'])) {
        $query['t'] = $dd['t']; // fe is a challenge, bv means the address is banned
    }
    $query['referer'] = PAGE_URL;
    $query['s'] = $dd['s'] ?? '';
    foreach (['e', 'b'] as $key) {
        if (!empty($dd[$key])) {
            $query[$key] = $dd[$key];
        }
    }
    $query['dm'] = 'cd';
    $path = ($dd['rt'] ?? '') === 'i' ? 'interstitial' : 'captcha';

    return 'https://' . ($dd['host'] ?? 'geo.captcha-delivery.com') . "/{$path}/?"
        . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
}

Schritt 2: lösen und das Cookie ins Cookie-Jar legen

Die Methode lautet dataDome(string $captchaUrl, string $url, array $options = []), und datadome() in Kleinbuchstaben lieferte in meinem Lauf dasselbe Ergebnis. Das Options-Array nimmt userAgent und proxy entgegen, null-Werte darin werden nicht gesendet. Ein Proxy wird wie unten als Array mit type und uri übergeben oder als einfacher String, den das SDK als HTTP-Proxy sendet.

Lassen Sie userAgent weg. CapSkip verwendet dann den User-Agent seines eigenen Browsers und meldet ihn im Ergebnis, und diesen String senden Sie ab da. Ein eigener muss ein Standard-Chrome für Windows mit derselben Hauptversion wie dieser Browser sein: Das SDK lehnte lokal einen Firefox-String ab, und der Mock antwortete auf einen Chrome-120-String mit ERROR_WRONG_USERAGENT. Die Methode wartet bis zu recaptchaTimeout, standardmäßig 300 Sekunden.

// Local mode. In Server mode, use your server's address instead.
$solver = new CapSkip(['host' => '127.0.0.1', 'port' => 8080]);

function solve(CapSkip $solver, string $captchaUrl, ?string $proxyUrl): array
{
    $options = [];
    if ($proxyUrl !== null) {
        [$scheme, $rest] = explode('://', $proxyUrl, 2);
        $options['proxy'] = ['type' => strtoupper($scheme), 'uri' => $rest];
    }
    // No userAgent option: CapSkip uses its own browser's and reports it.
    return $solver->dataDome($captchaUrl, PAGE_URL, $options);
}

Das Ergebnis ist ein Array mit den Schlüsseln captchaId, code, userAgent und cookie. Der Wert code ist das Cookie genau so, wie DataDome es gesetzt hat, einschließlich der Attribute. Der Wert cookie ist nur das Paar aus Name und Wert von datadome. Ein Schlüssel proxy erscheint nur, wenn Sie keinen übergeben haben und CapSkip einen Proxy aus seiner eigenen DataDome-Liste verwendet hat. Er enthält Host und Port ohne Schema und Login, ergänzen Sie den Login also selbst, wenn dieser Listeneintrag einen braucht. Das Cookie funktioniert dann nur über diesen Proxy.

Die Exceptions des SDK liegen unter CapSkip\Exceptions und erweitern CapSkipError, das Exception erweitert, nicht RuntimeException. Nur eine ApiException, also eine abgelehnte Aufgabe, ist eine gescheiterte Lösung, die einen weiteren Versuch wert ist.

Hier unterscheidet sich ein PHP-Skript von einem, das einen Cookie-Header baut. Der Wert code lässt sich direkt in ein Guzzle-SetCookie parsen, und das Cookie-Jar wendet dann die Regeln eines Browsers an: Es sendet das Cookie nur an einen Host, der zum Attribut Domain passt, für einen passenden Pfad im Attribut Path, bis Max-Age abläuft, und über https nur, wenn das Attribut Secure gesetzt ist. Es behält außerdem das Ersatz-Cookie. Der Mock sendete mit der 200-Seite ein neues, und die nächste Anfrage über denselben Client trug es.

Das Cookie-Jar scheitert auf vier Arten still, und ich bin in jede hineingelaufen. Ein Cookie ohne Attribut Domain wird gespeichert, aber nie gesendet. Passen Domain oder Secure nicht, geht kein Cookie-Header hinaus, sodass die Website mit 403 antwortet, obwohl das Cookie-Jar das Cookie enthält. setCookie() gibt false zurück, statt eine Exception zu werfen, wenn es ein Cookie ablehnt, zum Beispiel ein abgelaufenes. Und ein datadome-Cookie, das die Sperrseite selbst gesetzt hat, bleibt neben dem gelösten im Cookie-Jar, sodass beide hinausgehen und eine Website das falsche lesen kann. Mein erster Mock setzte dieses Cookie nie; als er es tat, scheiterten alle drei Versuche, bis der Aufruf clear() unten es entfernte. Der Block schützt vor allen vieren.

function sendWithCookie(array $result, CookieJar $jar, ?string $proxyUrl): array
{
    // code is the cookie as DataDome set it, with Domain, Path and Max-Age.
    $cookie = SetCookie::fromString($result['code']);
    if (!$cookie->getDomain()) {
        // A jar keeps a cookie with no Domain but never sends it.
        $cookie->setDomain(parse_url(PAGE_URL, PHP_URL_HOST));
    }
    // The block page may have set a datadome cookie of its own. Drop it.
    $jar->clear(parse_url(PAGE_URL, PHP_URL_HOST), '/', 'datadome');
    if (!$jar->setCookie($cookie)) { // false, not an exception, for a cookie it rejects
        throw new RuntimeException('The cookie jar refused: ' . $result['code']);
    }
    // The cookie works only from the address that earned it.
    $via = $proxyUrl;
    if ($via === null && !empty($result['proxy'])) {
        // CapSkip solved through its own list. Add user:pass@ if that entry needs a login.
        $via = 'http://' . $result['proxy'];
    }
    $http = session($jar, $result['userAgent'], $via);

    return [$http, $http->get(PAGE_URL)];
}

Schritt 3: das vollständige Beispiel mit einer Wiederholungsschleife bei 403

Der letzte Block verbindet die Schritte. Fügen Sie ihn zu datadome-demo.php hinzu und führen Sie php datadome-demo.php im Arbeitsordner aus. Jeder Versuch beginnt mit einem neuen Cookie-Jar und einem neuen Client, sodass jeder Durchlauf eine frische Challenge trägt und kein Cookie eines gescheiterten Versuchs. Diese Entscheidung stammt von mir und ist gegen DataDome ungetestet.

Die Schleife wiederholt eine ApiException wie ERROR_CAPTCHA_UNSOLVABLE sowie ein Cookie, das trotzdem ein 403 bekommt. Eine ApiException, die ERROR_PROXY_ nennt, wird sofort erneut geworfen, weil dieselbe Adresse genauso scheitern würde. Ein 403 ohne dd-Objekt oder iframe beendet die Schleife. Jede Wiederholung kostet die Website eine weitere Anfrage nach einer Sperrseite, daher sind drei Versuche eine sinnvolle Obergrenze.

function fetchProtected(CapSkip $solver, ?string $proxyUrl = null): array
{
    for ($attempt = 1; $attempt <= 3; $attempt++) {
        $jar = new CookieJar(); // a clean jar for every attempt
        $http = session($jar, BROWSER_UA, $proxyUrl);
        $block = $http->get(PAGE_URL); // a fresh block page, a fresh challenge
        $captchaUrl = captchaUrlFrom($block);
        if ($captchaUrl === null) {
            if ($block->getStatusCode() >= 400) {
                throw new RuntimeException('HTTP ' . $block->getStatusCode() . ' without a DataDome block page');
            }
            return [$http, $block]; // nothing to solve here
        }
        try {
            $result = solve($solver, $captchaUrl, $proxyUrl);
        } catch (ApiException $e) {
            if (str_contains($e->getMessage(), 'ERROR_PROXY_')) {
                throw $e; // the address is the problem, so a retry fails the same way
            }
            echo "attempt {$attempt}: {$e->getMessage()}\n";
            continue;
        }
        [$http, $page] = sendWithCookie($result, $jar, $proxyUrl);
        if ($page->getStatusCode() === 200) {
            return [$http, $page];
        }
        echo "attempt {$attempt}: HTTP {$page->getStatusCode()} with the cookie\n";
    }
    throw new RuntimeException('DataDome not passed in 3 attempts');
}

try {
    [$http, $page] = fetchProtected($solver); // or ($solver, 'http://user:[email protected]:8080')
    echo $page->getStatusCode(), ' ', substr((string) $page->getBody(), 0, 40), PHP_EOL;
    // Same Client, so the same jar, User-Agent and proxy.
    echo $http->get(PAGE_URL)->getStatusCode(), PHP_EOL;
} catch (CapSkipError | RuntimeException $e) {
    fwrite(STDERR, get_class($e) . ': ' . $e->getMessage() . PHP_EOL);
    exit(1);
}

Das vollständige Skript für ein DataDome-Captcha in PHP gab gegen den Mock Folgendes aus:

200 <html><body><h1>Real content</h1></body>
200

Jede Zeile unten kam auf allen diesen Kombinationen gleich heraus, bis auf ein Detail: Guzzle 8 wirft für cURL error 60 eine ConnectException, Guzzle 7 eine RequestException.

Mock-EinstellungWas das Skript tat
Sperrseite mit Slider, Geräteprüfung oder iframeEine Aufgabe, dann 200, und bei Wiederverwendung wieder 200
Die ersten zwei Lösungen abgelehnt, oder alle dreiEin 200 bei der dritten Aufgabe, oder RuntimeException und Exit-Code 1
Sperrseite mit t=bvValidationException; der Mock erhielt nichts
Das Ergebnis nennt einen Proxy, oder Sie übergeben einen eigenenBeide Anfragen mit dem Cookie nutzten diesen Proxy und waren erfolgreich
Die Lösung antwortete mit ERROR_PROXY_BANNEDApiException nach einer Aufgabe
Nicht passendes Attribut Domain, oder Secure bei einfachem httpDrei Versuche, jeweils ein 403 mit dem Cookie
Cookie ohne Attribut DomainEin 200 mit dem Fallback im Block von Schritt 2, drei 403-Versuche ohne ihn
Sperrseite, die ein eigenes datadome-Cookie setztEin 200 mit clear() im Block von Schritt 2, drei 403-Versuche ohne
https-Website mit Wegwerf-CAcURL error 60 ohne curl.cainfo; mit curl.cainfo ein 200, auch für ein Secure-Cookie

Die Bindung des Cookies und dass die Mock-Website das erste von zwei datadome-Cookies liest, sind Regeln, die ich nach dem Lesen der CapSkip-Dokumentation geschrieben habe. Über den Live-Dienst sagen sie nichts.

Kommandozeile oder Webanfrage?

Das SDK wartet bis zu 300 Sekunden auf eine Lösung. Laut dem PHP-Handbuch beträgt der Standardwert von max_execution_time 30 Sekunden und auf der Kommandozeile 0. Ich habe die Mock-Aufgabe 8 Sekunden lang festgehalten, sodass das Skript etwa 13 brauchte, und jeden Lauf unter PHP 8.4.15 von seiner ersten bis zu seiner letzten Anweisung gemessen; PHP 8.0.30 lieferte dieselben Ergebnisse auf 0,1 Sekunden genau. Die php.ini auf diesem PC sagt 120, und für das Limit von 5 habe ich eine Kopie davon verwendet. php.exe ignoriert max_execution_time in der php.ini, daher begrenzt nur die Option -d oder set_time_limit() einen Konsolenlauf, während php-cgi.exe, die CGI-Variante, über die ein Webserver PHP ausführen kann, die Datei liest.

Wie es liefErgebnis
php.exe, nichts gesetzt (php.ini sagt 120, php.exe liest 0)Beendet: Status 200 zweimal, etwa 13 Sekunden
php.exe, Limit von 5 in der php.iniBeendet: Das Limit wurde ignoriert, etwa 13 Sekunden
php.exe -d max_execution_time=5Fatal error nach 7,9 Sekunden
php-cgi.exe, Limit von 5 in der php.iniFatal error nach 7,9 Sekunden
php-cgi.exe -d max_execution_time=5 mit set_time_limit(60) als erster AnweisungBeendet nach etwa 13 Sekunden
php-cgi.exe, nichts gesetzt (liest 120 aus der php.ini)Beendet nach etwa 13 Sekunden

Ich habe das nur unter Windows ausgeführt, wo das Limit auch das Warten zählt, sodass ein Skript, das in der Poll-Schleife des SDK blockiert, es aufbraucht. PHP löste den Fatal Error nach etwa 8 Sekunden aus, nicht nach 5, weil das Skript zwischen den Abfragen schlief, als das Limit verstrich. Das Skript stirbt, ohne das Cookie zu lesen, und CapSkip hat die Lösung bereits verbraucht. Führen Sie diese Schleife über eine Konsole, einen geplanten Task oder einen Queue-Worker aus; zur Form des Workers siehe den Laravel-Queue-Leitfaden. Rufen Sie in einer Webanfrage zuerst set_time_limit() mit einem Wert auf, der größer ist als die Dauer einer Lösung.

Häufige Fehler

FehlerUrsacheBeheben
cURL error 60: SSL certificate problemPHP für Windows hat kein CA-BundleLaden Sie cacert.pem von curl.se herunter und setzen Sie curl.cainfo in der php.ini
ValidationExceptionDas SDK hat die Eingabe abgelehnt: eine Adresse außerhalb von captcha-delivery.com, eine Sperrseite mit t=bv, eine Seiten-URL, die nicht http oder https ist, oder ein userAgent, der kein Standard-Chrome für Windows ist. Die Meldung nennt, welcher Fall es istBauen Sie die Adresse aus einer neuen Sperrseite neu auf, wechseln Sie den Proxy, wenn er gesperrt ist, und lassen Sie userAgent weg
ApiException mit ERROR_PROXY_BANNED, ERROR_PROXY_CONNECT_REFUSED oder ERROR_WRONG_USERAGENTDie Adresse ist gesperrt, CapSkip kann sich nicht über den Proxy verbinden (nicht reproduziert), oder Ihr userAgent hat eine andere Chrome-Hauptversion als der Browser, mit dem CapSkip löstÄndern oder testen Sie den Proxy, oder senden Sie keinen userAgent
TimeoutException oder NetworkExceptionDie Wartezeit von 300 Sekunden ist abgelaufen, oder unter dem angegebenen Host und Port antwortet nichtsStarten Sie CapSkip und vergleichen Sie Ihren Host und Port mit dessen Verbindungseinstellungen
HTTP 403 mit dem Cookie, obwohl das Cookie-Jar es enthältDas Attribut Domain passt nicht zum angefragten Host, oder das Attribut Secure ist gesetzt und die URL ist httpGeben Sie das Cookie-Jar aus und vergleichen Sie es mit PAGE_URL
Fatal error: Maximum execution time of 30 seconds exceededEine Webanfrage hat beim Polling des SDK das Zeitlimit von PHP erreicht; die Zahl ist Ihr LimitFühren Sie das Skript über eine Konsole oder einen Worker aus

FAQ

Kann ich ein DataDome-Captcha in PHP gegen CapSkip auf einem Server ausführen?

Ja, dafür ist der Server-Modus da. CapSkip nimmt dann Aufrufe aus Ihrem Netzwerk oder von einer öffentlichen IP an (eine statische wird empfohlen), und Sie ändern nur Host und Port im Konstruktor. Der Windows-Rechner bleibt Ihrer und ohne Abrechnung. Der Server-Modus öffnet den Port beim Start für jede Adresse, halten Sie ihn daher mit einer Blockierregel der Windows Firewall für die Rechner, die Sie nicht erwarten, privat. Die Lösung läuft auf dem Server, das Cookie gehört also zu dessen Adresse: Senden Sie Ihre Guzzle-Anfragen von diesem Rechner aus, oder geben Sie der Lösung und dem Client denselben Proxy.

Warum ein Cookie-Jar und kein Cookie-Header?

Ein Header funktioniert. Das eigene Beispiel datadome.php des SDK sendet den Cookie-Wert in einem Header von einem einfachen cURL-Handle; ich habe es nicht ausgeführt. Das Cookie-Jar bringt den Abgleich über die Attribute Domain und Path, den Ablauf und das Ersatz-Cookie mit, zum Preis der stillen Fehler in Schritt 2.

Ist ein Proxy erforderlich?

Für einen ersten Test nicht. Laut der CapSkip-Dokumentation funktioniert das Cookie nur von der Adresse, mit der es ausgestellt wurde, daher brauchen Sie bei größerem Volumen einen Sticky-Proxy pro Aufgabe. Zum Rotieren lesen Sie den Leitfaden zur Proxy-Rotation.

Die Kurzfassung

Ein DataDome-Captcha in PHP zu lösen, erfordert einen Guzzle-Client pro Identität, eine Sperrseite, die mit ausgeschaltetem http_errors gelesen wird, einen Aufruf von dataDome() und das zurückgegebene Cookie im Cookie-Jar des Clients. Wiederholungen kosten nichts, denn ein lokaler Captcha-Löser wird von Ihnen selbst betrieben. Lassen Sie die Schleife also eine frische Challenge anfordern, statt eine veraltete wiederzuverwenden. Die SDK-Aufrufe für die anderen Captcha-Typen finden Sie auf der PHP-Captcha-Solver-Seite.