😽

Headless Chromeを使ってCAPTCHAをバイパス

2022/11/01に公開

2CaptchaとPuppeteerを使ってCAPCHA突破を自動化

この内容はYouTubeでも視聴可能です。動画のほうが理解しやすい場合はこちらをチェックしてみてください。
https://youtu.be/wsDRkAD6lPs
CAPTCHAは10年以上にわたってインターネット上で見てきたと思います。ログイン、登録、またはどこかにコメントを投稿しようとするときに、道を塞ぐ曲がりくねった線、単語、または数字のことです。

CAPTCHA (またはコンピューターと人間を区別するためのCompletely Automated Public Turing test-完全に自動化された公開チューリング テスト-) は人間を通過させ、ロボット (プログラム) を追い出すゲートとして設計されています。曲がりくねった線や波打つ単語は最近ではあまり見られなくなり、GoogleのreCAPTCHA version 2に置き換えられました。これはあなたの人間性指数が十分に高いと見なされる限り、緑色のチェックマークを付けるCAPTCHAです。

スコアが Googleが考える人間性指数の閾値を超えていない場合、reCAPTCHAはパズルのような画像チャレンジをけしかけます。これは、驚くべきことに実際には単語を解読するよりも面倒なものにできています。

CAPTCHAは人間にとって煩わしいものですが、少なくともその役割を果たしていれば許容できますが、CAPTCHAを自動化する方が、人間であることを証明するよりも簡単です。

2Captchaの仕組み

2Captchaは、ほぼ同じ2つのAPIエンドポイントを使用して、多数の異なるスタイルのCAPTCHAを突破します。最初のリクエストは、CAPTCHAを解決するために必要なデータを配信し、リクエスト ID を返します。画像ベースのCAPTCHAの場合、データはCAPTCHA 自体のbase64化された画像になります。

リクエスト ID を取得したら、リクエストを結果のエンドポイントに送信し、ソリューションの準備が整うまでポーリングする必要があります。

reCAPTCHA v2の場合、話は少し異なります。上記と同じ2段階プロセスに引き続き取り組んでいますが、別のデータを送信しています。この場合、iframe がロードされているかどうかに関係なく、含まれている<div>にあるreCAPTCHAサイトキーを送信する必要があります。

取得したレスポンスは、フォームと一緒に送信する必要があるトークンであり、g-recaptcha-responseのIDを持つ非表示のテキストフィールドに入力する必要があります。下の画像はその場所を示しており、ページに表示するためだけにdisplay: none cssプロパティを無効にしました。編集可能にすることで、2Captchaのレスポンスを手動で簡単にテストして、統合のテストの変数を減らすことができます。

画像ベースのCAPTCHAの場合、結果はほぼ瞬時に利用できます。 reCAPTCHA v2 の場合、15 ~ 30秒以上かかることがあります。

Puppeteerを使った自動化

CAPTCHAについて心配する前に、他のすべてを処理する必要があります。そしてその前には使うべき武器を選択する必要があります。この投稿では、次の3つの理由からGoogle Chromeを使用します。

Puppeteer API経由で非常に簡単に自動化できる
ヘッドレスでも、操作が簡単でポータブルなGUIを使用しても実行できる
世界で最も一般的なブラウザであるため、ウェブサイトにある他の自動化防止のトリック (Seleniumや PhantomJSのブロックなど) が機能する可能性は低い

Puppeteerの使用

そうしたくない場合はChromeをインストールする必要はありません。PuppeteerにはChromiumのインストールなど必要なものがすべて付属しています。必要に応じてChromeのローカルインストールを使用することもできますが、それはあなた次第です

$ npm install puppeteer

プログラムを一走りさせて、すべてがつながっていることを確認してください。この練習では、Redditの登録ページを自動化します。これがたまたまreCAPTCHAを使用した最初のページだったからです。

const puppeteer = require('puppeteer');
const chromeOptions = {
  headless:false,
  defaultViewport: null};
(async function main() {
  const browser = await puppeteer.launch(chromeOptions);
  const page = await browser.newPage();
  await page.goto('https://old.reddit.com/login');
})()

このコードでは、起動時に2つの構成プロパティを指定しています。headless: falseは、実行していることを確認できるようにするため、defaultViewport: null は、ビューポートがウィンドウを埋めないという見苦しい視覚的な不具合を説明するためです。headless操作ではどちらも重要ではありません。ただ見やすくなるのと、さらに重要なことにスクリーンショットを撮ることができるようにします。こちらをご覧ください。

