# CLAUDE.md — Méthodologie Pentest Web
> Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour guider les missions de test d'intrusion web.
---
## Quand utiliser ce contexte
- ✅ Audit de sécurité d'une application web (API REST, SPA, WebSocket)
- ✅ Test d'intrusion autorisé avec périmètre défini par écrit
- ✅ Review de code orientée sécurité (CORS, auth, cookies, injections)
- ✅ Rédaction de rapport de pentest avec CVSS et recommandations
- ❌ Toute cible sans autorisation écrite préalable — illégal
- ❌ Infrastructure réseau / pentest interne : ce contexte couvre uniquement la couche web applicative
- ❌ Tests en production sans validation explicite du commanditaire
---
## Section 1 : Les 5 phases du pentest web
### Phase 1 — Recon (reconnaissance)
Collecter un maximum d'informations sans générer de trafic suspect vers la cible.
**DNS :**
```python
import subprocess
target = "target.com"
for record in ["A", "MX", "NS", "TXT"]:
result = subprocess.check_output(["dig", "+short", record, target]).decode().strip()
print(f"{record}: {result}")
# SPF (anti-spoofing email)
spf = subprocess.check_output(["dig", "+short", "TXT", target]).decode()
print("SPF/DMARC:", spf)
# Sous-domaines via transfert de zone (si mal configuré)
subprocess.run(["dig", "axfr", f"@ns1.{target}", target])
```
**Certificate Transparency (sous-domaines cachés) :**
```python
import ssl, socket
ctx = ssl.create_default_context()
with ctx.wrap_socket(socket.socket(), server_hostname="target.com") as s:
s.connect(("target.com", 443))
cert = s.getpeercert()
sans = [san[1] for san in cert.get("subjectAltName", []) if san[0] == "DNS"]
print("SANs:", sans)
# Aussi : https://crt.sh/?q=%.target.com (CT logs publics)
```
**Headers HTTP (fingerprinting passif) :**
```python
import requests
r = requests.get("https://target.com", allow_redirects=True)
interesting = ["Server", "X-Powered-By", "X-Generator", "X-Framework",
"Set-Cookie", "Access-Control-Allow-Origin", "Content-Security-Policy"]
for h in interesting:
if h in r.headers:
print(f"{h}: {r.headers[h]}")
```
**OSINT :**
- Wayback Machine (`web.archive.org`) — routes supprimées, anciens paramètres
- `shodan.io` — si IP directe connue (hors CDN)
- `hunter.io` / `theHarvester` — emails, sous-domaines
- GitHub / GitLab — dépôts publics, leaks de clés, config files
---
### Phase 2 — Scan
**Ports sur IP directe (si hors CDN) :**
```bash
# Ne scanner que l'IP directe, jamais via Cloudflare (inutile + bruyant)
nmap -sV -T4 --open -p 1-65535 <ip_directe>
# Services web uniquement
nmap -sV -T4 --open -p 80,443,8080,8443,8888,3000,5000 <ip>
```
**Stack fingerprinting (actif) :**
- `whatweb https://target.com` — détecte CMS, frameworks, versions
- Burp Suite Passive Scanner — lors du browse
- Headers d'erreur HTTP 404/500 — souvent révèlent le stack
---
### Phase 3 — Énumération
**API discovery depuis le JS frontend :**
Les SPA (React, Vue, Nuxt) compilent les routes et endpoints dans les chunks JS. Chercher :
```bash
# Télécharger tous les chunks JS
curl -s https://target.com | grep -oP '(?<=src=")[^"]+\.js' | while read f; do
curl -s "https://target.com$f" >> all_js.txt
done
# Chercher les endpoints API
grep -oP '(?<=["`])/api/[a-zA-Z0-9/_-]+' all_js.txt | sort -u
grep -oP '(?<=["`])/v[0-9]+/[a-zA-Z0-9/_-]+' all_js.txt | sort -u
```
**Nuxt.js — `__NUXT_DATA__` :**
Nuxt 3 injecte l'état initial dans `window.__NUXT__` ou une balise `<script id="__NUXT_DATA__">`.
C'est souvent une mine d'informations : IDs utilisateurs, tokens, config interne.
```python
import requests, json, re
r = requests.get("https://target.com")
match = re.search(r'<script id="__NUXT_DATA__"[^>]*>(.*?)<\/script>', r.text, re.DOTALL)
if match:
data = json.loads(match.group(1))
print(json.dumps(data, indent=2))
```
**Routes cachées :**
```bash
# ffuf — fuzzing répertoires/routes
ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
-u https://target.com/FUZZ \
-mc 200,301,302,403 \
-t 50 -c
# API versioning
ffuf -w versions.txt -u https://target.com/api/FUZZ/users -mc 200,401
```
---
### Phase 4 — Tests de vulnérabilités
Voir les sections dédiées : CORS, IDOR, XSS, SQLi, WebSocket, Business Logic.
---
### Phase 5 — Rapport
Structure minimale :
1. **Résumé exécutif** (2 paragraphes, non technique)
2. **Périmètre testé** (domaines, dates, contexte)
3. **Méthodologie** (phases suivies, outils utilisés)
4. **Vulnérabilités** (une fiche par finding, voir template ci-dessous)
5. **Matrice de risque** (tableau récapitulatif avec CVSS)
6. **Recommandations** (priorité haute/moyenne/basse)
**Template fiche vulnérabilité :**
```
## [VULN-001] Titre de la vulnérabilité
**Sévérité** : Critique / Haute / Moyenne / Faible / Informatif
**CVSS v3.1** : 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
**URL affectée** : https://target.com/api/users/{id}/downloads
**Méthode** : GET
### Description
Ce que la vulnérabilité permet concrètement.
### Preuve (PoC)
Requête exacte envoyée + réponse reçue.
### Impact
Données exposées, actions possibles, utilisateurs affectés.
### Recommandation
Correction précise à implémenter.
```
---
## Section 2 : Techniques spécifiques
### Fingerprinting stack Nuxt.js
Indices à chercher :
| Indice | Localisation | Ce que ça révèle |
|--------|-------------|-----------------|
| `/_nuxt/` dans les URLs de chunks | Source HTML | Nuxt.js avec convention de build par défaut |
| `__NUXT_DATA__` | Balise `<script>` | Nuxt 3, état hydraté |
| `window.__NUXT__` | JS inline | Nuxt 2 |
| Chunks nommés `pages/`, `layouts/` | URLs JS | Structure de routage exposée |
| `_payload.json` en suffixe | URLs | Nuxt 3 avec `useFetch` / payloads statiques |
```python
# Récupérer le payload Nuxt d'une route
import requests
r = requests.get("https://target.com/profile/_payload.json")
if r.status_code == 200:
print(r.json()) # Données brutes avant hydratation
```
---
### Playwright pour WebSocket derrière Cloudflare
Cloudflare bloque les scrapers via TLS fingerprint (JA3). Un vrai navigateur Chromium passe.
```python
import asyncio, json
from playwright.async_api import async_playwright
async def capture_ws_frames(url: str, cookie: str) -> list:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
await context.add_cookies([{
"name": "session",
"value": cookie,
"domain": "target.com",
"path": "/"
}])
page = await context.new_page()
frames = []
def on_ws(ws):
ws.on("framereceived", lambda f: frames.append(f.payload))
ws.on("framesent", lambda f: frames.append(f"SENT: {f.payload}"))
page.on("websocket", on_ws)
await page.goto(url, wait_until="networkidle")
await asyncio.sleep(10) # Laisser les messages arriver
await browser.close()
return frames
frames = asyncio.run(capture_ws_frames("https://target.com/app", "SESSION_COOKIE"))
for f in frames:
print(f)
```
**Parser les frames Socket.IO (EIO4) :**
```python
import json
def parse_socketio(frame: str) -> dict | None:
"""
Format Socket.IO :
"42" = message event
"2" = ping
"3" = pong
"40" = connect
"""
if not isinstance(frame, str):
return None
if not frame.startswith("42"):
return {"type": "control", "raw": frame}
try:
data = json.loads(frame[2:])
return {
"event": data[0],
"payload": data[1] if len(data) > 1 else None
}
except (json.JSONDecodeError, IndexError):
return {"type": "unparsed", "raw": frame}
for f in frames:
parsed = parse_socketio(f)
if parsed and parsed.get("event"):
print(f"EVENT: {parsed['event']}")
print(f"PAYLOAD: {json.dumps(parsed['payload'], indent=2)}")
```
---
### CORS `*` + ACAC:true — mythe vs réalité
**Le mythe :** `Access-Control-Allow-Origin: *` avec `Access-Control-Allow-Credentials: true` est dangereux.
**La réalité :** Les navigateurs **refusent** cette combinaison. La spec CORS l'interdit explicitement.
Un navigateur qui voit `ACAO: *` et `ACAC: true` ensemble **ne enverra pas les cookies**.
**Ce qui est réellement dangereux :**
```
# Dangereux : origin reflétée dynamiquement avec credentials
Access-Control-Allow-Origin: https://attacker.com (reflétée depuis Origin header)
Access-Control-Allow-Credentials: true
```
**Tester :**
```python
import requests
# Simuler une requête cross-origin avec credentials
r = requests.get("https://target.com/api/me",
headers={"Origin": "https://evil.com"},
cookies={"session": "VOTRE_SESSION"})
print(r.headers.get("Access-Control-Allow-Origin"))
print(r.headers.get("Access-Control-Allow-Credentials"))
# Vulnérable si :
# ACAO = "https://evil.com" (ou reflète l'Origin)
# ACAC = "true"
```
**Cas borderline à tester :**
- `ACAO: null` — navigateurs anciens traitaient `null` comme trusted
- `ACAO: https://target.com.evil.com` — regex trop permissive
- Sous-domaine compromis en whitelisté : `ACAO: https://cdn.target.com`
---
### Cookie prefixes — protections méconnues
| Préfixe | Règle | Ce que ça bloque |
|---------|-------|-----------------|
| `__Host-` | Doit avoir `Secure`, `Path=/`, **pas de `Domain`** | Cookie injection depuis sous-domaine, downgrade HTTP |
| `__Secure-` | Doit avoir `Secure` | Cookie injection via HTTP downgrade |
**Vérifier les cookies en pentest :**
```python
import requests
r = requests.get("https://target.com/login", allow_redirects=True)
for cookie in r.cookies:
print(f"\n{cookie.name}:")
print(f" HttpOnly : {cookie.has_nonstandard_attr('HttpOnly')}")
print(f" Secure : {cookie.secure}")
print(f" SameSite : {cookie.get_nonstandard_attr('SameSite', 'Non défini')}")
print(f" Domain : {cookie.domain}")
print(f" Path : {cookie.path}")
if not cookie.name.startswith(("__Host-", "__Secure-")):
print(" ⚠️ Pas de préfixe de sécurité")
if not cookie.secure:
print(" ❌ Pas Secure")
```
---
### SSTI — éviter les faux positifs
**Niveau 1 — Possible SSTI :**
```
Input: {{7*7}}
Output contient: 49
```
Peut être une coïncidence. Certains moteurs affichent la valeur d'expression JS.
**Niveau 2 — Probable SSTI :**
```
Input: {{7*'7'}}
Jinja2: 49 (int * str → str répétée → 7777777)
Twig: 49
Freemarker: erreur type différente de "non interprété"
```
**Niveau 3 — SSTI confirmé + identification moteur :**
```
# Jinja2
{{config.items()}}
{{self.__class__.__mro__}}
# Twig
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
# Freemarker
${"freemarker.template.utility.Execute"?new()("id")}
```
**Règle :** Ne jamais escalader en RCE sans confirmation niveau 3 + autorisation explicite.
---
### Ce que Cloudflare WAF fait et ne fait PAS
| Cloudflare bloque | Cloudflare ne bloque PAS |
|-------------------|------------------------|
| SQLi évidents (`' OR 1=1`) | IDOR (requêtes légitimes avec ID d'un autre) |
| XSS avec payloads connus (`<script>alert`) | Business logic flaws |
| Scanners automatiques (User-Agent connu) | Privilege escalation (PATCH rôle) |
| Path traversal basique (`../../../etc`) | Race conditions |
| Requêtes avec trop d'anomalies HTTP | Broken Object Level Authorization |
| DDoS volumétrique | Auth bypass logique (ex: step skipping) |
**Implication pratique :** Pour les vulnérabilités OWASP API Top 10 (BOLA/IDOR, broken auth, mass assignment), Cloudflare est transparent. Tester normalement.
---
## Section 3 : IDOR et escalade de privilèges
### Pattern de test IDOR
```python
import requests
def test_idor(session_victim: requests.Session, session_attacker: requests.Session,
base_url: str, victim_id: str) -> None:
"""
Tester si l'attaquant peut accéder aux ressources de la victime.
session_victim : session authentifiée comme utilisateur A
session_attacker : session authentifiée comme utilisateur B (ou non authentifié)
"""
endpoints = [
f"/api/users/{victim_id}",
f"/api/users/{victim_id}/downloads",
f"/api/users/{victim_id}/invites",
f"/api/users/{victim_id}/payment-methods",
f"/api/orders?user_id={victim_id}",
f"/api/profile/{victim_id}/export",
]
print("=== Test IDOR ===")
for ep in endpoints:
url = f"{base_url}{ep}"
# Vérifier que la victime peut accéder (référence)
r_victim = session_victim.get(url)
# Tester si l'attaquant peut accéder
r_attacker = session_attacker.get(url)
status_v = r_victim.status_code
status_a = r_attacker.status_code
if status_a == 200 and status_v == 200:
print(f"❌ IDOR confirmé : {ep} ({status_a})")
elif status_a in (200, 201) and status_v != 200:
print(f"⚠️ Accès attaquant ({status_a}) mais victime ({status_v}) : {ep}")
else:
print(f"✅ OK : {ep} (attaquant={status_a}, victime={status_v})")
# Escalade de privilèges (mass assignment / BFLA)
def test_privilege_escalation(session: requests.Session, base_url: str, user_id: str) -> None:
print("\n=== Test Privilege Escalation ===")
payloads = [
{"role": "admin"},
{"is_admin": True},
{"permissions": ["admin", "read", "write"]},
{"subscription": "premium"},
]
for payload in payloads:
r = session.patch(f"{base_url}/api/users/{user_id}", json=payload)
print(f"PATCH {payload} → {r.status_code}")
if r.status_code in (200, 204):
print(f" ⚠️ Champ accepté : {payload}")
```
---
## Section 4 : WebSocket — vecteurs d'attaque
### Spoofing et channel hopping
```python
import asyncio
import websockets
async def ws_test(uri: str, cookie: str) -> None:
headers = {
"Cookie": f"session={cookie}",
"Origin": "https://target.com"
}
async with websockets.connect(uri, extra_headers=headers) as ws:
# Test 1 : accès à un channel privé (IDOR WS)
await ws.send('{"action": "subscribe", "channel": "user_42_notifications"}')
response = await asyncio.wait_for(ws.recv(), timeout=5)
print("Channel hopping:", response)
# Test 2 : XSS stocké via WebSocket
await ws.send('{"action": "message", "content": "<script>alert(1)<\/script>"}')
response = await asyncio.wait_for(ws.recv(), timeout=5)
print("XSS payload response:", response)
# Test 3 : injection de commandes dans les events
await ws.send('{"action": "ping", "data": "test; id"}')
response = await asyncio.wait_for(ws.recv(), timeout=5)
print("Injection test:", response)
asyncio.run(ws_test("wss://target.com/ws", "SESSION_COOKIE"))
```
### Vérifier l'origine WebSocket
Un serveur WebSocket mal configuré accepte des connexions de n'importe quelle origine.
Si l'application utilise WebSocket + cookies de session, c'est un CSRF potentiel.
```python
# Tester sans Origin valide
import websockets, asyncio
async def test_origin():
# Sans cookie — serveur doit rejeter (401/403)
try:
async with websockets.connect("wss://target.com/ws",
extra_headers={"Origin": "https://evil.com"}) as ws:
await ws.send('{"action": "ping"}')
print("⚠️ Connexion acceptée sans auth depuis evil.com")
except websockets.exceptions.InvalidStatusCode as e:
print(f"✅ Rejeté : {e.status_code}")
asyncio.run(test_origin())
```
---
## Section 5 : DNS recon complet
```python
import subprocess, json
def dns_recon(target: str) -> dict:
results = {}
for record in ["A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA"]:
try:
output = subprocess.check_output(
["dig", "+short", record, target],
stderr=subprocess.DEVNULL
).decode().strip()
results[record] = output.splitlines() if output else []
except subprocess.CalledProcessError:
results[record] = []
# SPF et DMARC spécifiquement
try:
dmarc = subprocess.check_output(
["dig", "+short", "TXT", f"_dmarc.{target}"],
stderr=subprocess.DEVNULL
).decode().strip()
results["DMARC"] = dmarc
except subprocess.CalledProcessError:
results["DMARC"] = None
return results
recon = dns_recon("target.com")
print(json.dumps(recon, indent=2))
```
---
## Section 6 : Cadre légal
**OBLIGATOIRE avant tout test :**
- [ ] Autorisation écrite signée par le responsable légitime (DSI, CTO, propriétaire)
- [ ] Périmètre documenté : liste exacte des domaines, sous-domaines, IPs testables
- [ ] Date de début et fin de la mission
- [ ] Contacts d'urgence (en cas d'incident déclenché pendant les tests)
- [ ] Clause de confidentialité sur les findings
**Pendant les tests :**
- Ne jamais exfiltrer de données réelles — documenter l'accès, ne pas télécharger
- En cas de découverte de données sensibles tierces (ex: PII d'autres clients), stopper et informer immédiatement
- Garder tous les logs de vos requêtes (CYA — cover your ass)
- Ne pas tester les dépendances tierces hors périmètre (CDN, paiement, SSO) sans autorisation séparée
**Rapport :**
- Livrer au commanditaire avant toute publication ou divulgation
- Anonymiser les données utilisateurs dans les PoC
- Respecter un délai raisonnable de remédiation (90 jours standard) avant publication publique
---
## Checklist pentest web
### Recon
- [ ] DNS (A, AAAA, MX, NS, TXT, SOA)
- [ ] SPF + DMARC (email spoofing possible ?)
- [ ] Certificate Transparency — `crt.sh` pour les SANs (sous-domaines cachés)
- [ ] OSINT (Wayback Machine, Shodan, GitHub, Hunter.io)
- [ ] Headers HTTP (Server, X-Powered-By, X-Generator, CSP)
### Scan & Énumération
- [ ] Stack fingerprinting (WhatWeb, headers d'erreur, chunks JS)
- [ ] Ports sur IP directe si hors CDN (`nmap -sV -T4 --open`)
- [ ] API discovery depuis JS frontend (regex sur chunks)
- [ ] `__NUXT_DATA__` / `window.__NUXT__` si Nuxt.js détecté
- [ ] Fuzzing routes/répertoires (ffuf)
### Tests applicatifs
- [ ] CORS : tester reflection d'Origin arbitraire + avec `null` + sous-domaines
- [ ] IDOR sur toutes les ressources utilisateur (GET + PUT/PATCH + DELETE)
- [ ] Privilege escalation via mass assignment (PATCH rôle/subscription/is_admin)
- [ ] SQLi sur tous les paramètres (GET, POST body, headers, cookies)
- [ ] XSS stored/reflected/DOM sur les inputs utilisateur
- [ ] WebSocket : channel hopping, XSS stored, spoofing origine
- [ ] Business logic : race conditions, workflow bypass, replay d'actions
- [ ] SSTI sur les champs de template/personnalisation (confirmation niveaux 1/2/3)
- [ ] Auth : brute force login, reset password predictable, JWT alg:none / RS→HS
### Cookies & Sessions
- [ ] HttpOnly sur tous les cookies de session
- [ ] Secure flag sur tous les cookies
- [ ] SameSite (Strict ou Lax pour les cookies critiques)
- [ ] Préfixes `__Host-` et `__Secure-`
- [ ] Durée de validité des sessions (expiration après logout ?)
### Infrastructure
- [ ] Rate limiting sur login, reset password, API sensibles
- [ ] Divulgation de stack dans les erreurs (mode debug en prod ?)
- [ ] Headers sécurité (HSTS, X-Frame-Options, X-Content-Type-Options, CSP)
- [ ] Versions de librairies exposées (CVE connues ?)
---
## Outils par contexte
| Contexte | Outil recommandé | Pourquoi |
|----------|-----------------|----------|
| Derrière Cloudflare / JA3 check | Playwright (Chromium) | Fingerprint navigateur réel, passe les WAF |
| Tests manuels + replay | Burp Suite Community/Pro | Proxy HTTP, Repeater, Intruder, scanner passif |
| Scan de ports | `nmap -sV -T4 --open` | Sur IP directe uniquement, jamais via CDN |
| Scripts de test custom | Python `requests` | Rapide, flexible, reproductible, lisible |
| WAF en place | `curl` avec cookies | Moins détectable que les scanners automatiques |
| Fuzzing répertoires | `ffuf` | Plus rapide et flexible que DirBuster |
| Fingerprinting web | `whatweb` | Détection stack sans bruit excessif |
| DNS recon | `dig` + `crt.sh` | Fiable, minimal, pas de dépendance externe |
| WebSocket derrière WAF | Playwright | Même raison que Cloudflare |
| WebSocket sans WAF | `websockets` (Python) | Plus léger, plus contrôlable |