API-Tutorials

PowerShell + CaptchaAI: Windows-Automatisierungs-CAPTCHA-Lösung

Ein Wartungsskript läuft seit Monaten unbeaufsichtigt im Aufgabenplaner – und scheitert eines Nachts, weil das Anmeldeformular der überwachten Anwendung jetzt ein reCAPTCHA zeigt. Dafür braucht es kein Framework und kein Zusatzmodul: Zwei Invoke-RestMethod-Aufrufe liefern ein Token, das Ihr Skript wie jeden anderen Formularwert weiterreicht.

Der Leitfaden zeigt den kompletten Weg in Windows PowerShell 5.1 und PowerShell 7 – übermitteln, abfragen, Token einsetzen – für reCAPTCHA v2 und v3, Turnstile und Bild-CAPTCHAs.


Voraussetzungen

  • PowerShell 5.1 (in Windows enthalten) oder PowerShell 7+ (auch unter Linux und macOS)
  • Ein CaptchaAI-Konto mit API-Schlüssel (Schlüssel abrufen)
  • Ausgehende HTTPS-Verbindungen zu ocr.captchaai.com – Module oder SDKs sind nicht nötig

Zwei Endpunkte, ein Ablauf

Die API kennt nur zwei Adressen. An in.php übermitteln Sie die Aufgabe – Methode, Sitekey, Seiten-URL – und erhalten eine Task-ID. Von res.php fragen Sie das Ergebnis ab, bis statt CAPCHA_NOT_READY das Token oder der erkannte Text zurückkommt.

Die Wartezeit hängt vom Typ ab: Bild-CAPTCHAs unter 0,5 Sekunden, Turnstile unter 10 Sekunden, reCAPTCHA v2 unter 60 Sekunden – jeweils mit hoher Erfolgsquote auf den unterstützten Typen. Ein Intervall von 5 Sekunden und ein Timeout von 300 Sekunden decken die Spanne ab.

Warum PowerShell für CAPTCHA-Automatisierung passt

  • Ohne Installation verfügbar – ab PowerShell 5.1 auf jedem Windows-System
  • Invoke-RestMethod spricht REST-APIs nativ an und wandelt JSON in Objekte um
  • Aufgabenplaner startet CAPTCHA-abhängige Skripte zeitgesteuert, ohne Zusatzdienst
  • Pipeline-tauglich – das Token geht direkt an nachgelagerte Cmdlets weiter
  • Plattformübergreifend – dieselben Skripte laufen mit PowerShell 7 auf Linux-Buildagenten

Bausteine: Aufgabe übermitteln

Die erste Funktion kapselt in.php. Schlüssel und json = 1 sind immer gleich, alles Typspezifische kommt als Hashtable dazu. Ein Status ungleich 1 wirft sofort eine Ausnahme.

function Submit-CaptchaTask {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [hashtable]$TaskParams
    )

    $body = @{
        key  = $ApiKey
        json = 1
    } + $TaskParams

    $response = Invoke-RestMethod -Uri "https://ocr.captchaai.com/in.php" `
        -Method Post `
        -Body $body `
        -ContentType "application/x-www-form-urlencoded"

    if ($response.status -ne 1) {
        throw "Submit failed: $($response.request)"
    }

    return $response.request
}

Ergebnis abfragen

Das Gegenstück fragt res.php ab, bis eine Lösung vorliegt oder die Frist abläuft. Wichtig ist die Reihenfolge: erst warten, dann abfragen – eine Anfrage direkt nach dem Übermitteln liefert ohnehin nur CAPCHA_NOT_READY.

function Get-CaptchaResult {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$TaskId,

        [int]$MaxWaitSeconds = 300,
        [int]$PollIntervalSeconds = 5
    )

    $deadline = (Get-Date).AddSeconds($MaxWaitSeconds)

    while ((Get-Date) -lt $deadline) {
        Start-Sleep -Seconds $PollIntervalSeconds

        $response = Invoke-RestMethod -Uri "https://ocr.captchaai.com/res.php" `
            -Method Get `
            -Body @{
                key    = $ApiKey
                action = "get"
                id     = $TaskId
                json   = 1
            }

        if ($response.request -eq "CAPCHA_NOT_READY") {
            Write-Verbose "Waiting for solution..."
            continue
        }

        if ($response.status -ne 1) {
            throw "Solve failed: $($response.request)"
        }

        return $response.request
    }

    throw "Timeout: CAPTCHA not solved within $MaxWaitSeconds seconds"
}

reCAPTCHA v2 in PowerShell lösen

Damit bleibt die eigentliche Lösefunktion kurz. Sie brauchen zwei Werte von der Zielseite: den Sitekey aus data-sitekey und die vollständige Seiten-URL. Beide gehen als googlekey und pageurl an die Methode userrecaptcha.

function Solve-RecaptchaV2 {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey
    )

    Write-Host "Submitting reCAPTCHA v2 task..."
    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method    = "userrecaptcha"
        googlekey = $SiteKey
        pageurl   = $SiteUrl
    }
    Write-Host "Task ID: $taskId"

    Write-Host "Polling for solution..."
    $token = Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
    Write-Host "Solved! Token: $($token.Substring(0, [Math]::Min(50, $token.Length)))..."

    return $token
}

# Usage
$apiKey = "YOUR_API_KEY"
$token = Solve-RecaptchaV2 `
    -ApiKey $apiKey `
    -SiteUrl "https://example.com/login" `
    -SiteKey "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"

