Alter Solutions a participé au Capture The Flag BreizhCTF organisé à Rennes les 1er et 2 avril 2022. Cette année, l’épreuve a connu l’introduction de quelques nouvelles catégories.
Pentest
Une catégorie visant à être réaliste. Chaque défi consiste à prendre le contrôle d’une machine (à la hackthebox). Les équipes n’ont accès qu’à une seule machine et ce n’est qu’après l’avoir « hackée » qu’ils peuvent accéder aux autres machines via « ssh tunneling ».
OSInt
L’objectif est de partir d’informations publiques sur Internet et d’exploiter des indices pour trouver une information critique concernant une personne. Par exemple, fouiller le compte Instagram d’une personne (fictive et créée uniquement pour cette épreuve) pour trouver son nom et son prénom sur une des photos publiées, les destinations de ses voyages, ses plaques d’immatriculation…
Web3
Tout le monde en parle, mais rares sont ceux qui savent ce que c’est. Pour aider les équipes à avoir une meilleure vision sur cette technologie qui fait le « buzz » (la « blockchain » ça vous dit quelque chose ?), les organisateurs ont inclus cette catégorie. Chaque défi se présente sous forme d’un code de « smart contract » en langage Solidity, que nous pouvons déployer sur une blockchain privée, et l’objectif est de trouver des vulnérabilités pour exfiltrer une donnée, vider un compte ou même prendre le contrôle du contrat.
Dans ce blog post, nous allons parler des trois premiers challenges de la catégorie Web3, car c’était une belle occasion pour apprendre à résoudre ce type de challenge, étant donné qu’il s’agit d’une nouvelle catégorie pour notre équipe.
Disclaimer : il y aura certainement des abus de langage par rapport au jargon utilisé par les développeurs Web3. Si vous en voyez, n’hésitez pas à nous contacter sur notre fil twitter @ASF_Cyberteam.
Chauffe-haut :
Figure 1 : Énoncé du challenge « chauffe-haut »
Figure 2 : Code source du premier contrat
Nous remarquons que, selon sa déclaration, l’adresse du chef est publique, donc nous devrions y avoir accès. Après quelques dizaines de minutes de recherche et de lecture de la documentation de la bibliothèque « web3 » de python, nous arrivons à la conclusion qu’il est possible de lire cette variable.
À nos claviers !
Après avoir installé « web3 » et « py-solc-x » à l’aide de pip, nous pouvons commencer à interagir avec le « provider » de la « blockchain ».
Figure 3 : exploit.py – Import des bibliothèques
La première étape est de se connecter au serveur :
Figure 4 : exploit.py – Connexion au serveur
Ensuite, nous compilons le contrat pour fournir son ABI (Abstract Binary Interface) à notre programme :
Figure 5 : exploit.py – Compilation du contrat
Après cette étape, il nous suffit de récupérer le contrat depuis la blockchain, en précisant son adresse (addr) ainsi que son ABI (contract_interface).
Figure 6 : exploit.py – Obtention du contrat depuis le serveur
Finalement, pour récupérer l’adresse du chef, nous n’avons plus qu’à « appeler » la variable et à afficher son contenu sous forme d’adresse.
Figure 7 : exploit.py – Affichage de l’adresse du chef
C’était stupide
Figure 8 : Énoncé du challenge « c’était stupide »
Dans ce défi, il faut devenir « maître » du contrat. En lisant l’énoncé, nous n’avions aucune idée sur la manière d’aborder le défi. Mais après avoir lu le code du contrat, nous remarquons une fonction qui s’appelle « claimMaster »
Figure 9 : contrat.sol – Contenu de la fonction « claimMaster »
Nous constatons que pour pouvoir devenir maître, il faut avoir un solde plus grand que celui du maître actuel.
Figure 10 : contrat.sol – Constructeur du contrat
D’après le code ci-dessus, le maître actuel du contrat est le possesseur du compte qui a appelé le constructeur la première fois. Par la suite, son compte est alimenté de 255000 ether. Notre objectif est donc d’avoir un solde supérieur à 255000 ether. En cherchant des moyens pour changer notre solde, nous trouvons la fonction « buyKKLF » :
Figure 11 : contrat.sol – Fonction « buyKKLF »
Après quelques tests, il s’avère que cette fonction ne peut être appelée que si l’on possède un compte et qu’il contient des ether pour pouvoir payer la « transaction », cela est probablement dû au modificateur « payable ».
Retour à la documentation pour savoir comment créer un compte : nous trouvons une méthode “new_account” que nous pouvons appeler depuis notre script python et qui permet la création d’un compte :
Figure 12 : exploit.py – Création du compte
La blockchain utilise un mécanisme de consensus basé sur l’autorité (Proof of Authority, PoA). Il faut donc le préciser à notre programme :
Figure 13 : exploit.py – Injection de l’algorithme PoA pour le consensus
En essayant d’acheter des KKLF coins en utilisant la fonction « buyKKLF », nous avons eu un message d’erreur qui indiquait que nous n’avions pas de quoi payer la « Gas Fee ».
Comme la majorité des blockchains, chaque transaction implique le paiement de frais pour pouvoir rémunérer les validateurs (mineurs). Il est donc nécessaire d’avoir des ether (attention : il ne s’agit pas de vrais ether, mais de coins valides uniquement sur cette blockchain déployée pour le CTF).
L’auteur des défis Web3 nous a fourni une interface web permettant d’alimenter notre compte jusqu’à un solde maximal de 5 ether.
Il paraît donc envisageable d’utiliser ces 5 ether pour acheter des KKLF coins avec la fonction buyKKLF, puis de réalimenter notre compte de 5 ether et de recommencer en boucle jusqu’à atteindre les 255000 ether KKLF. Cependant, après un simple calcul, en supposant qu’une transaction prenne 1 seconde (spoiler alert : ça prend plus d’une seconde) et en négligeant les frais de transaction, on se rend compte qu’il faudrait déjà 14 h rien que pour réaliser les 51 000 appels à la fonction « buyKKLF », or il ne nous restait plus que quelques heures avant la fin du CTF…
Nous continuons donc à chercher des fonctions qui nous permettent de changer notre solde. On trouve la fonction « transfer » :
Figure 14 : contrat.sol – Fonction « transfer »
Cette fonction vérifie si l’on a un solde strictement positif et fait un transfert d’argent vers le compte « receiver », sans vérifier si l’on a le solde nécessaire pour compléter la transaction.
À partir de là, on peut identifier deux chemins d’attaque possibles :
- Créer deux comptes et transférer un montant supérieur à 255000 ether KKLF vers notre deuxième compte qui deviendra alors maître du contrat.
- Créer un seul compte, avec un solde de 1 KKLF et transférer 2 KKLF au compte master. Ce transfert devrait mettre à jour notre solde à la valeur « -1 », non ? Eh bien non parce que la variable « balances » contient des entiers non signés, ce qui provoquera un dépassement de type « integer underflow » et par conséquent notre solde sera « UINT256_MAX », une valeur bien supérieure au solde du maître (255000).
Figure 15 : contrat.sol – Déclaration de la variable « balances »
La suite du code de notre exploit ressemblera donc à ceci :
Figure 16 : exploit.py – Transfert d’argent et passage en tant que maitre
Après avoir exécuté notre code, nous sommes maintenant maître du contrat. Nous allons donc sur l’interface web du challenge, nous cliquons sur « verify » et si tout s’est bien passé, nous recevons le flag.
Videz leur compte tant que vous y êtes
Figure 17 : Énoncé du challenge « Videz leur compte tant que vous y êtes »
Commençons par regarder le code du contrat :
Figure 18 : contrat.sol – Code source du troisième contrat
Après lecture du code, il est évident que ce qui est demandé est de contourner les vérifications de la fonction « allInOnAShitcoin ».
Pour la première vérification, il suffit de mettre une valeur de 1 ether comme montant de transaction (msg.value).
La deuxième vérification nécessite un peu plus d’informations : il faut appeler la fonction avec un argument contenant une valeur égale au numéro du bloc actuel.
Les numéros des blocs sont incrémentaux. Donc si on arrive à obtenir le numéro du bloc actuel, on peut en déduire le suivant en y ajoutant 1, et appeler la fonction « claimPrizePool ».
Devinez comment faire pour obtenir le numéro du bloc ? Oui ! C’est ça ! Il faut encore consulter la documentation.
Figure 19 : exploit.py – Affectation du numéro de bloc suivant
Maintenant que nous avons la valeur de « entry », nous pouvons appeler la fonction « allInOnAShitcoin » :
Figure 20 : exploit.py – Contournement des vérifications
Vous allez me dire : « Mais la valeur mise dans « value » n’est pas égale à 1 ». Et vous avez totalement raison.
Par défaut, l’unité des valeurs envoyées est le « WEI », la plus petite subdivision d’ether. 1eth = 10^18 wei soit 1000000000000000000 wei.
Après cette transaction, il suffit d’appeler « claimPrizePool » pour obtenir le flag.