簡単ですね!それでは起動して実行できたので、次のステップはCAPTCHAが配置されていないかのようにサインアップを自動化することです。これは必要なときに人間のようにブラウザを操作できるため、headlessのオンとオフを切り替える機能があると便利です。まず、操作する必要があるページ上の要素にアクセスする方法を理解する必要があります。ブラウザを実行し、読み込まれたページを Chromeのdevtools (ショートカット: F12) で調べます。次に操作する必要があるテキストフィールドを見つけます(ショートカット: Mac では ⌘+Shift+C、Windowsでは Ctrl+Shift+C)。 Redditの場合、ユーザー名フィールド、2 つのパスワード フィールド、およびボタンに直接アクセスできる必要があります。eメールフィールドは省略可能であるため、無視できます。 puppeteer API を使用すると、テキスト フィールドへの入力は滑稽なほど直感的です。要素と目的の文字列を識別するセレクターを.type()メソッドに渡すだけです。

await page.type('#user_reg', 'some_username');
await page.type('#passwd_reg', 'SuperStrongP@ssw0rd');
await page.type('#passwd2_reg', 'SuperStrongP@ssw0rd');

ボタンの操作は直感的ですが、RedditのページのボタンにはIDが関連付けられていないため、もう少し複雑なセレクターが必要です。CSSセレクターに慣れていない場合は、Mozilla Developer Networkで概要を確認してください。

await page.click('#register-form button[type=submit]');

そこにありますね!スクリプトをテストして、ログインが送信されていることを確認します。当然、CAPTCHAのせいで機能しませんが、フックが適切に機能していることをテストできます。

ちょっと待って!CAPTCHAも表示されず、JavaScriptコンソールにエラーが表示されます。何が起きているのでしょうか?ウェブページを自動化する場合、CAPTCHA以外のハードルが多数あり、そのうちの 1 つが速すぎてページが壊れてしまいます。ブラウザが自動化されると、通常の人間が操作できるよりも何倍も速く操作され、開発者がテストしていない順序でコードが実行されることがよくあります (これはレースコンディションと呼ばれます)。

Redditのページは2番目のパスワードフィールドがフォーカスされた後にのみ GoogleのreCAPTCHAがレンダリングされるというレースコンディションに悩まされています。このスクリプトは非常に高速に動作するため、reCAPTCHAスクリプトの準備が整う前にフォーカスが発生します。これに対しては多くの解決策がありますが、最も簡単なのはこのレースコンディションを回避するために必要な最小限の遅延を追加することです。フックとリスナーを追加して、reCAPTCHA がロードされた後にのみ動作するようにすることもできますが、Reddit 開発者自身はこのレースコンディションを特に気にしていないように見えるので、あまりスマートにする必要はありません。遅延させる方法はたくさんありますが、Puppeteer のブラウザ起動オプションは「slowMo」値を取り込んで、すべてのアクションを一定量遅延させます。Puppeteerのアクションが遅くなるため、これは手間のかかるアプローチですが、開始するには適しています。

const chromeOptions = {
 headless:false,
 defaultViewport: null,
 slowMo:10,
};

そのオプションを追加すると、CAPTCHAが表示され、元の状態に戻ります。楽しい実験として、CAPTCHAを今すぐ解決して、何が起こるかを確認してみてください。Puppeteerが開くデフォルトのChromiumインスタンスを使用し、自動化された手段で制御しているため、reCAPTCHAは私たちが人間ではないことを証明するために最善を尽くします。すべての写真を正しく撮影できたとしても、複数のレベルの課題を経験する可能性があります。私がテストしたときは、緑色のチェックマークを取得する前に、10回繰り返す必要がありました。

幸いなことに、これをするためのはるかに簡単な方法があります。

2Captchaにつなぐ

2Captchaにはサインアップ時に取得するAPIキーが必要です。また、いくらかの資金を入金する必要があります。人生で無料のものは何もありません。もちろん、楽しみのためにサインアップ時にCAPTCHAを解決する必要があります 😃

2CaptchaのAPIは、CAPTCHAデータを送信し、それから返ってきたリクエストIDで結果をポーリングする2ステップのプロセスで機能します。 今はreCAPTCHA v2を扱っているため、先ほど説明したRedditのサイトキーを送信する必要があります。また、methodをuserrecaptchaに設定し、このreCAPTCHAがあるページURLを配信する必要があります。

const formData = {
  method: 'userrecaptcha',
  key: apiKey, // your 2Captcha API Key
  googlekey: '6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC',
  pageurl: 'https://old.reddit.com/login',
  json: 1
};
const response = await request.post('http://2captcha.com/in.php', {form: formData});
const requestId = JSON.parse(response).request;

このコールを行ってリクエストIDを取得したら、レスポンスを取得するために、APIキーとリクエストIDを使用して「res.php」URLをポーリングする必要があります。

`http://2captcha.com/res.php?key=${apiKey}&action=get&id=${reqId}`;

CAPTCHAの準備ができていない場合は、「CAPTCHA_NOT_READY」というレスポンスが返されます。これは、1 ~ 2 秒後に再試行する必要があることを示しています。準備ができたら、レスポンスは送信したメソッドに適したデータになります。画像ベースのCAPTCHAの場合はソリューションであり、reCAPTCHA v2の場合はフォーム入力で送信する必要があるデータです。

