Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

2captcha를 이용해서 사이트의 CAPTCHA 자동화로 처리하기

이 글은 2Captcha로부터 비용을 받고 작성하는 글이다.

광고성 글은 거의 안 쓰는 편이라 비용을 받고 블로그에 글을 쓰는 건 처음인데 서비스 자체가 종종 필요한 경우가 있어서 도움이 될 부분이 있어 보였기에 작성하기로 했다. 글을 받아서 올리는 건 아니고 2capthca에 대한 소개 글을 써달라는 요청만 받고 글의 구성이나 예제는 모두 직접 작성했다.

CAPTCHA

발음도 어려운 CAPTCHA는 "Completely Automated Public Turing test to tell Computers and Humans Apart"의 약자로 사람과 컴퓨터를 구분하는 완전 자동화된 튜링 테스트를 말한다.

보통 로그인이나 회원 가입 같은 사용자 액션에 보통 봇이라고 부르는 자동화 프로그램을 막기 위해서 CAPTCHA를 넣고 일반적으로 컴퓨터는 읽기 어려운 문자를 보여주고 사람이 입력하게 한다.(때로는 사람도 읽기가 어렵다) 최근에는 다양한 CAPTCHA가 개발되어 여러 이미지 중에 선택하거나 비슷한 이미지를 고르거나 이미지를 회전시키는 등의 방법도 사용되고 있다.

CAPTCHA를 이용해서 자동화된 봇을 막는 이유도 있고 이를 뚫으려는 이유도 있다. 남의 사이트를 악의적으로 조작하는 것은 아니라고 하더라도 CAPTCHA 때문에 자동화를 못 한 경험이 쉽게 할 수 있다. 나 같은 경우도 사용하는 서비스에서 보고서 관련 API를 제공하지 않아서 매달 혹은 매주 사람이 수동으로 데이터를 정리해야 했기에 이를 자동화해보려고 했지만, CAPTCHA에 막혀서 제대로 하지 못한 기억이 있다.

2Captcha

2Captcha 홈페이지

2Captcha는 이러한 Captcha를 해결해주는 유료 서비스다.

2Captcha's main purpose is solving your CAPTCHAs in a quick and accurate way by human employees, but the service is not limited only to CAPTCHA solving.

문서를 보면 CAPTCHA를 사람이 해결해 주는 것으로 보인다. CAPTCHA 해결을 요청하면 사람이 이를 보고 직접 해결하고 그 처리된 값을 나한테 다시 알려주면 이를 이용해서 CAPTCHA를 해결할 수 있는 것이다. reCaptcha, hCaptcha, KeyCaptcha, RotateCaptcha등 다양한 CAPTCHA를 지원하고 여러 언어의 SDK도 제공하지만, HTTP API를 제공하므로 어떤 언어에서든 사용할 수 있다.

다음과 같은 과정으로 진행된다.

  1. 해결할 CAPTCHA를 2captcha에 요청하면 응답으로 해당 요청의 ID를 알려준다.
  2. 이 요청 ID로 해당 작업이 완료되었는지 반복해서 확인한다.
  3. 20~50초 후 요청이 완료되면 응답 값을 알려주고 이 값을 이용해서 CAPTHCA를 풀 수 있다.

그리고 2Captcha는 유료서비스이다.

2Captcha의 CAPTCHA별 가격표

CAPTCHA 종류에 따라 다른데 가격표를 보면 많이 쓰는 reCaptcha가 1,000번 요청에 2.99달러이므로 크게 부담될 가격은 아니라고 생각한다.

Walmart 로그인 데모

Walmart는 로그인 시에 reCaptcha v2를 사용하고 있다. 사실 서비스마다 CAPTCHA를 사용하는 패턴이 다르기에 적당한 예제를 찾는데도 꽤 오래 걸렸다. 많은 사이트가 CAPTHCA를 항상 보여주는 게 아니라(그런 사이트도 있지만...) 여러 조건을 검사하고 보여줬다 안 보여줬다 하고 봇을 막으로면 CAPTCHA에만 의존하는 것이 아니라 추가적인 방법도 함께 사용하기 때문에(폼의 DOM 변형을 준다거나 유저 에이전트로 차단한다거나...) 실제로 자동화를 하려면 이러한 부분도 다 찾아서 해결해야 한다.

여기서는 2captcha로 어떻게 reCaptcha v2를 해결하는지를 보여줄 것이고 실제로 사용하려면 대상 사이트의 패턴을 알아야 자동화를 할 수 있을 것이다.

월마트 로그인 페이지

Walmart의 로그인 페이지는 CAPTCHA를 요구한다. 주로 사용하는 브라우저에서는 보통 자동으로 넘어가지만, 시크릿 모드로 접속하면 다음과 같이 reCaptcha v2가 나타난다.

