PHP ve cURL Çerez Dosyası ile AWS WAF CAPTCHA Nasıl Çözülür

PHP ile bir AWS WAF CAPTCHA’sını çözmek için sayfayı cURL ile çekin, HTTP 405 yanıtından beş değer çıkarın ve bunları CapSkip AWS WAF çözücüsü içindeki amazonWaf metoduna verin, dönen voucher çiftini bir aws-waf-token ile değiştirin ve bu token’ı bir çerez dosyasına yazın. Bir PHP süreci çıkarken her şeyi unutur, bu yüzden token bu dosyada yaşar. CapSkip kendi Windows bilgisayarınızda çalışır ve çözüm başına ücret almaz.
Neye ihtiyacınız var
- CapSkip 3.0.0 veya üzeri çalışan bir Windows bilgisayarı; AWS WAF desteğini getiren sürüm.
- curl eklentisiyle PHP 8.0 veya üzeri, Composer ve amazonWaf metodunun ilk göründüğü capskip/capskip 1.4.0 veya üzeri. Aşağıdaki her şey PHP 8.0.30, 8.1.33, 8.2.29, 8.3.28, 8.4.15 ve 8.5.0 (Windows için WampServer derlemeleri) üzerinde capskip 1.6.0 ile, ayrıca 8.0.30 ve 8.5.0 üzerinde 1.5.0 ile de çalıştı. 1.6.0 sürümü hcaptcha ekledi ve amazonWaf ile HTTP katmanını olduğu gibi bıraktı.
- Bir CA bundle. Altı derlemenin hepsinde, curl.cainfo ayarlanmamışken, herkese açık bir siteye yapılan https isteği 60 numaralı hatayla başarısız oldu: SSL certificate problem: unable to get local issuer certificate. Başlamadan önce php.ini içindeki curl.cainfo ayarında bir CA dosyası belirtin.
- Bir PowerShell istemi ve CAPTCHA eylemli bir AWS WAF kuralının arkasında, sahibi olduğunuz ya da otomatikleştirmenize izin verilen bir sayfa.
- CapSkip için bir adres. Local modu, onu çalıştıran bilgisayarda 127.0.0.1 üzerinden hizmet verir. Server modu API’yi ağınıza ya da genel IP’nize açar, böylece başka bir makinedeki PHP onu çağırabilir. Birini şuradan seçin: bağlantı ayarları.
Nasıl test edildi: benim yazdığım bir mock; gerçek bir AWS WAF sitesi yok. CapSkip olarak ve korumalı sayfa olarak yanıt verir. Denetimleri API belgeleri içindeki AWS WAF bölümünde listelendiği gibi uygular. İkinci bir dinleyici, TLS üzerinden AWS’nin token host’u gibi yanıt verir; https_proxy değişkeni üzerinden erişilir ve curl.cainfo içinde geçici bir sertifika yetkilisi vardır, böylece sertifika denetimi açık kaldı. Buradaki hiçbir şey bir çözüm süresi değildir ve mock’un uyguladığı her kural AWS’nin belgelenmiş davranışı değil, benimdir.
1. Adım: SDK’yı kurun ve 405 sayfasını okuyun
Bir klasör oluşturun ve SDK’yı kurun:
mkdir waf-demo cd waf-demo composer require capskip/capskip
Sonra waf.php dosyasını oluşturun. 1 ile 4 arasındaki adımların hepsi sırayla bu tek dosyaya girer. example.com üzerinde CAPTCHA kuralı olmadığı için önce PAGE_URL değerini kendi sayfanıza ayarlayın.
Her istek yeni bir cURL handle’ı alır; fonksiyon dönünce serbest bırakılır. Sayfa istekleri CURLOPT_COOKIEFILE ayarlar, böylece cURL jar’ın tuttuğu token’ı gönderir; bilerek CURLOPT_COOKIEJAR ayarlamazlar (bkz. SSS). AWS CAPTCHA eylemi belgeleri bulmaca sayfasının, değeri text/html olan bir Accept başlığı taşıyan isteğe dahil edildiğini söyler; bu yüzden yardımcı tam olarak onu gönderir. cURL varsayılan olarak */* gönderir ve AWS’nin */* değerini text/html sayıp saymadığını test edemedim. Mock’um boş bir 405 gönderdi.
<?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
]);
}Eşleşen bir CAPTCHA kuralı, AWS WAF’ın x-amzn-waf-action başlığı captcha olarak ayarlanmış bir 405 ile yanıt vermesine yol açar. Gövde window.gokuProps değerini key, iv ve context girdileri olan bir nesneye ayarlar ve bir awswaf.com host’undan challenge.js ve captcha.js yükler. SDK örneği gokuProps.iv ve gokuProps.context değerlerinin dakikalar içinde eskidiğini söyler; bu yüzden döngü her çözümden önce sayfayı yeniden çeker. Fonksiyon başka her yanıt için null döndürür.
// 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;
}2. Adım: amazonWaf çağrısı
İmza amazonWaf(string $sitekey, string $url, array $options = []) biçimindedir. Sitekey, bir challenge sayfasında gokuProps.key değeridir. gokuProps.iv ve gokuProps.context değerleri birlikte verilir ya da hiç verilmez; ikisi de eksikse CapSkip, çözüm başladığında onları sayfadan okur. SDK her argümanı PHP’den bir istek çıkmadan önce doğrular, bu yüzden hatalı bir değer hemen başarısız olur ve mock onu hiç görmez. Hatalar tablosu denediğim durumları listeler.
Başarılı bir çağrı; captchaId, captcha_voucher ve existing_token içeren bir dizi ile birlikte, ham yanıtı da sonucun code girdisi altında döndürür. Bu bir token değildir: önce voucher çiftini bir token ile değiştirmeniz gerekir. Varsayılan bekleme 300 saniyedir, çünkü CapSkip gerçek bir tarayıcıda çözer. Tüm SDK istisnaları CapSkipError sınıfını genişletir ve ApiException, CapSkip’ten gelen hata metnini taşır. Proxy, type değeri HTTP ve uri değeri user:[email protected]:8080 olan bir dizi olarak proxy seçeneğiyle verilir; bu mock’a bir proxy ve bir proxytype olarak ulaştı. Kendi isteklerinizi de aynı proxy üzerinden yönlendirin; bunu test edemedim.
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'],
]);
}3. Adım: voucher’ı token ile değiştirin ve kaydedin
challenge.js dosyasının yüklendiği klasörü alın ve sonuna voucher ekleyin. Uç nokta budur ve bu adım sizin kendi isteğinizdir, bu yüzden AWS onda PHP adresinizi görür. Çifti bir JSON gövdesi olarak gönderin ve yanıtın token alanını okuyun. CapSkip belgeleri bir text/plain gövdesi artı bir Origin başlığı kullanır ve CapSkip uygulaması bir Referer ekler; bu yüzden fonksiyon üçünü de gönderir. Bir dize gövdesi için cURL, aksi belirtilmedikçe bir form gönderimi bildirir; başlık satırını sildiğimde mock bunu doğruladı. SDK belgeleri çifti kısa ömürlü olarak niteler, bu yüzden hemen takas edin.
Çağrı yönlendirmeleri de izler, çünkü CapSkip uygulamasının kaynağı, id.region.sdk.awswaf.com biçimindeki bölgesel bir token host’unun karma (hash) host’a yönlendirdiğini not eder. Mock’um böyle bir host için 307 gönderdi ve çağrı onu izledi. Seçenek olmadan HTTP 307 ile başarısız oldu. Başka kod denemedim.
// 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, Netscape biçiminde bir metin dosyasıdır: satır başına bir çerez, alanlar sekmelerle ayrılmış. Kaydetme fonksiyonu onu yazan tek koddur. Diğer her satırı korur, eski token’ı değiştirir ve özel bir kilit altında yazar. Satır yalnızca host içindir (host-only), bu yüzden cURL çerezi PAGE_URL içindeki host’a gönderir, başkasına değil. www ve api adlarının tek bir makineyi gösterdiği bir denemede host-only satır www adına ulaştı ama api adına ulaşmadı; başında nokta olan ve alt alan adı alanı TRUE olan bir satır ikisine de ulaştı. Sitenin bağışıklık süresini (immunity time) bilmediğim için süre sonu 0 olarak ayarlı.
Bir okuyucu dosyayı yazım sırasında yakalayabilir. Bir süreç 400 kez kaydederken diğeri 400 istek gönderdiğinde, 8.4.15 üzerinde 2 istek ve 8.0.30 üzerinde 1 istek çerez olmadan gitti. Geçici bir dosya yazıp yeniden adlandırmak Windows’ta işe yaramadı: yine 5 ve 4 istekte çerez yoktu ve yeniden adlandırmanın kendisi 400 denemede 1 ve 7 kez başarısız oldu. Tek başına gelen bir 405’i yeniden deneme nedeni olarak değerlendirin.
// 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);
}4. Adım: hepsini bir kilidin arkasında birleştirin
Son blok akışın tamamıdır. Çalıştırmak için php waf.php komutunu kullanın. İlk çekme jar’daki token’ı gönderir, bu yüzden geçerli bir token, CapSkip’e hiç başvurulmaması demektir. Yalnızca bir 405 çözüm yoluna girer ve bu yol bir kilitle başlar; yalnızca PHP’de anlam taşıyan kısım budur. Bir zamanlayıcının başlattığı iki script jar’ı paylaşır, bellek paylaşmaz; bu yüzden ikisi de 405 görür ve ikisi de çözerdi. flock ile ikincisi bekler, yeniden çeker, birincisinin token’ını bulur ve döner. PHP, fonksiyon bitip handle serbest kaldığında kilidi bırakır; bir çalıştırma en fazla üç deneme yapar.
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";Mock çalıştırmasının sonuçları, altı PHP sürümünde de aynıydı:
| Mock ayarı | Script ne yaptı |
|---|---|
| Boş jar, her bulmaca yanıtlandı | Tam olarak bir görev ve bir takas, 200 ile biter |
| İkinci çalıştırma, aynı jar | Görevsiz 200, çünkü ilk istek zaten çerezi taşıyordu |
| İlk iki bulmaca reddedildi | İki yeniden deneme iletisi, üçüncü görevde başarı |
| İlk üç bulmaca reddedildi | Üç yeniden deneme iletisi, yakalanmayan bir RuntimeException, çıkış kodu 255, voucher isteği yok |
| Birlikte başlatılan iki script, bulmaca 2 saniye bekletildi | Kilitle bir görev ve ikisi de 200 ile bitti. flock satırı olmadan iki ya da daha fazla görev |
8.0.30 ve 8.4.15 üzerinde her varyant için 10 kez tekrarlandığında kilit, 20 çalıştırmanın tamamında bir görev verdi. Kilitsiz, 20 çalıştırmanın 19 tanesinde iki görev, 8.4.15 üzerindeki bir çalıştırmada üç görev oluştu. Token, sitenin bağışıklık süresi dolana kadar geçerli kalır; CapSkip API belgeleri bunun varsayılanını beş dakika olarak belirtir. Bu pencere içindeki bir çalıştırma çerezi gönderir ve CapSkip’i atlar, bu yüzden zamanlanmış bir iş bu script’i birkaç dakikada bir çağırabilir.
Çözümü bir web isteğinin içinde yapmayın
Bir çözüm bir PHP web isteğinden uzun sürebilir. API belgeleri çoğu görevin 15 saniye içinde hazır olduğunu ve yavaş bir proxy’nin buna eklendiğini söyler; PHP kılavuzu ise web kullanımı için max_execution_time varsayılanını 30 saniye olarak verir, komut satırı için ise hiç sınır tanımaz. Script’in tamamını bir web SAPI olan php-cgi.exe altında, 8.0.30 ve 8.4.15 üzerinde, her bulmacayı 40 simüle saniye tutan bir mock’a karşı çalıştırdım. Yaklaşık 33 saniye sonra Maximum execution time of 30 seconds exceeded fatal error ile öldü, çünkü PHP hatayı yalnızca SDK içindeki bir yoklama uykusu bittiğinde verdi. php.exe altında aynı script yaklaşık 43 saniye sürdü.
Daha kısa bir SDK zaman aşımı onu kurtarmaz. 20 saniyelik zaman aşımıyla SDK yaklaşık 0,25; 0,75; 1,75; 3,75; 7,75; 12,75 ve 17,75 saniyede yoklar, sonra 22,75 saniyeye kadar uyur ve vazgeçer. 8.0.30 ve 8.4.15 üzerinde 16 ya da 17,5 saniye sonra hazır olan bir bulmaca yaklaşık 18 saniyede alındı, ancak 19 saniye sonra hazır olan kaçırıldı: çağrı yaklaşık 23 saniyede TimeoutException ile bitti ve yanıtı hiç okumadı. Varsayılan 5 saniyelik yoklama üst sınırıyla, 20 saniyelik zaman aşımı yalnızca yaklaşık 17,75 saniyeye kadar hazır olan yanıtları yakalar.
Web tarafını salt okunur tutun ve çözme işini zamanlanmış olarak çalışan php waf.php komutuna bırakın. Blok, script’in bir web kopyasının son iki satırının yerine geçer. 8.4.15 üzerinde, 40 saniyelik bir mock bulmacayla yapılan php-cgi testinde, iş çözerken gönderilen iki web isteği 0,25 saniye içinde Retry-After başlıklı bir 503 aldı. İş yaklaşık 43 saniye sonra bir görev ve bir takasla bitti ve sonraki web isteği gerçek sayfayı aldı.
// 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);Yaygın hatalar
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| ValidationException | Herhangi bir istekten önce SDK içinde fırlatılır: tek başına gönderilen gokuProps.iv veya gokuProps.context, awswaf.com host’unda https olmayan bir script adresi, yanlış adlı bir script dosyası, göreli bir sayfa URL’si ya da bilinmeyen bir seçenek | İletinin adını verdiği şeyi düzeltin |
| ERROR_CAPTCHA_UNSOLVABLE içeren ApiException | Bu bulmaca yanıtlanamıyordu. Her yeni görev farklı bir bulmaca alır, bkz. API belgeleri | Yapılacak bir şey yok: döngü yeni bir sayfa çeker ve yeniden gönderir |
| NetworkException | Host ve portta dinleyen yok. Kapalı bir yerel port yaklaşık 2 saniye sonra başarısız oldu | CapSkip’i başlatın ve host ile portu yeniden kontrol edin |
| TimeoutException | Bekleme yanıtsız bitti. ApiException değil CapSkipError sınıfını genişletir, bu yüzden yeniden deneme döngüsü onu geçirir | CapSkip’in çözdüğünü doğrulayın ya da vazgeçmek istediğiniz yerde yakalayın |
| Yakalamanıza rağmen yakalanmayan ApiException | CapSkip\CapSkip için bir use satırından sonra, CapSkip\Exceptions\ApiException biçiminde yazılmış bir catch, var olmayan bir sınıfı adlandırır | İstisnayı import edin ya da adı ters eğik çizgiyle başlatın |
| RuntimeException: SSL certificate problem: unable to get local issuer certificate | curl.cainfo ayarlı değil, bu yüzden PHP ilk https isteğinde, sayfa çekimi de olsa voucher çağrısı da olsa, sertifikayı doğrulayamaz | php.ini içinde curl.cainfo ayarını bir CA dosyasına yöneltin |
| voucher exchange failed: HTTP 400 | CapSkip uygulamasının kaynağı, voucher uç noktasından gelen bir 400 yanıtını null ya da geçersiz bir existing_token olarak ele alır; muhtemel gerçek neden budur. Mock’um hiç vermediği bir çift için 400 döndürür ve gerçek bir reddi yeniden üretemedim | Turu yeni bir görevle tekrarlayın ve hemen takas edin |
| PHP 8.5 üzerinde curl_close adını veren Deprecated satırları | SDK içindeki Http.php dosyası hâlâ curl_close çağırıyor ve PHP 8.5.0 bunu kullanımdan kaldırıyor. CapSkip’e giden her istek için bir satır yazdırır ve çözüm yine de çalışır | SDK bu çağrıyı bırakana kadar E_DEPRECATED değerini error_reporting dışında bırakın |
FAQ
Neden her sayfa çekiminde CURLOPT_COOKIEJAR ayarlamayalım?
Bir handle, PHP onu serbest bıraktığında jar’ını yazar ve token kaydedilmeden önce dosyayı okumuş bir handle eski içeriğini geri yazar. Bir denemede, böyle bir handle serbest kaldığı anda token dosyadan kayboldu; altı sürümün hepsinde. Siteniz tutmanız gereken çerezler ayarlıyorsa onlara ayrı bir jar verin.
Bir widget sayfası bundan nasıl farklıdır?
Bir widget sayfası, jsapi.js yükleyen ve bulmacayı kendisi çizen sıradan bir formdur. SDK, widget’ın apiKey değerini sitekey olarak, jsapi.js adresini jsapiScript seçeneği olarak alır; iv ya da context yoktur. Bu yazı yalnızca 405 sayfasını test eder, bu yüzden 1. Adım’daki okuyucunun bir widget için yeniden yazılması gerekir.
Başka bir makinedeki PHP, Server modundaki CapSkip’imi kullanabilir mi?
Evet. Server modunda CapSkip ağınızı ya da genel IP’nizi dinler, bu yüzden ikinci bir bilgisayardaki, bir VPS’teki veya barındırılan bir platformdaki PHP, constructor içinde farklı bir host ve portla aynı API’yi çağırır; başka hiçbir şey değişmez. Statik bir genel IP uzaktan erişimi kararlı tutar. Donanım sizin kalır ve çözüm başına ücretlendirme yoktur. Server modu CapSkip portunu her adrese açar, bu yüzden onu özel tutan şey, beklemediğiniz makineler için bir Windows Firewall engelleme kuralıdır. Voucher takası CapSkip makinesinde değil, PHP’nin çalıştığı yerde gerçekleşir ve AWS’nin bunu önemseyip önemsemediğini test etmedim.
PHP ile bir AWS WAF CAPTCHA’sı, token’ı tutan bir çerez dosyasına ve aynı anda yalnızca bir sürecin çözmesine izin veren bir kilide dayanır. CapSkip kendi makinenizde çalışan bir captcha çözücü olduğu için, yanıtlanamayan bir bulmaca bir yeniden denemeye mal olur, paraya değil. Bu akışın Python sürümünü şurada bulabilirsiniz: Python kılavuzu; daha geniş SDK için bkz. PHP CAPTCHA çözücü sayfası.
