Cara Memecahkan CAPTCHA AWS WAF di PHP dengan File Cookie cURL

Untuk memecahkan CAPTCHA AWS WAF di PHP, ambil halaman dengan cURL, ambil lima nilai dari balasan HTTP 405, serahkan ke amazonWaf di pemecah AWS WAF CapSkip, tukarkan pasangan voucher yang Anda terima dengan aws-waf-token, dan tulis token itu ke file cookie. Proses PHP melupakan semuanya saat berakhir, jadi file itulah tempat token disimpan. CapSkip berjalan di PC Windows Anda sendiri dan tidak menarik biaya per pemecahan.
Apa yang Anda butuhkan
- PC Windows dengan CapSkip 3.0.0 atau lebih baru, versi yang memperkenalkan dukungan AWS WAF.
- PHP 8.0 atau lebih baru dengan ekstensi curl, Composer, dan capskip/capskip 1.4.0 atau lebih baru, tempat amazonWaf pertama kali muncul. Semua yang ada di bawah dijalankan di PHP 8.0.30, 8.1.33, 8.2.29, 8.3.28, 8.4.15, dan 8.5.0 (build WampServer untuk Windows) dengan capskip 1.6.0, dan di 8.0.30 dan 8.5.0 dengan 1.5.0 juga. Versi 1.6.0 menambahkan hcaptcha dan membiarkan amazonWaf serta lapisan HTTP seperti semula.
- Sebuah CA bundle. Pada keenam build, dengan curl.cainfo tidak disetel, permintaan https ke situs publik gagal dengan error 60, SSL certificate problem: unable to get local issuer certificate. Tentukan file CA di curl.cainfo dalam php.ini sebelum Anda mulai.
- Prompt PowerShell, dan halaman milik Anda atau yang boleh Anda otomatisasi di balik aturan AWS WAF dengan action CAPTCHA.
- Sebuah alamat untuk CapSkip. Mode Local melayani 127.0.0.1 di PC yang menjalankannya. Mode Server membuka API ke jaringan atau IP publik Anda, sehingga PHP di mesin lain dapat memanggilnya. Pilih salah satu di pengaturan koneksi.
Cara pengujian: sebuah mock yang saya tulis, dan bukan situs AWS WAF sungguhan. Mock ini menjawab sebagai CapSkip dan sebagai halaman terlindungi, dengan menerapkan pemeriksaan yang tercantum di bagian AWS WAF pada Dokumentasi API. Listener kedua menjawab sebagai host token AWS lewat TLS, dijangkau melalui variabel https_proxy, dengan certificate authority sekali pakai di curl.cainfo agar pemeriksaan sertifikat tetap aktif. Tidak ada angka di sini yang merupakan waktu pemecahan, dan setiap aturan yang diterapkan mock adalah aturan saya, bukan perilaku AWS yang terdokumentasi.
Langkah 1: instal SDK dan baca halaman 405
Buat folder dan instal SDK:
mkdir waf-demo cd waf-demo composer require capskip/capskip
Selanjutnya buat waf.php. Langkah 1 sampai 4 semuanya masuk ke satu file itu, secara berurutan. Setel PAGE_URL ke halaman Anda sendiri terlebih dahulu, karena example.com tidak memiliki aturan CAPTCHA.
Setiap permintaan mendapat handle cURL baru, yang dibebaskan saat fungsi selesai. Pengambilan halaman menyetel CURLOPT_COOKIEFILE, sehingga cURL mengirim token apa pun yang dimiliki jar, dan sengaja tidak menyetel CURLOPT_COOKIEJAR (lihat FAQ). Menurut dokumentasi action CAPTCHA AWS, halaman puzzle disertakan jika permintaan memiliki header Accept dengan nilai text/html, jadi helper mengirim persis itu. cURL mengirim */* secara default, dan apakah AWS menganggap */* sebagai text/html tidak bisa saya uji. Mock saya mengirim 405 kosong.
<?php
// waf.php: Steps 1 to 4 go into this one file, in order.
require __DIR__ . '/vendor/autoload.php';
const PAGE_URL = 'https://www.example.com/'; // the page that answers HTTP 405
const JAR = __DIR__ . '/waf-cookies.txt'; // the token store, shared by every PHP process
// One cURL handle per request, freed when the function returns.
function request(string $url, array $options = []): array
{
$ch = curl_init($url);
curl_setopt_array($ch, $options + [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0 Safari/537.36',
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException(curl_error($ch));
}
return [curl_getinfo($ch, CURLINFO_RESPONSE_CODE), $body];
}
// A page fetch only reads the jar, so it sends whatever token is in the file.
function fetch(string $url): array
{
return request($url, [
CURLOPT_COOKIEFILE => JAR,
CURLOPT_HTTPHEADER => ['Accept: text/html'], // cURL sends */* unless told otherwise
]);
}Aturan CAPTCHA yang cocok membuat AWS WAF membalas 405 dengan header x-amzn-waf-action disetel ke captcha. Body-nya menyetel window.gokuProps ke sebuah objek dengan entri key, iv, dan context, serta memuat challenge.js dan captcha.js dari host awswaf.com. Contoh SDK menyebut bahwa gokuProps.iv dan gokuProps.context menjadi basi dalam hitungan menit, jadi loop mengambil halaman lagi sebelum setiap pemecahan. Fungsi mengembalikan null untuk respons lain apa pun.
// The five values on an AWS WAF CAPTCHA page, or null for any other response.
function read_challenge(int $status, string $html): ?array
{
if ($status !== 405 || !str_contains($html, 'gokuProps')) {
return null;
}
preg_match('/gokuProps\s*=\s*\{(.*?)\}/s', $html, $block);
$field = fn (string $name): ?string =>
preg_match('/"' . $name . '"\s*:\s*"([^"]+)"/', $block[1] ?? '', $m) ? $m[1] : null;
$script = fn (string $name): ?string =>
preg_match('/<script[^>]+src="([^"]+\/' . $name . '\.js)"/', $html, $m) ? $m[1] : null;
$found = [
'sitekey' => $field('key'),
'iv' => $field('iv'),
'context' => $field('context'),
'challenge_script' => $script('challenge'),
'captcha_script' => $script('captcha'),
];
return in_array(null, $found, true) ? null : $found;
}Langkah 2: panggil amazonWaf
Signature-nya adalah amazonWaf(string $sitekey, string $url, array $options = []). Sitekey adalah gokuProps.key pada halaman challenge. Nilai dari gokuProps.iv dan gokuProps.context dimasukkan bersama atau tidak sama sekali, dan ketika keduanya tidak ada, CapSkip membacanya dari halaman saat pemecahan dimulai. SDK memvalidasi setiap argumen sebelum permintaan meninggalkan PHP, jadi nilai yang buruk gagal seketika dan mock tidak pernah melihatnya. Tabel error mencantumkan kasus yang saya coba.
Pemanggilan yang berhasil mengembalikan array dengan captchaId, captcha_voucher, dan existing_token, ditambah jawaban mentah di entri code pada hasil. Ini bukan token: pasangan voucher harus ditukar terlebih dahulu. Waktu tunggu default adalah 300 detik, karena CapSkip memecahkan di browser sungguhan. Semua exception SDK mewarisi CapSkipError, dan ApiException membawa teks error dari CapSkip. Proxy dimasukkan sebagai opsi proxy, berupa array dengan type HTTP dan uri user:[email protected]:8080, yang sampai ke mock sebagai proxy dan proxytype. Arahkan permintaan Anda sendiri melalui proxy yang sama, hal yang tidak bisa saya uji.
use CapSkip\CapSkip;
// 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, array $challenge): array
{
// The answer is a voucher pair, not a token.
return $solver->amazonWaf($challenge['sitekey'], PAGE_URL, [
'iv' => $challenge['iv'],
'context' => $challenge['context'],
'challenge_script' => $challenge['challenge_script'],
'captcha_script' => $challenge['captcha_script'],
]);
}Langkah 3: tukarkan voucher dengan token dan simpan
Ambil folder tempat challenge.js dimuat dan tambahkan voucher. Itulah endpoint-nya, dan langkah ini adalah permintaan Anda sendiri, jadi AWS melihat alamat PHP Anda di sana. Kirim pasangan itu sebagai body JSON dan baca field token pada balasannya. Dokumentasi CapSkip memakai body text/plain ditambah header Origin, dan aplikasi CapSkip menambahkan Referer, jadi fungsi mengirim ketiganya. Untuk body berupa string, cURL menyatakan form post kecuali diberi tahu lain, hal yang dikonfirmasi mock ketika saya menghapus baris header itu. Dokumentasi SDK menyebut pasangan itu berumur pendek, jadi tukarkan segera.
Pemanggilan ini juga mengikuti redirect, karena source aplikasi CapSkip mencatat bahwa host token regional berbentuk id.region.sdk.awswaf.com melakukan redirect ke host ber-hash. Mock saya mengirim 307 untuk host seperti itu dan pemanggilan mengikutinya. Tanpa opsi itu, pemanggilan gagal dengan HTTP 307. Saya tidak mencoba kode lain.
// Trade the voucher pair for the aws-waf-token value.
function exchange(array $result, array $challenge): string
{
// The voucher endpoint sits beside challenge.js, same folder.
$script = $challenge['challenge_script'];
$voucherUrl = substr($script, 0, strrpos($script, '/')) . '/voucher';
$site = parse_url(PAGE_URL);
$origin = $site['scheme'] . '://' . $site['host'] . (isset($site['port']) ? ':' . $site['port'] : '');
[$status, $body] = request($voucherUrl, [
CURLOPT_POST => true,
CURLOPT_FOLLOWLOCATION => true, // a regional host may redirect to the real one
CURLOPT_POSTFIELDS => json_encode([
'captcha_voucher' => $result['captcha_voucher'],
'existing_token' => $result['existing_token'],
]),
CURLOPT_HTTPHEADER => [
'Content-Type: text/plain;charset=UTF-8', // PHP would send a form type otherwise
'Origin: ' . $origin,
'Referer: ' . $origin . '/',
],
]);
if ($status !== 200) {
throw new RuntimeException("voucher exchange failed: HTTP $status");
}
return json_decode($body, true, 512, JSON_THROW_ON_ERROR)['token'];
}Jar adalah file teks berformat Netscape: satu cookie per baris, field dipisahkan tab. Fungsi simpan adalah satu-satunya kode yang menulisnya. Fungsi itu mempertahankan setiap baris lain, mengganti token lama, dan menulis di bawah exclusive lock. Barisnya host-only, jadi cURL mengirim cookie ke host di PAGE_URL dan tidak ke host lain. Dalam sebuah probe dengan nama www dan api yang diarahkan ke satu mesin, baris host-only sampai ke www tetapi tidak ke api, sedangkan baris dengan titik di depan dan field subdomain disetel TRUE sampai ke keduanya. Masa berlaku adalah 0 karena saya tidak tahu immunity time situs itu.
Pembaca bisa menangkap file di tengah penulisan. Dengan satu proses menyimpan 400 kali dan proses lain mengirim 400 permintaan, 2 permintaan di 8.4.15 dan 1 di 8.0.30 keluar tanpa cookie. Menulis file sementara lalu mengganti namanya tidak membantu di Windows: 5 dan 4 permintaan masih tidak memiliki cookie, dan penggantian nama itu sendiri gagal 1 dan 7 kali dari 400. Perlakukan 405 yang berdiri sendiri sebagai alasan untuk mencoba ulang.
// The only writer of the jar. It keeps every other line and replaces the old token.
function save_token(string $token): void
{
$lines = is_file(JAR) ? file(JAR, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
$lines = array_filter($lines, fn (string $line) => !str_contains($line, "\taws-waf-token\t"));
// Netscape format: host, subdomains, path, secure, expiry (0 = session), name, value
$lines[] = parse_url(PAGE_URL, PHP_URL_HOST) . "\tFALSE\t/\tFALSE\t0\taws-waf-token\t" . $token;
file_put_contents(JAR, implode("\n", $lines) . "\n", LOCK_EX);
}Langkah 4: satukan semuanya di balik lock
Blok terakhir adalah seluruh alurnya. Jalankan dengan php waf.php. Pengambilan pertamanya mengirim token dari jar, jadi token yang valid berarti CapSkip tidak pernah dihubungi. Hanya 405 yang masuk ke jalur pemecahan, dan jalur itu dimulai dengan lock, bagian yang hanya masuk akal di PHP. Dua skrip yang dimulai oleh scheduler berbagi jar dan tidak berbagi memori, jadi keduanya akan melihat 405 dan keduanya akan memecahkan. Dengan flock, skrip kedua menunggu, mengambil lagi, menemukan token milik skrip pertama, lalu kembali. PHP melepas lock saat handle dibebaskan ketika fungsi berakhir, dan satu kali jalan paling banyak melakukan tiga percobaan.
use CapSkip\Exceptions\ApiException;
function fetch_protected(CapSkip $solver): array
{
[$status, $html] = fetch(PAGE_URL); // sends the token in the jar, if there is one
if (read_challenge($status, $html) === null) {
return [$status, $html];
}
$lock = fopen(JAR . '.lock', 'c');
flock($lock, LOCK_EX); // one solve at a time across processes, released when $lock is freed
for ($attempt = 1; ; $attempt++) {
[$status, $html] = fetch(PAGE_URL); // the process we waited for may have saved a token
$challenge = read_challenge($status, $html);
if ($challenge === null) {
return [$status, $html];
}
if ($attempt > 3) {
throw new RuntimeException('AWS WAF CAPTCHA not solved in 3 attempts');
}
try {
save_token(exchange(solve($solver, $challenge), $challenge)); // exchange at once
} catch (ApiException $e) {
echo "attempt $attempt: {$e->getMessage()}\n"; // each retry draws a new puzzle
}
}
}
[$status, $body] = fetch_protected($solver);
echo $status, ' ', substr($body, 0, 60), "\n";Hasil run mock, identik di keenam versi PHP:
| Pengaturan mock | Yang dilakukan skrip |
|---|---|
| Jar kosong, setiap puzzle dijawab | Tepat satu task dan satu penukaran, berakhir dengan 200 |
| Run kedua, jar yang sama | 200 tanpa task, karena permintaan pertama sudah membawa cookie |
| Dua puzzle pertama ditolak | Dua pesan percobaan ulang, berhasil pada task ketiga |
| Tiga puzzle pertama ditolak | Tiga pesan percobaan ulang, RuntimeException yang tidak tertangkap, exit code 255, tanpa permintaan voucher |
| Dua skrip dimulai bersamaan, puzzle ditahan 2 detik | Dengan lock, satu task dan keduanya berakhir dengan 200. Tanpa baris flock, dua task atau lebih |
Diulang 10 kali per varian di 8.0.30 dan 8.4.15, lock menghasilkan satu task pada semua 20 run. Tanpa lock, 19 dari 20 run menghasilkan dua task dan satu run di 8.4.15 menghasilkan tiga. Token tetap berlaku sampai immunity time situs habis, yang menurut Dokumentasi API CapSkip adalah lima menit secara default. Run di dalam jendela itu mengirim cookie dan melewati CapSkip, jadi job terjadwal bisa memanggil skrip ini setiap beberapa menit.
Jangan memecahkan di dalam permintaan web
Satu pemecahan bisa lebih lama daripada permintaan web PHP. Dokumentasi API menyebut sebagian besar task siap dalam 15 detik dan proxy yang lambat menambah waktu itu, sedangkan manual PHP memberi max_execution_time nilai default 30 detik untuk penggunaan web dan tanpa batas untuk command line. Saya menjalankan seluruh skrip di bawah php-cgi.exe, sebuah SAPI web, di 8.0.30 dan 8.4.15, terhadap mock yang menahan setiap puzzle selama 40 detik simulasi. Skrip mati dengan fatal error Maximum execution time of 30 seconds exceeded setelah sekitar 33 detik, karena PHP melempar error itu hanya ketika jeda polling di dalam SDK berakhir. Di bawah php.exe skrip yang sama memakan sekitar 43 detik.
Timeout SDK yang lebih pendek tidak menyelamatkannya. Dengan timeout 20, SDK melakukan polling pada sekitar 0.25, 0.75, 1.75, 3.75, 7.75, 12.75, dan 17.75 detik, lalu tidur sampai 22.75 dan menyerah. Di 8.0.30 dan 8.4.15, puzzle yang siap setelah 16 atau 17.5 detik terambil pada sekitar 18 detik, tetapi yang siap setelah 19 detik terlewat: pemanggilan berakhir dengan TimeoutException pada sekitar 23 detik dan tidak pernah membaca jawabannya. Dengan batas atas polling default 5 detik, timeout 20 hanya menangkap jawaban yang siap pada sekitar 17.75 detik.
Jaga sisi web tetap read-only dan serahkan pemecahan kepada php waf.php yang dijalankan sesuai jadwal. Blok ini menggantikan dua baris terakhir pada salinan web dari skrip. Dalam uji php-cgi di 8.4.15 dengan puzzle mock 40 detik, dua permintaan web yang dikirim saat job sedang memecahkan mendapat 503 dengan header Retry-After dalam 0.25 detik. Job selesai setelah sekitar 43 detik dengan satu task dan satu penukaran, dan permintaan web berikutnya menerima halaman yang sebenarnya.
// Web copy of waf.php: replace its last two lines with this read-only check.
// Solving stays with "php waf.php", run from a scheduled task.
[$status, $html] = fetch(PAGE_URL);
if (read_challenge($status, $html) !== null) {
http_response_code(503);
header('Retry-After: 60');
exit('No valid token yet, try again soon.');
}
http_response_code($status);
echo substr($html, 0, 60);Kesalahan umum
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| ValidationException | Dilempar di dalam SDK sebelum permintaan apa pun: gokuProps.iv atau gokuProps.context dikirim sendirian, alamat skrip yang bukan https di host awswaf.com, file skrip dengan nama yang salah, URL halaman relatif, atau opsi yang tidak dikenal | Perbaiki apa pun yang disebut pesannya |
| ApiException dengan ERROR_CAPTCHA_UNSOLVABLE di dalam pesannya | Puzzle itu tidak dapat dijawab. Setiap task baru mendapat puzzle yang berbeda, menurut Dokumentasi API | Tidak perlu apa pun: loop mengambil halaman baru dan mengirim lagi |
| NetworkException | Tidak ada yang mendengarkan di host dan port itu. Port lokal yang tertutup gagal setelah sekitar 2 detik | Jalankan CapSkip dan periksa kembali host dan port |
| TimeoutException | Waktu tunggu berakhir tanpa jawaban. Exception ini mewarisi CapSkipError, bukan ApiException, jadi loop percobaan ulang membiarkannya lewat | Pastikan CapSkip sedang memecahkan, atau tangkap di tempat Anda ingin menyerah |
| ApiException tidak tertangkap padahal Anda menangkapnya | Setelah baris use untuk CapSkip\CapSkip, catch yang ditulis sebagai CapSkip\Exceptions\ApiException menyebut class yang tidak ada | Impor exception itu, atau awali namanya dengan backslash |
| RuntimeException: SSL certificate problem: unable to get local issuer certificate | curl.cainfo tidak disetel, sehingga PHP tidak dapat memverifikasi sertifikat pada permintaan https pertama, baik pengambilan halaman maupun pemanggilan voucher | Arahkan curl.cainfo di php.ini ke file CA |
| voucher exchange failed: HTTP 400 | Source aplikasi CapSkip menganggap 400 dari endpoint voucher sebagai existing_token yang null atau tidak valid, penyebab sebenarnya yang paling mungkin. Mock saya mengembalikan 400 untuk pasangan yang tidak pernah diterbitkannya, dan saya tidak bisa mereproduksi penolakan yang sebenarnya | Ulangi putaran dengan task baru dan tukarkan segera |
| Baris Deprecated yang menyebut curl_close di PHP 8.5 | File Http.php di dalam SDK masih memanggil curl_close, yang tidak lagi dianjurkan di PHP 8.5.0. File itu mencetak satu baris per permintaan ke CapSkip, dan pemecahan tetap berfungsi | Jangan sertakan E_DEPRECATED di error_reporting sampai SDK menghapus pemanggilan itu |
FAQ
Mengapa tidak menyetel CURLOPT_COOKIEJAR pada setiap pengambilan halaman?
Handle menulis jar-nya saat PHP membebaskannya, dan handle yang membaca file sebelum token disimpan menulis kembali isi lamanya. Dalam sebuah probe, token hilang dari file begitu handle seperti itu dibebaskan, di keenam versi. Jika situs Anda menyetel cookie yang harus dipertahankan, berikan jar terpisah untuk cookie itu.
Apa bedanya halaman widget dengan ini?
Halaman widget adalah form biasa yang memuat jsapi.js dan menggambar puzzle sendiri. SDK menerima apiKey milik widget sebagai sitekey dan alamat jsapi.js sebagai opsi jsapiScript, tanpa iv atau context. Tulisan ini hanya menguji halaman 405, jadi pembaca di Langkah 1 perlu ditulis ulang untuk widget.
Bisakah PHP di mesin lain memakai CapSkip saya dalam mode Server?
Bisa. Dalam mode Server, CapSkip mendengarkan di jaringan atau IP publik Anda, sehingga PHP di PC kedua, VPS, atau platform hosted memanggil API yang sama dengan host dan port yang berbeda di constructor, dan tidak ada hal lain yang berubah. IP publik statis menjaga akses jarak jauh tetap stabil. Perangkat kerasnya tetap milik Anda dan tanpa meteran. Mode Server membuka port CapSkip untuk semua alamat, jadi aturan blokir Windows Firewall untuk mesin yang tidak Anda harapkan adalah yang menjaganya tetap pribadi. Penukaran voucher berjalan di tempat PHP berjalan, bukan di mesin CapSkip, dan saya tidak menguji apakah AWS mempermasalahkannya.
CAPTCHA AWS WAF di PHP intinya adalah file cookie yang menyimpan token dan lock yang membuat hanya satu proses memecahkan pada satu waktu. Karena CapSkip adalah sebuah pemecah captcha di mesin Anda sendiri, puzzle yang tidak dapat dijawab hanya memakan satu percobaan ulang dan tidak memakan uang. Versi Python dari alur ini ada di panduan Python, dan SDK yang lebih luas dibahas di halaman pemecah CAPTCHA PHP.
