CAPTCHAS——验证码


你在互联网上看到验证码(CAPTCHAS)已经有十多年了。当尝试登录、注册或在任何地方发表评论时,那些曲折的线条、文字或数字搞得人们比较烦。

CAPTCHAS(或完全自动化的公共图灵测试,用来区分计算机和人类)被设计成一扇门,让人类通过,机器人(程序)被挡在外面。扭曲的线条和摇摆不定的词语现在已经不那么常见了,它们已经被谷歌的reCAPTCHA版本2所取代。这就是CAPTCHA,只要你的人文指数(humanity quotient)被认为足够高,它就会给你绿色的复选标记。

如果你的得分没有超过谷歌的人类门槛,那么reCAPTCHA又回到了一个类似拼图的挑战,令人惊讶的是,这个挑战实际上比解读几个单词更烦人。


尽管人们非常讨厌CAPTCHA,但它们也起到了一定的安全防护作用。

2Captcha如何工作

2Captcha解决了许多不同的CAPTCHA样式,几乎所有样式都是使用相同的两个API端点。第一个请求传递解决CAPTCHA所需的数据,并返回一个请求ID。对于基于图像的CAPTCHA,数据将是CAPTCHA本身的Base64-ed图像。


获得请求ID后,您需要向结果端点提交请求,轮询直到解决方案准备就绪。


对于reCAPTCHA v2来说,情况有点不同。您仍在执行与上述相同的两步流程,但发送的数据不同。在这种情况下,您需要发送reCAPTCHA站点密钥,该密钥可以在包含<div>的位置找到,无论iframe是否已加载。


您获得的响应是需要与表单一起提交的token,需要输入带有g-recaptcha-response ID的隐藏文本字段。下面的图片显示了它的位置,我已经禁用了display:none css属性。使其可编辑使您可以轻松地手动测试2Captcha响应,以减少测试集成的变量。


对于基于图像的CAPTCHA,结果几乎可以立即获得。对于reCAPTCHA v2,它可能需要15-30秒。

使用Puppeteer实现自动化

我们需要选择合适的工具绕过验证码,在这篇文章中,我们使用Google的Chrome有三个原因:
它很容易通过PuppeteerAPI实现自动化。
它既可以以无头模式运行,也可以使用GUI运行,这使得它易于使用和移植。
它是世界上最常见的浏览器,所以网站上的任何其他反自动化技巧都不太可能起作用(比如屏蔽Selenium或PhantomJS)

使用Puppeteer

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');
})()

在这段代码中,我们在启动时指定了两个配置属性,headless:false,并且defaultViewport:null来解决没有填满窗口的丑陋视觉故障。这两个都不是重要的无头操作—这些操作只是让我们看起来更加直观,能截屏才是最重要的:

很简单!既然我们已经启动并运行了,下一步就是将注册自动化,就像没有CAPTCHA一样。
首先,我们需要了解如何访问页面上需要操作的元素。运行浏览器,并通过Chrome的DevTools(快捷方式:F12)检查加载的页面。接下来,找到我们需要操作的文本字段(快捷方式:Mac上的⌘+ Shift + C和Windows上的Ctrl + Shift + C)。在Reddit的例子中,我们需要能够直接访问用户名字段、2个密码字段和按钮。电子邮件字段是可选的,因此我们可以忽略它。使用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,它不起作用,但我们可以测试hook是否正常工作。


等一下!我们甚至没有看到CAPTCHA,JavaScript控制台抱怨错误。当浏览器被自动化时,他们被操作的速度比普通人快很多倍,这常常导致代码以开发人员没有测试过的顺序执行(这被称为竞争条件)。
Reddit的页面受到竞争条件的影响,只有在第二个密码字段聚焦后才会呈现Google的reCAPTCHA。我们的脚本操作速度如此之快,以至于焦点出现在reCAPTCHA脚本准备就绪之前。有许多解决方案,但最简单的是添加所需的最小延迟,以绕过这种竞争条件。我们可以添加钩子和侦听器,以确保我们只在加载reCAPTCHA之后才进行操作,但是Reddit开发人员对这种竞争条件很满意,所以我们不需要多复杂的操作。Puppeteer的浏览器启动选项会采用“slowMo”值,全局将所有操作延迟一个固定的数值。

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

添加该选项后,我们看到CAPTCHA,事情又回到了正轨。我们使用的是Puppeteer打开的默认Chromium实例,并且我们通过自动化的方式来控制它,所以reCAPTCHA将最大限度来证明我们不是人类。你可能会经历多个层次的挑战,即使你所有的图片都是正确的。当我测试它时,我必须经过10次不同的测试,才能得到绿色的复选标记。

Wiring up 2Captcha

2Captcha需要注册时获得的API密钥。你还需要存一些钱,因为,嗯,生活中没有什么是免费的。在注册时,您需要解决CAPTCHA问题。

2Captcha的API通过两个步骤工作,在该过程中提交CAPTCHA数据,然后使用返回的请求ID轮询结果。因为我们正在处理reCAPTCHA v2,所以我们需要发送Reddit的SiteKey,这是我在前面概述的我们还需要确保将方法设置为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”响应,表明您需要在一两秒钟内重试。当它准备就绪时,响应将是您发送的方法的适当数据。对于基于图像的CAPTCHA,这是解决方案,对于reCAPTCHA v2,则需要随表单输入一起发送数据。
对于reCAPTCHAV2,解决方案的时间可能会有所不同,速度短则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);

一旦我们注入了这个值,那么我们就可以完成注册了。
完整Demo演示:

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))

你现在可以做什么?

这篇文章有两个目的:

1.向您展示CAPTCHAS有多糟糕。

和。

2.向您显示CAPTCHAS不需要阻止您
CAPTCHA的存在通常是为了阻止恶意行为者出于欺诈或恶意目的操纵内容的攻击活动,这些攻击活动涉及数以百万计的请求。您可能想要以编程方式控制网站有很多正当理由,如果CAPTCHA没有阻止黑客,那么他们肯定不应该阻止用户。

谢谢你的阅读!随时可以在twitter@jsoverson上提出问题或评论。

本文链接:https://medium.com/@jsoverson/bypassing-captchas-with-headless-chrome-93f294518337

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