Bypassing CAPTCHAs with Headless Chrome
By: Jarrod Overson
Tags: Security, Recaptcha, JavaScript, Chrome, Puppeteer

For image-based CAPTCHAs the result is almost instantaneously available. For reCAPTCHA v2, it can take upwards of 15–30 seconds.

Automating with Puppeteer

Before worrying about the CAPTCHA you need to get everything else taken care and before we can do that we need to choose our weapons. In this post we’re going to be using Google’s Chrome for 3 reasons:

It is very easily automatable via the Puppeteer API. It can run both headlessly and with a GUI which makes it easy to work with and portable. It’s the world’s most common browser so any other anti-automation tricks a website has will be less likely to work (like blocking Selenium or PhantomJS)

Using Puppeteer

You don’t even need to install Chrome if you don’t want it, Puppeteer comes with everything you need including a Chromium install. You can use a local installation of Chrome if you want it, but that’s up to you.

$ npm install puppeteer

Make sure everything’s all wired up by taking it for a spin. For this exercise we’re going to be automating Reddit’s signup page simply because it was the first page I came across that used 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');

})()

In this code we’re specifying two configuration properties at launch, headless: false in order for us to be able to see what we’re doing and defaultViewport: null to account for an ugly visual glitch where the viewport doesn’t fill the window. Neither are important for headless operation — they just make it easier to see and, more importantly, take screenshots. Like this one:

That was easy! Now that we’re up and running the next step is to automate the signup as if there wasn’t a CAPTCHA in place. This is where having the ability to toggle headless off and on is helpful because we can drive the browser like a human when we need to. First we need to understand how to access the elements on the page that need to be manipulated. Get your browser running and inspect the loaded page via Chrome’s devtools (Shortcut: F12). Next, find the text fields we’ll need to manipulate (Shortcut: ⌘+Shift+C on Mac and Ctrl+Shift+C on Windows). In Reddit’s case we need to be able to directly access the username field, the 2 password fields, and the button. The email field is optional so we can ignore it. Typing in text fields is almost comically intuitive with the puppeteer API, you simply pass in selector that identifies the element and the desired string to the .type() method.

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

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

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

Manipulating the button is just as intuitive except that the button in Reddit’s page doesn’t have an ID associated with it so we need to have a slightly more complicate selector. If you aren’t familiar with CSS Selectors, check out the Mozilla Developer Network for a quick rundown.

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

There you have it! Test the script to make sure the login is being submitted. It won’t work, of course, because of the CAPTCHA, but we can test to see the hooks are working properly.

Wait a second! We don’t even see a CAPTCHA and the JavaScript console is complaining of errors. What is happening here? When automating web pages there are numerous non-CAPTCHA hurdles that can get in your way and one of them is just going so fast that the page breaks. When browsers are automated they are manipulated many, many times faster than a normal human can operate and that often leads to code being executed in an order that developers had not tested (this is called a race condition).

Reddit’s page suffers from a race condition where Google’s reCAPTCHA is only being rendered after the second password field has been focused. Our script operates so quickly that focus occurs before the reCAPTCHA script is ready. There are many solutions to this but the easiest is to add the smallest delay necessary that gets around this race condition. We could add hooks and listeners to make sure we only operate after reCAPTCHA has been loaded but the Reddit developers themselves seem fine with this race condition so there’s no need for us to get too smart. There are many ways we can delay but Puppeteer’s browser launch options take in a “slowMo” value that globally delays all actions a set amount. This is a heavy handed approach since it slows down any Puppeteer action but it’s a good place to start.

const chromeOptions = {

headless:false,

defaultViewport: null,

slowMo:10,

};

After adding that option we see the CAPTCHA and things are back on track. For a fun experiment you might as well try to solve the CAPTCHA right now and see what happens. Since we’re using the default Chromium instance that Puppeteer opens and we are controlling it via automated means then reCAPTCHA will try its damnedest to prove that we’re not human. You will likely go through multiple levels of the challenge even if you’re getting all the pictures right. When I tested this I had to go through 10 different iterations before I got the green checkmark.

Luckily we have a much easier way of doing this.

Wiring up 2Captcha

2Captcha needs an API key which you get when you sign up. You’ll also need to deposit some funds because, well, nothing in life is free. And just for fun, of course, you’ll need to solve a CAPTCHA when signing up 😃