Les secrets du CTF révélés : le défi TopLang de SnykCon 2021 expliqué
Michael Aquilina
6 janvier 2022
0 minutes de lectureSi vous avez assisté à SnykCon 2021, vous vous souvenez peut-être de notre tout premier CTF : Fetch the Flag. Dans ce CTF, TopLang était un défi Web de difficulté moyenne qui nous a valu de nombreux retours positifs. Pour celles et ceux qui l’ont apprécié, cet article explique comment notre équipe s’y est prise pour relever et résoudre ce défi. Mieux encore, vous pouvez l’essayer vous-même en vous rendant sur https://ctf-2021.snyk.io/, puis dans la section Challenges.
Ce défi était un exemple assez classique de ce que l’on appelle une « attaque oracle » utilisant une injection SQL aveugle. Je vais expliquer comment nous avons abordé ce problème en partant du principe que vous ne connaissez pas les défis CTF. Les étapes sont détaillées, mais je suppose que vous avez quelques notions de Python et de SQL.
Voici la description du défi :
Quel est votre langage de programmation préféré ?
Les défis donnent parfois des indices sur la solution. Mais ici, il ne semble pas y avoir d’informations utiles. Alors passons directement à nos premières investigations !
Première investigation
Au début d’un défi Web CTF, il est conseillé de repérer les pages disponibles et leur contenu avant de commencer à écrire du code. Cette première investigation devrait vous donner une idée des vecteurs d’attaque possibles et de l’emplacement probable du flag du CTF. Elle ne devrait pas vous prendre plus de 10 minutes : il suffit de cliquer sur les différents liens disponibles et de noter tout ce qui pourrait être utile.
En ouvrant le lien Web fourni dans la description du défi, nous arrivons sur la page suivante :

On voit un tableau présentant les langages de programmation les plus populaires en 2020 et 2021, ainsi que quelques métadonnées supplémentaires, comme les notes.
Les colonnes semblent pouvoir être triées en cliquant sur leur en-tête. Plus important encore, le tri par certaines colonnes ajoute le paramètre de chaîne de requête sort à l’URL. Par exemple, le tri par la colonne « Jun 2021 » donne l’URL path/?sort=jun2021
Tout type d’entrée que nous pouvons manipuler constitue un vecteur d’attaque potentiel dont nous pouvons tirer parti. Cela semble donc être une piste intéressante à examiner plus tard.

Un autre endroit évident à explorer est le lien Admin panel en bas de la page. En cliquant dessus, nous sommes redirigés vers /admin.php. Il ne semble pas y avoir grand-chose à faire ici sans être connecté.

Si nous allons sur la page de connexion, nous obtenons un formulaire classique :