로그인 했을 때 나오는 reCapthca 화면으로 I'm not a robot를 확인하라고 나온다

"I'm not a robot"을 입력하면 보통 많이 보았을 여러 사진 중 버스를 고르라거나 신호등을 고르라거나 해서 이미지 경계에 살짝 버스가 걸리면 이 이미지도 선택해야 하나 말아야 하나 고민하게 만드는 과정이 진행된다.

로그인 자동화

단순히 CAPTCHA만 푸는 것이 아니라 로그인 자체를 완전히 자동화하기 위해 Playwright를 사용했다. Playwright는 브라우저를 조작하는 API를 사용해서 자동화할 수 있게 하는 프로젝트고 Microsoft에서 만들었다. 주로 Puppeteer를 사용했지만 궁금해서 이번에는 Playwright를 사용했다. 전체 예제는 GitHub에 올려두었다.

const playwright = require('playwright');
const got = require('got');

const ID = process.env.ID;
const PASSWORD = process.env.PASSWORD;

playwright를 사용했고 HTTP 라이브러리로 got을 사용했다. 월마트에 로그인할 계정정보가 필요하므로 환경변수에서 가져오도록 했다.

const browser = await playwright.chromium.launch({});
const context = await browser.newContext();
const page = await context.newPage();

// login page
await page.goto('https://www.walmart.com/account/login');

playwright를 사용해서 크롬 브라우저를 실행하고 월마트 로그인 페이지에 접속했다. 이 코드로는 헤드레스 크롬이 실행되고 실제 크롬으로 실행하려면 playwright.chromium.launch({ headless: false })처럼 옵션을 주어야 한다.

// enter email
await page.type('#email', ID, {delay: 100});
await page.screenshot({ path: 'img/id.png' });

// check password field in a form
const hasPassword = await page.$('#sign-in-form');
if (hasPassword) {
  console.log('has password field')
  // enter password
  await page.type('#password', PASSWORD, {delay: 100});
} else {
  console.log('no password field')
  await page.click('#sign-in-with-email-validation [type=submit]');

  // enter password
  await page.type('#sign-in-password-no-otp', PASSWORD, {delay: 100});

  // login
  await page.click('#sign-in-with-password-form [type=submit]');
}

// login
await page.click('#sign-in-form [type=submit]');

로그인 페이지에서 이메일과 비밀번호를 입력하고 로그인 버튼을 누르는 코드이다.

playwright로 이메일, 비밀번호를 입력한 화면

이메일과 비밀번호가 잘 입력되는 것을 확인할 수 있다. 위 코드에서 if (hasPassword) {} 조건절이 있는 이유는 가끔 위 스크린숏과 달리 이메일만 입력하고 로그인 버튼을 누르면 패스워드 입력란이 나오는 UI가 등장한다. 이 부분도 아마 자동화를 막기 위해 변형을 준 것 같아서 처리를 했는데 테스트했을 때는 else 조건으로 빠지는 UI는 거의 등장하지 않아서 사실 이 부분은 제대로 처리하지 않았다.(동작하지 않는다는 의미이다) 여기 예제에서는 월마트의 구현이 중요한 건 아니라서 상관없다.

2captcha로 reCaptcha 해결 자동화

2captcha에서 제공하는 데모 페이지에서 테스트해보면서 사용 방법을 알 수 있다.

// retrive data from recaptcha
const recaptcha = await page.waitForSelector('.g-recaptcha');
const sitekey = await recaptcha.getAttribute('data-sitekey');
const currentUrl = await page.url();

reCaptcha v2의 정보가 필요하므로 .g-recaptcha 요소에 data-sitekey 속성으로 저장된 키가 필요하고 현재 URL이 필요하기 때문에 이 값을 가져왔다.

이제 이를 이용해서 2captcha에 요청을 보낼 차례이다.

// 2captcha
const APIKEY = process.env.APIKEY;
const twoCaptchaURL = `https://2captcha.com/in.php?key=${APIKEY}&method=userrecaptcha&googlekey=${sitekey}&pageurl=${currentUrl}`;

let requestId;
try {
  const response = await got(twoCaptchaURL);
  const result = response.body;
  if (result.startsWith('OK|')) {
    requestId = result.split('|')[1];
    console.log('RequestId: ' + requestId);
  } else {
    throw new Error(`Wrong response: ${result}`);
  }
} catch(e) {
  console.log(e.response.body);
  await browser.close();
}

2captcha의 API Key는 대시보드에서 볼 수 있다. 이를 이용해서 CAPTCHA 해결 요청 URL인 https://2captcha.com/in.php에 보내면서 앞에서 저장해 놓은 sitekeycurrentUrl을 파라미터로 보낸다.

