Garrit CTF

Le blog de Garrit propose un challenge de type "Capture the Flag" dont le but est de trouver 6 flags (ou drapeaux) cachés sur son site. J'ai vu passer plusieurs fois ce blog sur Hacker News et j'ai trouvé l'idée de cacher un CTF excellente sans jamais prendre le temps de participer auparavant.

Comme le souligne l'auteur, le blog étant open-source le but n'est pas de se gâcher le plaisir de la recherche en consultant le code mais plutôt de fouiller le site pour les dénicher.

L'ordre de découverte des flags détaillé dans cet article est arbitraire et ne représente pas la difficulté de recherche mais dépend simplement de l'ordre dans lequel j'ai effectué mes recherches.

Comme précisé dans l'article expliquant les règles, tous les drapeaux sont de la forme GXYZ{***********}.

Premier drapeau : robots.txt

Un grand classique des CTF sur des sites web est de cacher une référence à un drapeau dans le fichier robots.txt. Ce fichier est utilisé par les robots des moteurs de recherche lors de l'indexation d'un site internet pour identifier les pages qu'ils peuvent ou non indexer.

Une erreur de débutant est d'ajouter une référence à un fichier ou dossier sensible en demandant son exclusion de l'indexation. Le fichier ou dossier n'apparait alors pas sur les moteurs de recherche, mais le fichier robots.txt étant accessible par tous, il permet de récupérer des informations sur des éléments sensibles.

Commençons par requêter le fichier en question en utilisant notre terminal :

$ curl garrit.xyz/robots.txt
User-agent: *
Allow: /posts
Allow: /cv
Allow: /todo
Allow: /blogroll

Disallow: /flag.txt
Disallow: /git/HEAD

# Sitemaps
Sitemap: https://garrit.xyz/sitemap-0.xml%

Le fichier a un contenu standard, on peut cependant remarquer la présence de la référence au fichier flag.txt. Si on effectue une nouvelle requête vers ce fichier on obtient :

$ curl garrit.xyz/flag.txt
UjFoWlduczNhRFEzWDNjME5WOHpORFY1ZlE9PQ==

Le fichier semble contenir du texte encodé en base64, essayons de le déchiffrer :

$ curl -s garrit.xyz/flag.txt | base64 --decode
R1hZWns3aDQ3X3c0NV8zNDV5fQ==

On obtient une nouvelle chaîne de caractères en base64. Essayons de décoder celle-ci :

$ curl -s garrit.xyz/flag.txt | base64 --decode | base64 --decode
GXYZ{7h47_w45_345y}

Et voici notre premier drapeau ! C'était facile !

Second drapeau : code source

Comme tout les CTF identiques, la difficulté principale est de trouver ou chercher le prochain drapeau. Ici après quelques secondes de réflexion sur le prochain endroit ou chercher, une solution possible me semble être de chercher dans le code HTML de chaque page du site.

Commençons par la page expliquant les règles du CTF :

$ curl -s garrit.xyz/ctf | grep -oE "GXYZ\{.*\}"
GXYZ{***********}

La commande grep -oE permet de chercher en utilisant une expression régulière (le paramètre E) et de n'afficher que le résultat exact et non l'ensemble de la ligne (le paramètre o)

On obtient seulement l'exemple de drapeau écrit dans les règles. Après quelques essais, je remarque en regardant les ressources chargés sur chaque page du site internet que le favicon, l'image représentant un site internet affiché dans l'onglet des navigateurs internet n'est pas à aux formats ico ou png habituels, mais au format svg. Contrairement à des formats d'images compressés le format svg est écrit avec des balises HTML. Essayons de chercher dans son contenu :

$ curl -s garrit.xyz/favicon.svg | grep -oE "GXYZ\{.*\}"
GXYZ{y0u_423_my_f4v02173}

Troisième drapeau : flag.png

Intuitivement le fichier https://garrit.xyz/assets/white_flag.png présent sur la page des règles du CTF semble suspect après la découverte du précédent drapeau, caché dans un fichier d'image.

Essayons de chercher dans le fichier white_flag.png :

$ curl -s https://garrit.xyz/assets/white_flag.png | grep -oE "GXYZ\{.*\}"
Binary file (standard input) matches

Ici grep nous indique qu'il a trouvé quelque chose mais l'image étant au format binaire il n'affichera pas le résultat. Utilions les paramètres -a pour traiter le fichier d'entrée comme du texte standard et -z pour continuer la recherche sur plusieurs lignes :