Cloudflare Turnstile lösen

Für Turnstile ändern sich nur Methode und Parametername. Das Ergebnis landet im versteckten Feld cf-turnstile-response statt in g-recaptcha-response; Turnstile ist meist in unter 10 Sekunden gelöst – deutlich schneller als reCAPTCHA v2.

function Solve-Turnstile {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey
    )

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method  = "turnstile"
        key     = $SiteKey
        pageurl = $SiteUrl
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

# Usage
$token = Solve-Turnstile `
    -ApiKey "YOUR_API_KEY" `
    -SiteUrl "https://example.com/form" `
    -SiteKey "0x4AAAAAAAB5..."

reCAPTCHA v3: Version und Action mitgeben

reCAPTCHA v3 zeigt nichts an, sondern bewertet im Hintergrund. Dafür kommen zwei Parameter dazu: version = "v3" und die action aus dem grecaptcha.execute-Aufruf im Quelltext. Beide müssen zur Zielseite passen – ein Token mit falscher Action wird serverseitig anders bewertet.

function Solve-RecaptchaV3 {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey,

        [string]$Action = "verify",
    )

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method    = "userrecaptcha"
        googlekey = $SiteKey
        pageurl   = $SiteUrl
        version   = "v3"
        action    = $Action
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

Bild-CAPTCHAs aus Datei und URL lösen

Bild-CAPTCHAs laufen über die Methode base64: Datei einlesen, umwandeln, als body übermitteln. Zurück kommt kein Token, sondern der erkannte Text – unter 0,5 Sekunden und damit auch für Massenläufe geeignet.

function Solve-ImageCaptcha {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$ImagePath
    )

    if (-not (Test-Path $ImagePath)) {
        throw "Image file not found: $ImagePath"
    }

    $imageBytes = [System.IO.File]::ReadAllBytes($ImagePath)
    $base64 = [Convert]::ToBase64String($imageBytes)

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method = "base64"
        body   = $base64
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

# Usage
$text = Solve-ImageCaptcha -ApiKey "YOUR_API_KEY" -ImagePath "C:\captcha.png"
Write-Host "CAPTCHA text: $text"

Direkt von einer URL

Liegt das Bild nicht auf der Platte, holt Invoke-WebRequest die Bytes direkt – die Zwischendatei entfällt.

function Solve-ImageCaptchaFromUrl {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$ImageUrl
    )

    $imageBytes = (Invoke-WebRequest -Uri $ImageUrl).Content
    $base64 = [Convert]::ToBase64String($imageBytes)

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method = "base64"
        body   = $base64
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

Ein wiederverwendbares Modul statt Copy-Paste

Sobald mehrere Skripte auf einem Server CAPTCHAs lösen, gehört die Logik in ein Modul. Die folgende Klasse bündelt Übermittlung, Polling und Guthabenabfrage; Intervall und Timeout sind Eigenschaften. Speichern Sie die Datei als CaptchaAI.psm1 in einem versionierten Verzeichnis, das GitLab CI oder Ansible verteilt.

class CaptchaAISolver {
    [string]$ApiKey
    [string]$BaseUrl = "https://ocr.captchaai.com"
    [int]$PollInterval = 5
    [int]$MaxWait = 300

    CaptchaAISolver([string]$apiKey) {
        $this.ApiKey = $apiKey
    }

    [string] SolveRecaptchaV2([string]$siteUrl, [string]$siteKey) {
        return $this.Solve(@{
            method    = "userrecaptcha"
            googlekey = $siteKey
            pageurl   = $siteUrl
        })
    }

    [string] SolveTurnstile([string]$siteUrl, [string]$siteKey) {
        return $this.Solve(@{
            method  = "turnstile"
            key     = $siteKey
            pageurl = $siteUrl
        })
    }

    [string] SolveImage([string]$imagePath) {
        $bytes = [System.IO.File]::ReadAllBytes($imagePath)
        $base64 = [Convert]::ToBase64String($bytes)
        return $this.Solve(@{
            method = "base64"
            body   = $base64
        })
    }

    [double] GetBalance() {
        $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/res.php" `
            -Body @{ key = $this.ApiKey; action = "getbalance"; json = 1 }
        return [double]$response.request
    }

    hidden [string] Solve([hashtable]$params) {
        $taskId = $this.Submit($params)
        return $this.Poll($taskId)
    }

    hidden [string] Submit([hashtable]$params) {
        $body = @{ key = $this.ApiKey; json = 1 } + $params
        $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/in.php" `
            -Method Post -Body $body
        if ($response.status -ne 1) { throw "Submit: $($response.request)" }
        return $response.request
    }

    hidden [string] Poll([string]$taskId) {
        $deadline = (Get-Date).AddSeconds($this.MaxWait)
        while ((Get-Date) -lt $deadline) {
            Start-Sleep -Seconds $this.PollInterval
            $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/res.php" `
                -Body @{ key = $this.ApiKey; action = "get"; id = $taskId; json = 1 }
            if ($response.request -eq "CAPCHA_NOT_READY") { continue }
            if ($response.status -ne 1) { throw "Solve: $($response.request)" }
            return $response.request
        }
        throw "Timeout"
    }
}