응답이 제대로 처리된 경우 OK|66648692707 같은 형식의 응답을 받으므로 앞의 OK| 부분을 잘라내고 뒷부분인 요청 ID를 따로 저장했다. 해당 요청이 처리되었는지 확인하려면 이 요청 ID가 필요하다.

const resultUrl = `https://2captcha.com/res.php?key=${APIKEY}&action=get&id=${requestId}`;
const getCaptchaResult = () => {
  return new Promise((resolve, reject) => {
    setTimeout(async () => {
      try {
        const response = await got(resultUrl);
        resolve(response.body);
      } catch(e) {
        reject(e.response.body);
      }
    }, 10000);
  });
};


let answer;
let isNotSolved = true;
while(isNotSolved) {
  try {
    const result = await getCaptchaResult();
    if (result.startsWith('OK|')) {
      answer = result.split('|')[1];
      isNotSolved = false;
    } else {
      throw new Error(`Wrong response: ${result}`);
    }
    console.log(result);
  } catch(e) {
    console.log('error')
    console.log(e);
  }
}

해당 CAPTCHA의 해결은 실제로 사람이 하므로 시간이 걸리고 언제 완료되는지 알 수가 없다. 그러므로 요청 ID를 가지고 주기적으로 확인해 보면서 해당 요청이 처리되었는지 확인해야 한다. 이를 확인하는 함수가 getCaptchaResult()이고 이를 while(isNotSolved) {}으로 10초마다 계속 호출하면서 요청이 처리되었는지를 확인한다. 요청이 아직 처리 안 되었을 때는 CAPCHA_NOT_READY 응답이 오고 요청이 완료되면 OK|03AGdBq25rA1ntkj4Q67Zj...처럼 OK| 뒤에 결과값이 오므로 이를 추출해서 가져온다.

// enter recaptcha answer
await page.$eval('#g-recaptcha-response', el => {
  el.style.display = 'block';
});
await page.fill('#g-recaptcha-response', answer, {delay: 30});

await page.evaluate((answer) => {
  handleCaptcha(answer);
}, answer);

setTimeout(async () => {
  await page.screenshot({ path: 'img/complated.png' });
  await browser.close();
}, 5000);

이제 해결된 reCaptcha의 결과값을 처리해줘야 한다 reCaptcha v2의 경우 g-recaptcha-response를 ID로 가진 <textarea>에 이 결과값(answer)를 입력해주어야 한다. 해당 <textarea>가 보이지 않으면 입력할 수 없으므로 display: block;으로 보이게 처리한 뒤에 값을 입력한다.

기본적으로는 해당 폼을 제출하면 처리되어야 하지만 월마트의 경우 자동화를 막기 위해서인지 그냥 <form> 제출로는 처리되지 않고 해당 <form>을 처리하는 별도의 함수 handleCaptcha()를 만들어 두었기 때문에 이 함수에 answer를 전달해서 호출한다. 호출할 때는 페이지에서 JavaScript가 실행되도록 page.evaluate()를 사용했다. 폼 제출만으로 처리되면 좋겠지만 월마트의 경우처럼 대부분의 사이트는 자동화를 막기 위한 다양한 추가 트릭을 제공할 것이므로 이 부분은 상황에 맞게 각자 사이트를 분석해서 처리해야 한다.

마지막은 로그인된 페이지의 스크린숏을 찍기 위해서 setTimeout()으로 대기한 후에 스크린숏을 찍고 browser.close()로 브라우저를 종료했다.(해당 URL을 대기하는 식으로 구현해도 되지만 CAPTCHA 해결에 집중했기 때문에 이 부분은 테스트용으로 작성했던 것을 그대로 두었다.)

월마트 로그인을 자동화한 영상

headless 옵션을 풀고 실행하면 실제로 크롬을 실행해서 동작하는 것을 볼 수 있는데 위처럼 동작한다. 로그가 찍히는 것을 볼 수 있도록 뒤에 터미널의 로그를 볼 수 있게 했고 응답을 기다리다가 완료되면 처리하는 것을 볼 수 있다. 마지막에 페이지가 뜨자마자 끝나기는 하는데 아래처럼 로그인된 화면이 뜨는 것을 확인할 수 있다.

로그인된 월마트 페이지

CAPTCHA를 직접 해결하거나 우회하려면 비용이 많이 들고 창과 방패처럼 자동화를 피하려는 사이트의 작업이 계속되는 만큼 이를 해결하려는 노력도 계속해야 하는데 그 가운데 다루기 어려운 CAPTCHA는 2captcha를 이용하면 적은 비용으로 쉽게 해결할 수 있다.

2021/04/18 03:37 2021/04/18 03:37