curl -s https://garrit.xyz/assets/white_flag.png | grep -azoE "GXYZ\{.*\}"
GXYZ{d0n7_91v3_up}

Et c'est un troisième drapeau !

Quatrième drapeau : en-têtes

Ma prochaine idée à vérifier était de contrôler les en-têtes des réponses du serveur, autre endroit ou l'on peut trouver des drapeaux dans de nombreux CTF. Regardons :

$ curl -I garrit.xyz
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 4472
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Last-Modified: Thu, 24 Nov 2022 12:45:32 GMT
Access-Control-Allow-Origin: *
ETag: "637f676c-1178"
expires: Thu, 24 Nov 2022 18:33:18 GMT
Cache-Control: max-age=600
x-proxy-cache: MISS
X-GitHub-Request-Id: D4F2:1371C:1A9EB1:1B4E72:637FB696
Accept-Ranges: bytes
Date: Thu, 24 Nov 2022 18:59:10 GMT
Via: 1.1 varnish
Age: 0
X-Served-By: cache-cdg20783-CDG
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1669316351.517326,VS0,VE93
Vary: Accept-Encoding
X-Fastly-Request-ID: 8aa04498f38d64f31fa60e5ad9f95dd6b405238c

Comme on peut malheureusement le voir il n'y a pas de drapeau ici et pire, le site semble être hébergé sur Github pages un service fourni par Github permettant d'héberger des sites statiques. Les utilisateurs ne gèrent pas eux mêmes les serveurs, donc n'ont pas de possibilité de créer des en-têtes personnalisé.

En surveillant les requêtes effectuées au chargement de la page des règles du CTF j'ai cependant remarqué qu'une requête était effectuée vers l'url https://jurassic.garrit.xyz/ et retournait une erreur 405. Les erreurs 405 correspondent à l'erreur Method Not Allowed, qui peut survenir lorsqu'on utilise le mauvais type de requête (par exemple une requête GET au lieu d'une requête POST). Ici c'est une requête GET qui déclenche l'erreur.

Commençons par essayer de récupérer les en-têtes de la page :

$ curl -I https://jurassic.garrit.xyz
HTTP/2 405
access-control-allow-methods: HACK
access-control-allow-origin: *
date: Thu, 24 Nov 2022 19:06:57 GMT

La présence de l'en-tête access-control-allow-methods: HACK nous indique que l'on est sur la bonne voie. Essayons d'effectuer une requête en utilisant le type fictif indiqué HACK pour voir si cela change quelque chose :

$ curl -X HACK https://jurassic.garrit.xyz
Na ah ah, you didn't say the Magic-Word!
Na ah ah, you didn't say the Magic-Word!
Na ah ah, you didn't say the Magic-Word!
Na ah ah, you didn't say the Magic-Word!
Na ah ah, you didn't say the Magic-Word!

La réponse du serveur est une référence à une scène du premier film Jurassic Park ou les protagonistes cherchent à accéder à un ordinateur. Le mot-magique est please, essayons d'envoyer ce motavec curl :

$ curl -X HACK -d "please" https://jurassic.garrit.xyz

Malheureusement la réponse est toujours la même. Idem en utilisant l'envoi de donnée au format urlencoded ou json :

$ curl -X HACK -d "Magic-Word=please" https://jurassic.garrit.xyz
...
$ curl -X HACK -d '{"Magic-Word": "please"}' https://jurassic.garrit.xyz

Essayons d'envoyer un en-tête personnalisé :

$ curl -X HACK https://jurassic.garrit.xyz -H "Magic-Word: please"
GXYZ{w31c0m3_2_ju24551c_p42k}

Et nous avons un quatrième drapeau !

On peut noter que la réponse Na ah ah, you didn't say the Magic-Word! nous donnait un (petit) indice sur le fait qu'il faille envoyer un en-tête : de manière générale tous les en-têtes HTTP composés de plusieurs mots ont la première lettre de chacun d'eux en majuscule et sont séparés par des tirets.

Cinquième drapeau : git

Une fois de plus je suis à court d'idée concernant l'emplacement d'un autre drapeau. Une des premières hypothèses à laquelle j'ai pensé était que le nom de domaine jurassic.garrit.xyz avait dû demander un travail de déploiement et de paramétrage à l'auteur du blog et qu'il y avait peut être un autre drapeau présent sur le même serveur.