# Export
Export-ModuleMember

Modul einbinden und Guthaben prüfen

using module lädt die Klasse, danach genügt eine Zeile pro Lösung. Die Guthabenabfrage gehört an den Anfang jedes geplanten Laufs – ein nachts an ERROR_ZERO_BALANCE gescheitertes Skript fällt sonst erst morgens auf.

using module .\CaptchaAI.psm1

$solver = [CaptchaAISolver]::new("YOUR_API_KEY")

# Check balance
$balance = $solver.GetBalance()
Write-Host "Balance: `$$balance"

# Solve reCAPTCHA v2
$token = $solver.SolveRecaptchaV2("https://example.com/login", "SITEKEY")
Write-Host "Token: $($token.Substring(0, 50))..."

Token in das Formular eintragen und absenden

Das Token ist nur ein weiterer Formularwert: bei reCAPTCHA heißt das Feld g-recaptcha-response, bei Turnstile cf-turnstile-response. Zwei Punkte entscheiden über den Erfolg – lösen Sie unmittelbar vor dem Absenden, denn Tokens gelten nur rund zwei Minuten, und verwenden Sie jedes Token genau einmal.

function Submit-FormWithToken {
    param(
        [string]$Url,
        [string]$Token,
        [hashtable]$FormData
    )

    $body = $FormData + @{
        "g-recaptcha-response" = $Token
    }

    $response = Invoke-WebRequest -Uri $Url `
        -Method Post `
        -Body $body `
        -ContentType "application/x-www-form-urlencoded"

    return $response
}

# Usage
$token = Solve-RecaptchaV2 -ApiKey "YOUR_API_KEY" `
    -SiteUrl "https://example.com/login" `
    -SiteKey "SITEKEY"

$result = Submit-FormWithToken `
    -Url "https://example.com/login" `
    -Token $token `
    -FormData @{
        username = "[email protected]"
        password = "password"
    }

Write-Host "Response: $($result.StatusCode)"

Mehrere CAPTCHAs parallel lösen – und was Ihr Tarif hergibt

Ein Polling besteht überwiegend aus Warten, deshalb skaliert der Durchsatz fast linear mit der Parallelität. Start-Job startet je Zielseite einen Prozess, Wait-Job und Receive-Job sammeln die Ergebnisse ein.

Die Obergrenze setzt nicht PowerShell, sondern Ihr Tarif. CaptchaAI rechnet Thread-basiert ab, nicht pro Lösung: BASIC 15 $/Monat mit 5 Threads, ADVANCE 90 $/Monat mit 50 Threads, ENTERPRISE 300 $/Monat mit 200 Threads – jeweils mit unbegrenzten Lösungen je Thread. Ein Thread ist eine laufende Abfrage; überzählige Jobs quittiert die API mit ERROR_NO_SLOT_AVAILABLE. Preise in US-Dollar.

$apiKey = "YOUR_API_KEY"

$tasks = @(
    @{ Url = "https://site-a.com"; Key = "SITEKEY_A" },
    @{ Url = "https://site-b.com"; Key = "SITEKEY_B" },
    @{ Url = "https://site-c.com"; Key = "SITEKEY_C" }
)

$jobs = $tasks | ForEach-Object {
    $task = $_
    Start-Job -ScriptBlock {
        param($ApiKey, $Url, $SiteKey)

        $taskId = (Invoke-RestMethod -Uri "https://ocr.captchaai.com/in.php" -Method Post -Body @{
            key = $ApiKey; json = 1; method = "userrecaptcha"
            googlekey = $SiteKey; pageurl = $Url
        }).request

        $deadline = (Get-Date).AddSeconds(300)
        while ((Get-Date) -lt $deadline) {
            Start-Sleep -Seconds 5
            $result = Invoke-RestMethod -Uri "https://ocr.captchaai.com/res.php" -Body @{
                key = $ApiKey; action = "get"; id = $taskId; json = 1
            }
            if ($result.request -ne "CAPCHA_NOT_READY" -and $result.status -eq 1) {
                return @{ Url = $Url; Token = $result.request }
            }
        }
        return @{ Url = $Url; Error = "Timeout" }
    } -ArgumentList $apiKey, $task.Url, $task.Key
}

# Wait and collect results
$results = $jobs | Wait-Job | Receive-Job
$results | ForEach-Object {
    if ($_.Token) {
        Write-Host "$($_.Url): $($_.Token.Substring(0, 50))..."
    } else {
        Write-Host "$($_.Url): $($_.Error)" -ForegroundColor Red
    }
}
$jobs | Remove-Job

Wiederholungslogik für den unbeaufsichtigten Betrieb

Nicht jeder Fehler verdient einen zweiten Versuch. ERROR_NO_SLOT_AVAILABLE und ERROR_CAPTCHA_UNSOLVABLE sind vorübergehend – hier hilft exponentielles Backoff mit Zufallsanteil, damit mehrere Skripte nicht im Gleichtakt anfragen. ERROR_WRONG_USER_KEY und ERROR_ZERO_BALANCE sollten dagegen sofort durchschlagen.

function Solve-WithRetry {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [hashtable]$TaskParams,

        [int]$MaxRetries = 3
    )

    $retryableErrors = @(
        "ERROR_NO_SLOT_AVAILABLE",
        "ERROR_CAPTCHA_UNSOLVABLE"
    )

    for ($attempt = 0; $attempt -le $MaxRetries; $attempt++) {
        if ($attempt -gt 0) {
            $delay = [Math]::Pow(2, $attempt) + (Get-Random -Maximum 3)
            Write-Host "Retry $attempt/$MaxRetries after $($delay)s..."
            Start-Sleep -Seconds $delay
        }

        try {
            $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams $TaskParams
            $result = Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
            return $result
        }
        catch {
            $errorMsg = $_.Exception.Message
            $isRetryable = $retryableErrors | Where-Object { $errorMsg -like "*$_*" }

            if (-not $isRetryable -or $attempt -eq $MaxRetries) {
                throw
            }
            Write-Warning "Retryable error: $errorMsg"
        }
    }
}