Nous pouvons essayer quelques combinaisons de connexion courantes, comme « admin » / « admin », mais aucune ne semble fonctionner. Comme il s’agit d’un défi Web, nous pouvons être assez sûrs de ne pas avoir à lancer un outil de cassage de mots de passe tel que THC Hydra pour forcer cette connexion.
Il semble probable que nous devions extraire du serveur les valeurs de login et de password pour nous connecter à ce panneau d’administration.
Très bien, maintenant que nous avons une bonne compréhension des différentes pages, creusons un peu pour voir si quelque chose semble exploitable.
Injection SQL
Les données sur les principaux langages de programmation sont affichées dans un tableau sur la page Web. Il est possible de trier les données de différentes façons à l’aide du paramètre de chaîne de requête sort. À partir de ces informations, il est assez probable que les données soient récupérées et interrogées dans une base de données SQL en arrière-plan.
Si nous avons effectivement affaire à un backend SQL, nous pouvons vérifier si la page est vulnérable aux attaques par injection SQL.
Remarque : Nous pourrions utiliser un outil d’injection SQL comme sqlmap pour faciliter ce défi CTF. En fait, cela fonctionnerait très bien dans bien des cas. Cependant, pendant la compétition, j’ai eu peu de succès avec cet outil sur ce défi. Plutôt que de perdre du temps à comprendre quelles options activer pour qu’il fonctionne correctement, j’ai décidé d’écrire mon propre code d’exploitation. Et puis, écrire soi-même le code de la solution est bien plus amusant !
Nous avons vu que le paramètre de chaîne de requête sort était un vecteur d’attaque potentiel intéressant. La valeur transmise au paramètre sort semble correspondre au nom d’une colonne d’une table de base de données. Voici les valeurs possibles obtenues en cliquant sur les différents en-têtes de colonne :
sort=jun2020sort=jun2021sort=ratingssort=change
Si le nom de cette colonne est transmis à une instruction SQL ORDER BY au moyen d’une mise en forme de chaîne non sécurisée, nous pourrions extraire des données avec une attaque par injection SQL aveugle.
Pour vérifier si le système est vulnérable, nous pouvons essayer de modifier l’ordre des résultats à l’aide d’une instruction CASE avec une condition booléenne. Plus précisément, l’ordre change-t-il selon que l’instruction CASE renvoie True ou False ? Testons les deux valeurs sort suivantes pour voir ce qui se passe :
?sort=”(CASE WHEN 1=1 THEN jun2021 ELSE jun2020)”?sort=”(CASE WHEN 1=0 THEN jun2021 ELSE jun2020)”
À ce stade, il est judicieux de commencer à écrire des scripts pour interagir avec la page Web cible. D’une part, nous aurons probablement besoin d’automatiser certaines opérations très bientôt ; d’autre part, les navigateurs comme Firefox et Chrome ont tendance à déformer et à transformer les entrées complexes contenant des espaces et d’autres caractères spéciaux.
Je connais bien Python, et c’est généralement un excellent langage pour les CTF, car il est facile d’obtenir une solution fonctionnelle avec l’aide de quelques bibliothèques externes.
En installant les bibliothèques requests et BeautifulSoup depuis PyPi, nous pouvons récupérer la page et analyser le code HTML pour repérer les différences dans les résultats. Plus précisément, nous pouvons détecter les changements en vérifiant si l’ordre des langages dans la colonne a changé.
On voit que « C » arrive toujours en tête en 2020 comme en 2021. En revanche, « Go » était le langage le moins apprécié en 2021 et « Fortran » en 2020. Nous pouvons donc écrire du code qui vérifie quel langage est le moins apprécié afin de détecter les différences.
Créons donc une fonction qui renvoie le dernier langage affiché sur la page :
Si nous associons une condition True à Jun2021 et une condition False à Jun2020, nous devrions obtenir « Go » si nous transmettons une instruction True à get_data, et « Fortran » si nous transmettons une instruction False à get_data.
« 1=1 » est un moyen simple de transmettre une condition vraie à SQL, et « 1=0 » peut servir de condition fausse.
Testons cela avec du code !
Voici le résultat :
Réussi ! Nous avons réussi à modifier l’ordre à l’aide d’une condition booléenne ! Ce test suffit à prouver que le paramètre order est vulnérable à une attaque par injection SQL aveugle. Il ne nous reste plus qu’à exploiter cette faille.
Attaque oracle
Notre exploitation par injection SQL aveugle est une forme d’« attaque oracle ». Elle consiste à poser au serveur des questions auxquelles il répond par « oui » ou « non », ce qui nous indique à quel point nous sommes proches de la valeur recherchée.
Dans notre cas, il semble que notre objectif soit de récupérer les valeurs login et password du formulaire du panneau d’administration que nous avons vu précédemment.
Avec une attaque oracle, nous ne pouvons pas demander directement au serveur de nous communiquer l’identifiant et le mot de passe. Mais nous pouvons lui soumettre des hypothèses successives sur ces valeurs et les affiner peu à peu jusqu’à trouver les bonnes réponses.
L’astuce habituelle pour exploiter cette attaque oracle consiste à poser des questions sur des sous-chaînes, en les précisant progressivement à mesure que nous obtenons des réponses positives.
Voici à quoi pourrait ressembler une série de questions posées à l’oracle, sans code :
« L’identifiant commence-t-il par a ? » Serveur : Non
« L’identifiant commence-t-il par b ? » Serveur : Non
« L’identifiant commence-t-il par c ? » Serveur : Oui
« L’identifiant commence-t-il par ca ? » Serveur : Non
« L’identifiant commence-t-il par cb ? » Serveur : Non
« L’identifiant commence-t-il par cc ? » Serveur : Non
« L’identifiant commence-t-il par ce ? » Serveur : Oui
Nous répétons cette opération jusqu’à avoir extrait l’intégralité de l’identifiant, caractère par caractère. Si le mot de passe est également en texte brut, nous pouvons procéder de la même façon pour le récupérer.
Traduisons cela en code. Nous pouvons commencer par écrire une fonction oracle qui renvoie True si la réponse à notre question est « oui » et False si elle est « non ». En modifiant légèrement notre fonction get_data initiale, nous pouvons vérifier quel est le dernier langage du tableau HTML pour déterminer le résultat de nos questions auxquelles il faut répondre par oui ou par non :
Récupération des informations de connexion
Nous savons que nous devons récupérer login et password dans la base de données. Le problème, c’est que nous ne connaissons pas le schéma SQL nécessaire à la rédaction de nos requêtes. Nous pourrions deviner les noms des tables et des colonnes (certaines équipes ont fait ce choix), mais nous avons d’abord extrait les métadonnées de la base de données pour savoir quelles tables et colonnes interroger.
En essayant différentes requêtes propres aux principaux types de backend SQL (SQLite, MySQL, PostgreSQL et SQL Server) et en vérifiant lesquelles ne provoquaient pas de plantage de la page Web, nous avons pu déterminer que le backend utilisait une base de données SQLite.
Interroger les tables et les colonnes disponibles dans une base de données SQLite est simple. Commençons par déterminer quelles tables nous pouvons interroger pour en extraire des données.
La requête suivante nous donnerait la liste de tous les noms de tables d’une base de données SQLite :
Cependant, nous ne pouvons pas transmettre cette requête telle quelle à notre fonction oracle, car elle ne pose pas une question à laquelle on peut répondre par « oui » ou par « non ». Pour résoudre ce problème, nous pouvons concaténer les noms de toutes les tables avec la fonction GROUP_CONCAT, puis proposer des hypothèses sur le résultat de cette concaténation à l’aide de la fonction substr.
En combinant ces éléments, nous obtenons :
Notez que end et guess sont des paramètres de notre requête. Le paramètre guess correspond simplement à notre hypothèse actuelle, et end représente la longueur en caractères de notre hypothèse, plus un.
Il ne nous reste plus qu’à mettre à jour notre code pour envoyer des hypothèses au serveur, caractère par caractère. Notre code doit continuer à envoyer des requêtes jusqu’à ce que l’oracle renvoie True. Dès que nous obtenons une réponse True, nous passons au caractère suivant et répétons le processus pour affiner notre hypothèse.
Voici à quoi ressemble le code :
Lorsque vous exécutez ce code, vous voyez que les noms de toutes les tables nous sont révélés, caractère par caractère :