Malheureusement après avoir lancé un scan nmap des ports les plus communs, seulement 4 d'entre eux sont ouverts :

$ nmap -sV jurassic.garrit.xyz
Starting Nmap 7.93 ( https://nmap.org ) at 2022-11-28 20:32 CET
Nmap scan report for jurassic.garrit.xyz (195.201.141.84)
Host is up (0.033s latency).
rDNS record for 195.201.141.84: static.84.141.201.195.clients.your-server.de
Not shown: 996 filtered tcp ports (no-response)
PORT    STATE  SERVICE  VERSION
22/tcp  open   ssh      OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
25/tcp  closed smtp
80/tcp  open   http     Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
443/tcp open   ssl/http Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 67.41 seconds

Les ports 80 et 443 sont dédiés à la navigation web, le 22 permet de se connecter en ssh au serveur et le 25 semble correspondre à du smtp (permettant l'envoi d'email). Rien ne semble inhabituel à sur ce serveur.

Parmi les autres idées qui me sont venues en tête ; un éventuel drapeau caché dans les informations Whois (requêtable via la commande whois garrit.xyz), mais là encore chou-blanc.

Après avoir refait une passe sur les précédents flags trouvés, j'ai remarqué que le fichier robots.txt contient une entrée qui n'a pas de raison d'être présente : Disallow: /git/HEAD.

Même si le site est hébergé sur Github pages il n'y a aucune raison à ce qu'une copie du de la configuration git soit hébergée parmi les fichiers statiques. Si l'on requête le fichier HEAD on obtient :

$ curl https://garrit.xyz/git/HEAD
ref: refs/heads/main

Le fichier HEAD est une référence à l'état de la branche active du repository. Ici on peut voir que la branche utilisée est main. Si on requête le fichier main on obtient :

$ curl https://garrit.xyz/git/refs/heads/main
93cb27bca2374c50863b254de639b0448dded563

Cette référence indique le hash du commit courant de la branche main.

On sait que si l'on regarde dans le sous-dossier objects du dossier git, on devrait trouver un dossier nommé avec Les 2 premières lettres du hash du dernier commit et un fichier dans ce dossier nommé avec les lettres restantes. Essayons de récupérer ce fichier

$ curl https://garrit.xyz/git/objects/93/cb27bca2374c50863b254de639b0448dded563

Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.

Curl nous prévient que la donnée en sortie contient des caractères au format binaire. En effectuant des recherches, on peut voir que git utilise zlib pour compresser les objets de commit.

Après avoir enregistré le contenu de l'objet de commit en local avec la commande :

curl https://garrit.xyz/git/objects/93/cb27bca2374c50863b254de639b0448dded563 > object-93

On peut utiliser Python pour lire le contenu de ce fichier :

import zlib


object_data = open("./object-93", "rb").read()
uncompressed_data = zlib.decompress(object_data)
print(uncompressed_data.decode("utf-8"))

Si on exécute ce script on obtient :

commit 230tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 6b04fdbf7dd55d5cb0353e35e93532e7c4217735
author Garrit Franke <garrit@slashdev.space> 1657657984 +0200
committer Garrit Franke <garrit@slashdev.space> 1657657984 +0200

Cleanup

L'entrée parent nous donne le hash du commit précédent. Si l'on récupère l'objet correspondant à ce dernier avec la même méthode que précédemment :

curl https://garrit.xyz/git/objects/6b/04fdbf7dd55d5cb0353e35e93532e7c4217735 > object-6b

Et que l'on relance notre script python en ciblant ce nouveau fichier on obtient :

$ python ./git_object_reader.py
commit 189tree 43a2fd848722156133c3e4e8188434f181dffa69
author Garrit Franke <garrit@slashdev.space> 1657657549 +0200
committer Garrit Franke <garrit@slashdev.space> 1657657549 +0200

Initial Commit

Pas de commit parent ici, par contre tout comme pour le fichier objet précédent on peut voir que les hash de commits ne correspondent pas au nom des fichiers objet, probablement parce qu'ils sont issus d'une autre branche. Essayons de requêter les objets de commits avec les hash suivants :

Le premier ne nous avance guère :

$ curl https://garrit.xyz/git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 > object-4b
$ python ./git_object_reader.py
tree 0

Le second nous retourne une erreur de decoding des caractères :

$ curl https://garrit.xyz/git/objects/43/a2fd848722156133c3e4e8188434f181dffa69 > object-43
$ python ./git_object_reader.py
python object.py
Traceback (most recent call last):
  File "object.py", line 6, in <module>
    print(uncompressed_data.decode("utf-8"))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xac in position 24: invalid start byte

Si l'on enlève la méthode decode("utf8") dans notre code Python, on obtient le résultat sous forme de bytes :

b'tree 36\x00100644 flag.txt\x00\xac\x1c\x8e\xce\xfe\xfd\xcao*\x1f\xc0\xea;\xcf=3>\xce\xe0\xa4'

J'ai eu la flemme de chercher quel était l'encoding / le format de compression utilisé pour afficher le détail des informations de ce commit et j'ai eu l'idée de me tourner vers les outils fourni par git. En créant un nouveau dossier contenant un dossier .git et en répliquant l'architecture des différents fichier que l'on a obtenu jusqu'ici on obtient :

.git
├── HEAD
├── objects
│   ├── 43
│   │   └── a2fd848722156133c3e4e8188434f181dffa69
│   ├── 4b
│   │   └── 825dc642cb6eb9a060e54bf8d69288fbee4904
│   ├── 6b
│   │   └── 04fdbf7dd55d5cb0353e35e93532e7c4217735
│   └── 93
│       └── cb27bca2374c50863b254de639b0448dded563
└── refs
    └── heads
        └── main

Cela nous permet d'utiliser des outils mis à disposition par git, notamment git cat-file, qui permet de lire le contenu d'un objet de commit :

$ git cat-file -p 43a2fd848722156133c3e4e8188434f181dffa69
100644 blob ac1c8ecefefdca6f2a1fc0ea3bcf3d333ecee0a4	flag.txt

On obtient un nouveau hash de commit ; ac1c8ecefefdca6f2a1fc0ea3bcf3d333ecee0a4. Si l'on récupère le fichier correspondant au hash de commit et que l'on ré-utilise git cat-file, on obtient notre drapeau :

git cat-file -p ac1c8ecefefdca6f2a1fc0ea3bcf3d333ecee0a4
Congrats on finding this one! :)