Im Aufgabenplaner produktiv betreiben

Ein Beispiel aus dem DACH-Alltag: Ein Shop-Team betreibt seine Shopware-Staging-Instanz auf einem Windows-Server bei Hetzner oder IONOS. Nachts prüft ein PowerShell-Skript Login und Bestellstrecke der eigenen Testumgebung; seit das Formular durch Turnstile geschützt ist, löst dasselbe Skript die Abfrage über die API.

Für den Dauerbetrieb haben sich drei Punkte bewährt:

  • Schlüssel nicht im Skript. API-Schlüssel als Umgebungsvariable des Dienstkontos oder in Microsoft.PowerShell.SecretManagement ablegen.
  • Ausführungskontext klären. Aufgabe mit eigenem Dienstkonto und der Option „Unabhängig von der Benutzeranmeldung ausführen“ registrieren.
  • Datensparsam protokollieren. Tokens und Anmeldedaten gehören nicht in Klartext-Logs; wer fremde Systeme abfragt, klärt Rechtsgrundlage und Umgang mit IP-Adressen nach DSGVO vorab.
# Create a scheduled task that runs CAPTCHA automation daily
$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-ExecutionPolicy Bypass -File C:\Scripts\captcha-automation.ps1"

$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"

Register-ScheduledTask `
    -TaskName "CaptchaAutomation" `
    -Action $action `
    -Trigger $trigger `
    -Description "Run daily CAPTCHA automation mit CaptchaAI"