Si nous attendons la fin du script, le résultat final est « users:languages ». Cela signifie que nous avons les tables users et languages que nous pouvons examiner plus en détail. Comme notre objectif est de nous connecter au panneau d’administration, il est raisonnable de penser que la table « users » est celle qui nous intéresse.
L’étape suivante consiste à déterminer quelles colonnes sont disponibles dans la table users. SQLite dispose d’une fonction PRAGMA_TABLE_INFO(table_name) qui permet d’interroger les noms des colonnes d’une table précise. Dans notre cas, la requête ressemblerait à ceci :
Comme nous voulons toujours envoyer au serveur des questions de type oracle, nous pouvons adapter cette requête à l’aide des fonctions GROUP_CONCAT et substr afin d’obtenir cette charge utile SQL :
Il nous suffit de modifier notre script précédent pour que la variable command contienne cette nouvelle requête.
En exécutant de nouveau le script, nous finirons par obtenir la réponse :
On dirait que nous avons trouvé les colonnes ciblées : login et password. Il ne nous reste plus qu’à envoyer des charges utiles SQL pour en extraire toutes les informations :
Voici la charge utile SQL au format oracle qui permettrait d’extraire les données de login :
Et voici la charge utile SQL au format Oracle pour extraire le champ passworddata :
Pour exécuter ces deux commandes, il suffit de remplacer la valeur de commanddans le même script pour chaque cas, comme nous l’avons fait précédemment. Ces attaques permettront de récupérer deux ensembles de combinaisons identifiant/mot de passe.
Je ne vais pas gâcher le plaisir : je vous laisse découvrir les réponses par vous-même.
Connexion au panneau d’administration
Grâce aux identifiants que nous venons d’extraire, nous pouvons nous connecter au panneau d’administration à l’adresse /admin.php.
Cependant, il semble qu’il nous reste un dernier défi à relever : cette page Web nous accueille :

