TP2e année10 livrablesQuiz corrigé
Lab de cracking de mots de passe
Le film t'a montré ce que vaut une donnée volée. Cet après-midi, tu te mets dans la peau de l'attaquant — sur un jeu 100 % fictif — pour casser des mots de passe hachés, mesurer pourquoi un mauvais hachage est une catastrophe, et coder la vraie parade. Tu codes ton propre cracker.
Par Josselyn C., prof de BTS SIO. Publié le , mis à jour le .
- Objectif
- coder un cracker par dictionnaire, comparer MD5/SHA-1/bcrypt, et implémenter les parades
- On manipule
- Node.js (crypto, bcryptjs), un jeu de hash fictif fourni, un mini-dictionnaire
- Durée
- ~3 h (suite de la matinée The Great Hack)
- Pré-requis
- Node.js installé, un terminal, un éditeur de code
- Pour qui
- SLAM, 2e année
Compétences : C3.3 Sécuriser les équipements et les usages des utilisateurs, C3.5 Assurer la cybersécurité d'une solution applicative
On casse pour comprendre comment protéger. Rien d'autre.
Avant de commencer
Ce qu'on fait, et pourquoi
Ce matin, The Great Hack a montré la donnée personnelle comme un butin. Mais quand un attaquant vole une base d'utilisateurs, il n'y trouve (presque) jamais des mots de passe en clair : il trouve des hash. Pour s'en servir, il doit les casser. Aujourd'hui tu fais ce travail — sur des comptes GSB inventés que tu vas générer toi-même — pour comprendre de l'intérieur pourquoi certains hachages tombent en une seconde et d'autres résistent des siècles. Puis tu codes la défense. C'est le cœur du métier : on n'apprend à défendre qu'en comprenant l'attaque.
Ce que tu vas apprendre
| Concept | Utilité |
|---|---|
| Hachage vs chiffrement | comprendre pourquoi on ne « déhache » pas, mais on « devine » |
| Attaque par dictionnaire | le mécanisme réel du cracking — que tu codes |
| Coût d'un algorithme | pourquoi MD5 tombe et bcrypt résiste (chrono à l'appui) |
| Sel (salt) | pourquoi deux mots de passe identiques ne doivent pas donner le même hash |
| Parades dev | bcrypt, refus des mots de passe fuités, rate limiting |
Prépare ton poste
node --version # doit afficher v18+ (sinon installe Node.js)
mkdir crack-lab && cd crack-lab
npm init -y
npm install bcryptjs
1. Hacher, ce n'est pas chiffrer
Avant de casser, il faut comprendre ce qu'on casse.
| Chiffrement | Hachage | |
|---|---|---|
| Réversible ? | Oui, avec la clé | Non, jamais |
| Sert à… | protéger un message qu'on veut relire | vérifier un mot de passe sans le stocker |
| Exemple | AES, RSA | MD5, SHA-1, bcrypt, argon2 |
Un mot de passe se hache. Le serveur ne stocke jamais azerty, il stocke son empreinte. À la connexion, il re-hache ce que tu tapes et compare les empreintes.
L'attaque par dictionnaire : hacher chaque candidat et comparer. Rien de magique — juste beaucoup d'essais, très vite.
2. Le marché derrière les hash (recon)
D'où viennent ces bases de hash qu'un attaquant cherche à casser ? Du marché des fuites. Tu dois connaître ce terrain — en analyste, par la documentation, jamais en t'y connectant.
| Lieu | Ce qui s'y échange |
|---|---|
| Forums de leaks (type BreachForums) | bases volées ; on gagne des crédits souvent en uploadant d'autres données volées |
| Marketplaces / Telegram | vente et diffusion rapide, avec échantillons de « preuve » |
| Initial Access Brokers | vendent l'accès à un réseau (identifiants VPN/RDP), souvent aux enchères |
| Ransomware leak sites | « name & shame » : publication des données d'une victime qui refuse de payer |
Le format d'une annonce (reconstitué, fictif)
[VENTE] Base "GSB-Corp" — 120 000 comptes — FRESH 2026
Contenu : email, hash (voir échantillon), nom, ville
Prix : 400$ — escrow uniquement
Preuve : 50 lignes sur demande | Vendeur : rep 4.8/5
« Traquer la source », côté défense
Identifier → traquer → neutraliser, au niveau des États, ça donne des saisies : RaidForums (Opération TOURNIQUET, 2022), BreachForums (saisi par le FBI en 2023, puis ses réincarnations re-saisies), Genesis Market (Opération COOKIE MONSTER, 2023). Ces domaines changent en permanence : le savoir utile, c'est la mécanique, pas une URL du jour.
Flag D1
Investigation (une saisie)
Via une source de presse fiable, documente une saisie (RaidForums, BreachForums ou Genesis) : qui, quand, comment, ce qui s'y vendait. Cite l'URL et la date.
Rends : ta fiche (6 lignes) + la source.
3. Fabrique ta « base volée » (fictive)
Pour attaquer, il te faut une cible. Tu vas la générer toi-même : d'abord tu joues le rôle du site GSB qui stocke (mal) les mots de passe de ses employés, ensuite tu joueras l'attaquant qui a volé le fichier.
Étape 1 — Le dictionnaire d'attaque
Crée un fichier dico.txt avec un mini-dictionnaire (les mots de passe les plus courants, façon RockYou) :
123456
password
azerty
soleil
motdepasse
qwerty
iloveyou
gsb2026
marseille
doliprane
admin
bonjour
123456789
loulou
naruto
Étape 2 — Générer la fuite
Crée generer-fuite.js. Ce script joue GSB : il hache les mots de passe de 10 employés fictifs, avec de mauvais algorithmes (MD5, SHA-1) et un bon (bcrypt), et écrit le fichier fuite-gsb.txt.
const crypto = require("crypto");
const bcrypt = require("bcryptjs");
const fs = require("fs");
const md5 = t => crypto.createHash("md5").update(t).digest("hex");
const sha1 = t => crypto.createHash("sha1").update(t).digest("hex");
// 10 employés GSB fictifs : login, mot de passe, algo de stockage
const comptes = [
["arizzo", "soleil", "md5"],
["bmercier", "gsb2026", "md5"],
["cfaure", "123456", "md5"],
["dlopez", "Tr0ub4dour&3!", "md5"], // fort : hors dico
["enguyen", "azerty", "sha1"],
["froux", "soleil", "sha1"], // meme mdp que arizzo !
["gpetit", "doliprane", "sha1"],
["iortiz", "Azerty2026", "md5"], // dico deguise : majuscule + annee
["jbernard", "s0leil", "sha1"], // dico en leet : o -> 0
["hvidal", "marseille", "bcrypt"],
];
let out = "";
for (const [login, mdp, algo] of comptes) {
let hash;
if (algo === "md5") hash = md5(mdp);
else if (algo === "sha1") hash = sha1(mdp);
else hash = bcrypt.hashSync(mdp, 10);
out += `${login}:${algo}:${hash}\n`;
}
fs.writeFileSync("fuite-gsb.txt", out);
console.log("fuite-gsb.txt genere (10 comptes).");
node generer-fuite.js
fuite-gsb.txt genere (10 comptes).
Ouvre fuite-gsb.txt : tu vois 10 lignes de hash. C'est le fichier qu'un attaquant aurait volé. À partir de maintenant, tu es l'attaquant : tu n'as que ça, et ton dico.
Flag A1
Checkpoint (la base)
Lance le script, ouvre fuite-gsb.txt. Combien de comptes sont en MD5, en SHA-1, en bcrypt ? Colle la ligne de cfaure (son hash MD5).
Rends : le décompte par algo + la ligne de cfaure.
4. Étape 1 : casser le MD5
MD5 est rapide : des millions de hachages par seconde sur un simple portable. C'est ce qui le rend catastrophique pour stocker des mots de passe. On attaque.
Construis ton cracker, morceau par morceau
Crée cracker.js. D'abord, charger le dico et la fuite :
const crypto = require("crypto");
const fs = require("fs");
const dico = fs.readFileSync("dico.txt", "utf8").split("\n").filter(Boolean);
const fuite = fs.readFileSync("fuite-gsb.txt", "utf8").split("\n").filter(Boolean);
const md5 = t => crypto.createHash("md5").update(t).digest("hex");
Puis la boucle d'attaque sur les lignes MD5 :
for (const ligne of fuite) {
const [login, algo, hash] = ligne.split(":");
if (algo !== "md5") continue; // on ne traite que MD5 ici
let trouve = null;
for (const candidat of dico) {
if (md5(candidat) === hash) { trouve = candidat; break; }
}
console.log(login, "->", trouve ?? "(non cassé)");
}
node cracker.js
arizzo -> soleil
bmercier -> gsb2026
cfaure -> 123456
dlopez -> (non cassé)
iortiz -> (non cassé)
Flag B1
Production (ton cracker MD5)
Fais tourner ton cracker. Rends : ton code + la sortie (les mots de passe cassés). Combien sur les 5 comptes MD5 ? Les deux qui résistent le font pour des raisons différentes : lesquelles ?
5. Étape 2 : SHA-1 et le problème du sel
SHA-1 est un peu plus « sérieux » que MD5… mais toujours rapide, et surtout, dans ta fuite, il est non salé. On l'attaque de la même façon — il suffit d'ajouter SHA-1 à ton cracker.
const sha1 = t => crypto.createHash("sha1").update(t).digest("hex");
// dans la boucle, remplace la condition MD5 par :
for (const ligne of fuite) {
const [login, algo, hash] = ligne.split(":");
if (algo !== "md5" && algo !== "sha1") continue;
const fn = algo === "md5" ? md5 : sha1;
let trouve = null;
for (const candidat of dico) {
if (fn(candidat) === hash) { trouve = candidat; break; }
}
console.log(login, `(${algo})`, "->", trouve ?? "(non cassé)");
}
arizzo (md5) -> soleil
bmercier (md5) -> gsb2026
cfaure (md5) -> 123456
dlopez (md5) -> (non cassé)
enguyen (sha1) -> azerty
froux (sha1) -> soleil
gpetit (sha1) -> doliprane
iortiz (md5) -> (non cassé)
jbernard (sha1) -> (non cassé)
Flag B2
Production (SHA-1 + le sel)
Étends ton cracker au SHA-1 et fais-le tourner. Puis, dans un petit script, hache soleil deux fois en SHA-1 et montre que le résultat est identique.
Rends : la sortie du cracker + la preuve des deux hash identiques + une phrase expliquant en quoi c'est un problème.
6. Étape 3 : bcrypt résiste (mesure-le)
Passons au compte hvidal, stocké en bcrypt. Même dico, même méthode… mais tu vas sentir la différence. bcrypt est lent volontairement.
const bcrypt = require("bcryptjs");
for (const ligne of fuite) {
const [login, algo, hash] = ligne.split(":");
if (algo !== "bcrypt") continue;
let trouve = null;
for (const candidat of dico) {
if (bcrypt.compareSync(candidat, hash)) { trouve = candidat; break; }
}
console.log(login, "(bcrypt) ->", trouve ?? "(non cassé)");
}
Ça finit par trouver marseille… mais mesure le temps. Ajoute un chrono :
const t0 = Date.now();
// ... la boucle bcrypt ci-dessus ...
console.log("temps :", Date.now() - t0, "ms pour", dico.length, "essais");
hvidal (bcrypt) -> marseille
temps : 1450 ms pour 15 essais
Fais parler les chiffres
Mesure le temps par essai pour chaque algo (hache 10 000 fois test) :
function chrono(nom, fn) {
const t0 = Date.now();
for (let i = 0; i < 10000; i++) fn("test" + i);
console.log(nom, ":", Date.now() - t0, "ms pour 10 000 hachages");
}
chrono("md5", md5);
chrono("sha1", sha1);
chrono("bcrypt", t => bcrypt.hashSync(t, 10));
md5 : 12 ms pour 10 000 hachages
sha1 : 15 ms pour 10 000 hachages
bcrypt : 68000 ms pour 10 000 hachages
| Algo | ~ temps / hachage | Casser un dico de 14 M (RockYou) |
|---|---|---|
| MD5 | ~0,001 ms | quelques secondes |
| SHA-1 | ~0,0015 ms | quelques secondes |
| bcrypt (coût 10) | ~7 ms | ~1 jour… par compte, et ça grimpe avec le coût |
Flag B3
Production (le tableau de mesure)
Fais tourner le chrono chez toi. Rends : ton tableau des 3 temps (tes vraies valeurs) + une phrase : pourquoi le même dico casse MD5 « pour rien » et rend bcrypt impraticable ?
7. Améliorer l'attaque : les règles de mutation
Ton cracker teste les mots du dico bruts. Mais personne n'écrit soleil tout nu : on écrit Soleil, soleil2026, s0leil, Soleil!. Un attaquant sérieux applique des règles de mutation à chaque candidat — c'est précisément ce qui fait tomber iortiz (Azerty2026) et jbernard (s0leil), que ton cracker basique a ratés.
Étape 1 — Générer les variantes d'un mot
function variantes(mot) {
const cap = mot[0].toUpperCase() + mot.slice(1); // Soleil
const leets = [
s => s.replace(/o/g, "0"), // s0leil
s => s.replace(/a/g, "@"),
s => s.replace(/e/g, "3"),
];
const set = new Set();
for (const base of [mot, cap]) {
set.add(base);
for (const suf of ["2024","2025","2026","!","123"]) set.add(base + suf);
for (const leet of leets) set.add(leet(base));
}
return [...set];
}
console.log(variantes("soleil"));
Étape 2 — Brancher les règles sur le cracker
for (const ligne of fuite) {
const [login, algo, hash] = ligne.split(":");
if (algo === "bcrypt") continue;
const fn = algo === "md5" ? md5 : sha1;
let trouve = null;
for (const candidat of dico) {
for (const v of variantes(candidat)) { // <-- la seule nouveauté
if (fn(v) === hash) { trouve = v; break; }
}
if (trouve) break;
}
console.log(login, `(${algo})`, "->", trouve ?? "(non cassé)");
}
dlopez (md5) -> (non cassé)
iortiz (md5) -> Azerty2026
jbernard (sha1) -> s0leil
Flag B5
Production (le cracker à règles)
Ajoute variantes() à ton cracker et relance. Rends : ton code + la sortie. Combien de comptes en plus tombent grâce aux règles ? Lequel résiste toujours, et pourquoi ?
8. Quand le dico échoue : brute force & entropie
dlopez résiste à ton dico, même avec règles. Il reste une arme : la force brute — tester toutes les combinaisons possibles. Elle marche toujours… en théorie. En pratique, elle explose vite.
Démo — casser un code PIN à 4 chiffres
const cible = md5("4271"); // un PIN "volé" (fictif)
const t0 = Date.now();
let found = null;
for (let i = 0; i <= 9999; i++) {
const pin = String(i).padStart(4, "0"); // 0000, 0001, ... 9999
if (md5(pin) === cible) { found = pin; break; }
}
console.log("PIN:", found, "en", Date.now() - t0, "ms");
PIN: 4271 en 6 ms
Pourquoi la longueur gagne (et pas la « complexité »)
Chaque caractère ajouté multiplie l'espace de recherche. Voici l'explosion, à ~1 milliard de hachages/seconde (un GPU sur du MD5) :
| Mot de passe | Espace de recherche | Temps de brute force (MD5, GPU) |
|---|---|---|
| 4 chiffres | 10⁴ = 10 000 | instantané |
| 6 lettres minuscules | 26⁶ ≈ 3 × 10⁸ | < 1 seconde |
| 8 lettres minuscules | 26⁸ ≈ 2 × 10¹¹ | ~3 minutes |
| 8 (maj + min + chiffres) | 62⁸ ≈ 2 × 10¹⁴ | ~2,5 jours |
| 12 (maj + min + chiffres) | 62¹² ≈ 3 × 10²¹ | ~90 000 ans |
| 16 caractères aléatoires | 94¹⁶ ≈ 4 × 10³¹ | bien au-delà de l'âge de l'univers |
Flag B6
Production (le brute-forcer)
Code le brute-forcer de PIN et mesure son temps chez toi. Puis, sans le coder, estime : combien de temps pour un PIN à 6 chiffres (× combien de plus) ? Et explique en une phrase pourquoi un attaquant préfère toujours essayer le dictionnaire avant la force brute.
Rends : ton code + le temps mesuré + tes deux réponses.
9. La parade côté développeur
Tu as vu l'attaque de l'intérieur. Maintenant, code ce qui rend une base inexploitable même si elle fuit. Trois règles.
Règle 1 — Stocker en bcrypt (jamais MD5/SHA-1/clair)
const bcrypt = require("bcryptjs");
// Inscription
const hash = bcrypt.hashSync(motDePasse, 12); // 12 = coût, salé automatiquement
// on stocke `hash`, jamais `motDePasse`
// Connexion
const ok = bcrypt.compareSync(saisie, hash); // true / false, sans "déchiffrer"
Règle 2 — Refuser un mot de passe déjà fuité
On branche l'API Pwned Passwords (modèle k-anonymity : on n'envoie que 5 caractères du hash, le secret ne quitte jamais le serveur).
async function estFuite(pwd) {
const sha1 = crypto.createHash("sha1").update(pwd).digest("hex").toUpperCase();
const prefixe = sha1.slice(0, 5), suffixe = sha1.slice(5);
const rep = await fetch("https://api.pwnedpasswords.com/range/" + prefixe);
return (await rep.text()).split("\n").some(l => l.split(":")[0] === suffixe);
}
// à l'inscription : if (await estFuite(mdp)) refuser("mot de passe trop courant");
Règle 3 — Ralentir & renforcer
| Parade | Ce que ça bloque |
|---|---|
| Rate limiting (ex. 5 essais / 15 min) | le credential stuffing et le brute force en ligne |
| 2FA / MFA | un mot de passe cassé ne suffit plus à entrer |
| Longueur > complexité (min. 12) | casse les mots de passe type dico (la longueur = l'entropie) |
Flag B4
Production (l'inscription sécurisée)
Écris une fonction inscrire(login, mdp) qui : (1) refuse un mot de passe présent dans ton dico.txt (blocklist locale), (2) sinon le hache en bcrypt coût 12 et renvoie la ligne à stocker. Teste avec 123456 (refusé) et un mot de passe long inventé (accepté).
Rends : ton code + les deux sorties.
Flag E1
Extension (Pwned Passwords en vrai)
Pour les rapides : implémente estFuite (règle 2) et teste-la avec password123 (doit être fuité) puis un mot de passe long inventé (doit passer).
Rends : ton code + les 2 résultats.
10. Pour aller plus loin : ton cracker v2
Le cracker que tu as codé est un jouet — mais un jouet qui marche. Voici la feuille de route pour en faire un vrai outil. Tu n'as pas à tout faire : choisis les niveaux qui t'attirent, construis, et mesure. C'est un terrain de jeu idéal pour un futur pentester, et un excellent projet de portfolio.
Les niveaux de montée en puissance
| Niveau | Ce que tu ajoutes | La notion derrière |
|---|---|---|
| 1 · Multi-hash auto | deviner l'algo par la longueur du hash (32 = MD5, 40 = SHA-1, débute par $2 = bcrypt) au lieu de le lire dans le fichier | reconnaissance de format |
| 2 · Masques | brute force ciblé : générer « prénom + 4 chiffres » plutôt que tout l'espace (masque ?l?l?l?d?d?d?d) | réduire l'espace de recherche |
| 3 · Combinateur | coller deux mots du dico (soleil+lune → soleillune) | attaque combinatoire |
| 4 · Vraie wordlist | brancher rockyou.txt (dépôt SecLists) à la place du mini-dico | passage à l'échelle |
| 5 · Performance | mesurer le hashrate, paralléliser (Worker threads), afficher une barre de progression | optimisation |
| 6 · Rapport | produire un rapport final (comptes cassés, temps, algo le plus faible, recommandations) | livrable de pentester |
Les vrais outils à explorer (hors classe, légal)
- John the Ripper et hashcat — les crackers de référence (règles, masques, GPU). Lis leur documentation avant de les lancer.
- SecLists — le dépôt public des wordlists (dont
rockyou.txt). - OWASP Password Storage Cheat Sheet — la référence défensive : quel algo, quel coût, aujourd'hui.
- Root-Me, TryHackMe, Hack The Box — catégories « cracking » / « crypto » pour t'entraîner légalement, sur des cibles prévues pour ça.
Flag E2
Le grand défi (sur la semaine)
Construis GSB-Crack v2 avec au moins 3 niveaux de la feuille de route. Dépose ton dépôt Git + un court README qui explique : tes choix, ce que ton cracker attrape en plus, et tes mesures de vitesse. Bonus : lance-le sur ton fuite-gsb.txt régénéré tout en bcrypt coût 12 — que se passe-t-il, et que conclus-tu pour un développeur ?
Rends : le lien du dépôt + ton README.
Clôture
Mini-quiz (corrige-toi seul)
Exercice Réussi À revoir
| # | Question | Ta réponse |
|---|---|---|
| 1 | Pourquoi ne peut-on pas « déhacher » un mot de passe, mais peut-on le « cracker » ? | … |
| 2 | Pourquoi MD5 est-il catastrophique pour stocker des mots de passe ? | … |
| 3 | À quoi sert le sel, concrètement ? | … |
| 4 | Un mot de passe hors dictionnaire mais stocké en MD5 : est-il en sécurité ? Nuance. | … |
Voir le corrigéMasquer le corrigé de l'exercice
1. Le hachage est irréversible ; on ne l'inverse pas, on teste des candidats jusqu'à retomber sur le même hash. 2. Il est trop rapide : des millions d'essais/seconde → un dico entier passe en un clin d'œil. 3. Rendre chaque hash unique même pour un mot de passe identique → tue les tables précalculées et masque les réutilisations. 4. Mieux protégé contre un dictionnaire, mais un attaquant motivé peut passer au brute force ou à des règles ; et si l'algo est rapide, ça reste risqué. La vraie sécurité = mot de passe long et algo lent salé.
Tu avais trouvé ?
Glossaire
| Terme | Définition |
|---|---|
| Hachage | empreinte irréversible d'une donnée (MD5, SHA-1, bcrypt, argon2) |
| Attaque par dictionnaire | hacher une liste de candidats probables et comparer au hash volé |
| Brute force | tester toutes les combinaisons possibles (plus lent que le dictionnaire) |
| Sel (salt) | valeur aléatoire ajoutée avant hachage → empreintes uniques |
| Rainbow table | table géante de hash précalculés ; inutile si les hash sont salés |
| Règle de mutation | transformation appliquée à un candidat (majuscule, +année, leet) pour élargir le dico |
| Masque | brute force ciblé sur un motif connu (ex. ?l?l?l?d?d?d?d) |
| Entropie | mesure de la force d'un mot de passe en bits (= log₂ de l'espace de recherche) |
| Coût / facteur de travail | paramètre de lenteur d'un algo (bcrypt : 10, 11, 12…) |
| k-anonymity | interroger une base en ne révélant qu'un préfixe du hash |
Compétences mobilisées (référentiel BTS SIO SLAM)
Bloc 3 · Cybersécurité des services informatiques
→ C3.3 Sécuriser les équipements et les usages des utilisateurs : identifier les menaces (dictionnaire, brute force, réutilisation) et mettre en œuvre les défenses.
→ C3.5 Assurer la cybersécurité d'une solution applicative et de son développement : prévenir les attaques et prendre en compte la sécurité dans le développement (hachage bcrypt, salage, rate limiting).