Solution du CTF Fetch the Flag 2022 : Moongoose
Jason Lynch
10 novembre 2022
0 minutes de lectureMerci d’avoir participé à Fetch the Flag avec nous ! Félicitations aux milliers de joueurs qui ont participé à Fetch the Flag CTF. Et un grand merci aux Snykers qui ont créé, testé et documenté les défis !
En tant qu’employé de Snyk, j’ai eu l’occasion de rejoindre mes collègues pour tester en avant-première le concours Fetch the Flag 2022 de Snyk. Nous avons pris beaucoup de plaisir à résoudre ces défis ensemble et, personnellement, j’ai beaucoup appris de cette expérience. Voici la solution d’un défi que j’ai particulièrement apprécié : Moongoose.
Le défi
Si vous avez cru qu’ils avaient envoyé une oie sur la Lune
Ce défi commence sur une page de connexion énigmatique sur le thème de la Lune et des oies. Pour trouver le flag, nous devrons exploiter plusieurs vulnérabilités : une traversée de répertoires, une injection NoSQL et un comportement délicat de JavaScript. Comme pour tout autre défi CTF, notre première étape consiste à trouver un point d’entrée.
Guide pas à pas
Trouver un point d’entrée
Vous avez peut-être remarqué que le nom du défi, « Moongoose », ne diffère que d’une lettre de « Mongoose », qui est le nom d’un framework MongoDB populaire pour Node.js. Serait-ce un indice pour trouver la solution ? Voyons si nous pouvons confirmer cette intuition.
Commençons par accéder au défi et nous faire une première idée. Nous voyons :
Un formulaire de connexion
Un formulaire d’inscription
Deux images animées fantaisistes représentant la Lune et une oie
Un superbe arrière-plan étoilé
Tout élément qui accepte une saisie utilisateur est un bon point de départ pour l’analyse. Nous allons donc d’abord nous concentrer sur les formulaires de connexion et d’inscription.

En essayant différentes combinaisons de noms d’utilisateur et de mots de passe dans chaque formulaire, nous obtenons deux messages d’erreur distincts :
Lorsque les champs du nom d’utilisateur et du mot de passe sont tous deux renseignés, nous obtenons :
{ "☾ 𓅬": "invalid moongoose!" }Lorsque l’un de ces champs ou les deux sont vides, nous obtenons :
{"☾ 𓅬":"invalid moongoose: geese have credentials"}
Curieusement, le comportement est identique pour les formulaires de connexion et d’inscription. Ouvrons l’onglet Réseau des outils de développement de notre navigateur pour mieux comprendre ce qui se passe.

Ces captures d’écran ont été réalisées avec Firefox, mais les mêmes principes s’appliquent à tous les principaux navigateurs.
En observant le trafic réseau, nous constatons que les deux formulaires envoient la même requête POST au point de terminaison api/auth. Peu importe donc lequel nous utilisons. L’un des en-têtes de réponse de ces requêtes retient notre attention :
Si vous ne connaissez pas ce nom, Express est un framework de serveur web Node.js très répandu. Pour nous, en tant qu’attaquants, c’est une information potentiellement précieuse. Nous connaissons désormais (ou du moins, nous soupçonnons fortement) le langage, l’environnement d’exécution et le framework utilisés par ce serveur. Cela ne confirme pas que le serveur utilise Mongoose et MongoDB, mais c’est un indice qui vient étayer cette hypothèse.
Si nous suivons cette hypothèse et supposons que MongoDB est utilisé dans le processus de connexion, nous pourrions tenter une injection MongoDB sur le point de terminaison api/auth. Voici une injection de requête tirée de l’excellent site book.hacktricks.xyz :
Dans la syntaxe de requête de MongoDB, $ne est un opérateur qui correspond aux champs dont la valeur est différente de la valeur spécifiée. En SQL, cela s’écrirait ainsi :
Si nous supposons que ce point de terminaison interroge MongoDB pour trouver un utilisateur dont username = X et password = Y, cette injection revient à dire : « donne-moi l’utilisateur dont le nom d’utilisateur n’est pas nul et dont le mot de passe n’est pas nul ». En théorie, cette requête correspondrait à n’importe quel utilisateur valide. Ouvrons notre terminal et essayons avec cURL :
On dirait que ce ne sera pas si simple ! Retournons dans le navigateur et, en gardant l’onglet Réseau ouvert, cherchons d’autres points de terminaison d’API que nous pourrions exploiter. Sur cette capture, j’ai rechargé la page, puis filtré le trafic réseau pour n’afficher que les requêtes dont le chemin contient api.