Heureusement, la solution à cette dernière étape est assez simple. Il est toujours judicieux de vérifier quels cookies sont stockés dans votre navigateur pour voir si nous pouvons les manipuler à notre avantage. Dans l’onglet Stockage de Firefox, voici ce que nous trouvons :

En particulier, la valeur analysée nous indique que le cookie stocke une valeur isAdmin, actuellement égale à 0. Voyons ce qui se passe si nous la remplaçons par 1.
Si nous copions la valeur complète du cookie, nous obtenons :
Sans chercher à comprendre cet encodage, notre équipe a décidé que le plus simple était de remplacer le « 0 » vers la fin de la chaîne par un « 1 », puis de remplacer la valeur du cookie dans notre navigateur :
Après avoir creusé la question une fois le CTF terminé, il est apparu clairement qu’il s’agissait simplement d’un objet sérialisé en PHP. Cependant, je pense qu’il vaut la peine de rappeler que prendre des raccourcis comme nous l’avons fait est tout à fait acceptable dans un défi CTF, où le temps gagné peut faire la différence dans le classement.
Collez-le dans l’onglet Valeur (double-cliquez, puis collez votre cookie modifié), puis actualisez la page : le très convoité drapeau CTF SNYK{...} s’affichera ! J’ai de nouveau masqué le résultat réel pour que vous puissiez essayer par vous-même :

Pour conclure… pour l’instant !
En résumé, voici un bref récapitulatif des étapes suivies :
Nous avons examiné les pages disponibles et découvert une chaîne de requête order, ainsi qu’une page de connexion administrateur.
Nous avons confirmé que la chaîne de requête order était vulnérable aux attaques par injection SQL aveugle.
Nous avons extrait les noms des tables et des colonnes, ainsi que les données d’identification de la page d’administration.
Nous avons modifié les données des cookies dans notre navigateur pour faire croire au serveur que nous étions connectés en tant qu’administrateur, puis récupéré le drapeau Snyk CTF.
J’espère que vous avez apprécié ce compte rendu du CTF et que vous avez peut-être appris quelque chose de nouveau ! Les défis CTF sont un excellent moyen de découvrir des exploits réels et, par conséquent, de mieux vous préparer à vous en défendre dans vos propres systèmes. Nous publierons d’autres comptes rendus prochainement : restez à l’affût de guides plus détaillés.
Enfin, John Hammond (chercheur principal en sécurité chez Huntress) a créé des explications détaillées sur certains des autres CTF Fetch the Flag de SnykCon 2021. Je vous invite à les regarder :
Si vous souhaitez rejoindre l’équipe Recherche en sécurité de Snyk, qui conçoit ces CTF (et bien d’autres choses encore), consultez nos postes à pourvoir !