Typische Fehler und ihre Ursache

Meldung Ursache Lösung
ERROR_WRONG_USER_KEY Schlüssel falsch kopiert Schlüssel im Dashboard prüfen
ERROR_ZERO_BALANCE Guthaben aufgebraucht Konto aufladen, Guthabenabfrage an den Skriptanfang
ERROR_NO_SLOT_AVAILABLE Mehr Jobs als Threads Parallelität an den Tarif anpassen
Invoke-RestMethod: SSL/TLS 5.1 handelt noch TLS 1.0 aus [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 setzen
The response content cannot be parsed Antwort ist kein JSON Mit Invoke-WebRequest den Rohtext auswerten
Execution policy-Fehler Ausführungsrichtlinie blockiert Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
Token wird abgelehnt Abgelaufen oder pageurl weicht ab Kurz vor dem Absenden lösen, URL exakt übergeben

Häufige Fragen

Wie viele CAPTCHAs kann ich gleichzeitig lösen?

So viele, wie Ihr Tarif Threads umfasst – von 5 bei BASIC bis 5.000 bei VIP-3. Mehr Start-Job-Instanzen als Threads bringen keinen Durchsatz.

Wie lange ist ein gelöstes Token gültig?

Etwa zwei Minuten. Lösen Sie deshalb erst, wenn das Formular fertig befüllt ist, und senden Sie unmittelbar danach ab.

Welche CAPTCHA-Typen deckt die API ab?

reCAPTCHA v2 und v3 (inklusive Invisible und Enterprise), Cloudflare Turnstile und Challenge, GeeTest v3, Bild- und Rasterbild-CAPTCHAs sowie BLS; CaptchaFox, Friendly Captcha und Lemin in der Beta. hCaptcha und FunCaptcha werden nicht unterstützt, GeeTest v4 gilt als „bald verfügbar“.

Wie hinterlege ich den API-Schlüssel in einer geplanten Aufgabe?

Über Microsoft.PowerShell.SecretManagement oder eine Umgebungsvariable des ausführenden Dienstkontos. So steht der Schlüssel weder im Skript noch in der Aufgabendefinition und lässt sich rotieren, ohne den Code anzufassen.

Warum meldet Windows PowerShell 5.1 einen TLS-Fehler?

Weil 5.1 ältere Protokollversionen aushandelt. Setzen Sie [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 an den Skriptanfang; in PowerShell 7 entfällt das.


Verwandte Leitfäden


Automatisieren Sie CAPTCHA-Abfragen direkt aus Ihren Windows-Skripten – mit CaptchaAI.

Kommentare sind für diesen Artikel deaktiviert.