Nous voyons que l’image de l’oie est chargée par un appel à /api/image/goose.png. Comme le point de terminaison /api/auth, cette réponse contient également l’en-tête X-Powered-By: Express. Nous pouvons probablement supposer que ce même serveur sert les fichiers statiques. Voyons ce qui se passe si nous demandons un autre fichier, comme package.json :
Cette réponse est un sérieux signal d’alerte. On dirait que :
Le serveur tente d’ouvrir sur le disque le fichier que nous avons indiqué
Le code de l’application se trouve probablement dans
/app
Nous ne savons pas encore si ce serveur utilise une bibliothèque ou du code personnalisé pour servir les fichiers statiques. Mais nous pouvons essayer une vulnérabilité courante des serveurs de fichiers statiques : la traversée de répertoires. Le principe de cette exploitation consiste à utiliser ../ pour demander un fichier situé dans le répertoire parent de celui où se trouvent les images. Notez que nous devons encoder la barre oblique dans l’URL (%2f), sans quoi notre requête serait interprétée comme /api/package.json :
Nous avons enfin confirmé notre hypothèse initiale : ce serveur utilise la bibliothèque MongoDB Mongoose. Nous constatons également que le code du serveur principal se trouve dans server.js. Essayons de récupérer ce fichier :
Ça a marché ! Analysons ce serveur pour voir comment accéder au flag.
Analyse du système d’authentification
Voici les sections de server.js qui constituent le système d’authentification :
Il y a beaucoup à analyser. Voici les principaux éléments que j’en retiens :
MongoDB n’est pas utilisé dans le système d’authentification.
Ce code ignore le
usernameet compare uniquement le hash dupasswordà une variableADMIN_HASHcodée en dur.Ce mot de passe est haché à l’aide de bcrypt, un algorithme dont le calcul est très coûteux, ce qui rend la force brute difficile.
Une fois l’authentification réussie, le serveur génère un jeton aléatoire et l’ajoute à l’objet global
SESSIONS. Le jeton est renvoyé au client dans un en-têteAuthorization. Le client peut ensuite envoyer cet en-tête aux points de terminaison qui utilisent le middlewarerequireAuthentication.
Analyse du point de terminaison du flag
Pour récupérer le flag, nous devons :
réussir le contrôle d’authentification
fournir la bonne valeur pour
flagdans le corps de la requête
En demandant le fichier models/user.model.js grâce à notre exploitation de traversée de répertoires, nous voyons que Flag est un modèle Mongoose et que cet appel Flag.find() interroge MongoDB pour trouver les entrées où name = <flag>.
Le gestionnaire de /api/flags ne nettoie ni ne valide le corps de la requête. Il semble donc que nous puissions réutiliser notre injection MongoDB précédente à la place du nom du flag. Mais nous devons d’abord contourner le middleware requireAuthentication().
Assemblons les pièces du puzzle
Comme nous l’avons indiqué plus tôt, il est peu probable que nous puissions casser ADMIN_HASH par force brute dans un délai raisonnable. Pouvons-nous tromper le serveur pour qu’il nous croie déjà authentifiés ?
Examinons la fonction qui valide le jeton de session :
La faiblesse de cette vérification est qu’elle suppose que l’objet SESSIONS ne contient que des jetons de session. Or, l’héritage prototypal de JavaScript signifie que l’objet SESSIONS est initialisé avec des propriétés cachées et non énumérables. Nous pouvons obtenir la liste de ces propriétés en exécutant cette instruction dans un REPL Node.js ou dans l’onglet Console des outils de développement de notre navigateur :
Si notre hypothèse est juste, nous devrions pouvoir utiliser n’importe laquelle de ces propriétés comme jeton de session et réussir la vérification de la fonction checkToken(). En combinant cela avec notre injection MongoDB, nous obtenons la requête finale suivante :
La réponse à cette requête contient tout le contenu de la collection flag, y compris le véritable flag !
Moongoose démasqué !
Ce défi a mis en évidence plusieurs types de vulnérabilités. Nous avons utilisé une traversée de répertoires pour récupérer le code du serveur. Nous avons exploité l’héritage prototypal de JavaScript pour contourner le système d’authentification. Enfin, nous avons utilisé une injection de requête MongoDB pour éliminer toute incertitude au niveau du point de terminaison /api/flags. Cette application a été conçue pour être vulnérable, mais ce type de vulnérabilité existe aussi dans le monde réel. Pour en savoir plus sur les différents types de vulnérabilités, leur exploitation et les moyens de s’en protéger, Snyk Learn est une excellente ressource pratique.
Vous voulez savoir comment nous avons trouvé tous les autres flags ? Consultez notre page Solutions de Fetch the Flag pour découvrir comment nous avons procédé.
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.