reCAPTCHA v2の場合、解決にかかる時間は少し異なります。速いときは15秒で、遅いときは45 秒ほどかかったことがあります。以下はポーリングメカニズムの例ですが、これは単純なURLコールであり、アプリに統合することができます。

async function pollForRequestResults(
  key, 
  id, 
  retries = 30, 
  interval = 1500, 
  delay = 15000
) {
  await timeout(delay);
  return poll({
    taskFn: requestCaptchaResults(key, id),
    interval,
    retries
  });
}
function requestCaptchaResults(apiKey, requestId) {
  const url = `http://2captcha.com/res.php?key=${apiKey}&action=get&id=${requestId}&json=1`;
  return async function() {
    return new Promise(async function(resolve, reject){
      const rawResponse = await request.get(url);
      const resp = JSON.parse(rawResponse);
      if (resp.status === 0) return reject(resp.request);
      resolve(resp.request);
    });
  }
}
const timeout = millis => new Promise(resolve => setTimeout(resolve, millis))	

レスポンスデータを取得したら、その結果を Redditのサインアップフォームにある非表示の g-recaptcha-responseテキストエリアに挿入する必要があります。これはPuppeteerの .type()メソッドを使用した場合ほど簡単ではありません。要素が表示されず、フォーカスを受け取ることができないためです。表示可能にしてから.type()を使用するか、JavaScriptを使用して値をページに挿入できます。Puppeteerを使用してJavaScriptをページに挿入するには、.evaluate()メソッドを使用します。このメソッドは、関数または文字列を受け取り (関数を渡す場合は、単に .toString() します)、ページ コンテキストで実行します。

const response = await pollForRequestResults(apiKey, requestId);
const js = `document.getElementById("g-recaptcha-response").innerHTML="${response}";`
await page.evaluate(js);

その値を挿入したら、サインアップを完了する準備が整いました。本当にこんなに簡単なんです。

https://youtu.be/-nW_YO35-P8

Puppeteerと2Captchaを試してみたい場合は、下に完全なスクリプトを掲載しています。

const puppeteer = require('puppeteer');
const request = require('request-promise-native');
const poll = require('promise-poller').default;

const siteDetails = {
  sitekey: '6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC',
  pageurl: 'https://old.reddit.com/login'
}

const getUsername = require('./get-username');
const getPassword = require('./get-password');
const apiKey = require('./api-key');

const chromeOptions = {
  executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  headless:false, 
  slowMo:10,
  defaultViewport: null
};

(async function main() {
  const browser = await puppeteer.launch(chromeOptions);

  const page = await browser.newPage();

  await page.goto('https://old.reddit.com/login');

  const requestId = await initiateCaptchaRequest(apiKey);

  await page.type('#user_reg', getUsername());

  const password = getPassword();
  await page.type('#passwd_reg', password);
  await page.type('#passwd2_reg', password);

  const response = await pollForRequestResults(apiKey, requestId);
  
  await page.evaluate(`document.getElementById("g-recaptcha-response").innerHTML="${response}";`);

  page.click('#register-form button[type=submit]');
})()

async function initiateCaptchaRequest(apiKey) {
  const formData = {
    method: 'userrecaptcha',
    googlekey: siteDetails.sitekey,
    key: apiKey,
    pageurl: siteDetails.pageurl,
    json: 1
  };
  const response = await request.post('http://2captcha.com/in.php', {form: formData});
  return JSON.parse(response).request;
}

async function pollForRequestResults(key, id, retries = 30, interval = 1500, delay = 15000) {
  await timeout(delay);
  return poll({
    taskFn: requestCaptchaResults(key, id),
    interval,
    retries
  });
}

function requestCaptchaResults(apiKey, requestId) {
  const url = `http://2captcha.com/res.php?key=${apiKey}&action=get&id=${requestId}&json=1`;
  return async function() {
    return new Promise(async function(resolve, reject){
      const rawResponse = await request.get(url);
      const resp = JSON.parse(rawResponse);
      if (resp.status === 0) return reject(resp.request);
      resolve(resp.request);
    });
  }
}

const timeout = millis => new Promise(resolve => setTimeout(resolve, millis))

これで何ができるようになったのですか?

この投稿は次の2つの目的で作成されました。

  1. CAPTCHAがどれだけ最悪か見せること

  1. CAPTCHAがあなたをブロックする必要がないことを共有すること

CAPTCHAは通常、何百万ものリクエストを実行する攻撃キャンペーンを行い、不正または悪意のある目的でコンテンツを操作する悪意のある人物をブロックするために存在します。ウェブサイトをプログラムで制御したい正当な理由はたくさんあります。CAPTCHAが悪意のあるユーザーをブロックしていないのであれば、攻撃を止めることはできません。

読んでくれてありがとう!いつものように、Twitter @jsoverson で質問やコメントをお気軽にお寄せください。

Discussion