GXYZ{917_0n_my_1v1}

Sixième drapeau : DNS

Après avoir longuement cherché en vain le sixième et dernier drapeau j'ai contacté l'auteur du blog en listant les différentes pistes que j'avais suivi. Celui-ci m'a conseillé de re-vérifier les recherches que j'avais effectué au niveau des DNS et, effectivement, si l'on utilise la commande nslookup pour lister les enregistrements DNS on obtient :

$ nslookup -type=any garrit.xyz
Server:		2a02:8428:2586:8f01::1
Address:	2a02:8428:2586:8f01::1#53

Non-authoritative answer:
garrit.xyz	nameserver = shades16.rzone.de.
garrit.xyz	nameserver = docks12.rzone.de.
garrit.xyz	text = "GXYZ{700_m4ny_d0m41n5}"
Name:	garrit.xyz
Address: 185.199.108.153
garrit.xyz	text = "openpgp4fpr:2218337E54AA1DBE207B404DBB54AF7EB0939F3D"
garrit.xyz
	origin = docks12.rzone.de
	mail addr = hostmaster.strato-rz.de
	serial = 2020052158
	refresh = 86400
	retry = 7200
	expire = 604800
	minimum = 300
garrit.xyz	has AAAA address 2606:50c0:8000::153
garrit.xyz	text = "google-site-verification=__l7xKuC_9-wXdBGAUv-p-mDPi2V6VAvt6b2YqF85Jc"
garrit.xyz	mail exchanger = 5 smtpin.rzone.de.

Authoritative answers can be found from:

On peut voir que l'enregistrement DNS du nom de domaine a une entrée TXT ayant pour valeur le dernier drapeau : GXYZ{700_m4ny_d0m41n5}.

Conclusion

J'ai trouvé ce challenge vraiment amusant, tout particulièrement les drapeaux sur le serveur jurassic et dans la copie de dépôt git. Je pense que c'est également un excellent exercice que de s'essayer à la création d'épreuves pour des challenges de ce type, en arrivant à trouver un point d'équilibre dans le trio difficulté / originalité / accessibilité.