first commit

This commit is contained in:
Paul Schneider 2026-09-24 17:50:43 +01:00
commit 2e22667181
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
17 changed files with 889 additions and 5 deletions

View file

@ -0,0 +1,36 @@
# api-fuzzer
Cartographie la surface d'API des trois hôtes preprod (Org, Api,
Blogs) : codes de réponse par endpoint, verbes autorisés (via
`OPTIONS` + en-tête `Allow`), fuites d'information dans les erreurs
500.
## Principe
Lit une liste de chemins dans `endpoints.txt` (modifiable) et, pour
**chaque hôte du périmètre** (`TARGET_BASE_URL` + `TARGET_ALLOWED_HOSTS`),
émet `GET` puis `OPTIONS`. Journalise le code, la taille du corps et
les verbes autorisés.
## Lancement
```bash
./fuzz.sh
# ou avec une liste d'endpoints personnalisée :
./fuzz.sh targets.env mes-endpoints.txt
```
## Sortie
- stdout : endpoints notables (200 accessible, 500 fuite possible).
- `output/api-fuzz.log` : une ligne par test
(`horodatage \t GET \t base \t code \t taille \t chemin`).
## Actions typiques
- Endpoint renvoyant 500 avec une stack trace → fuite d'info : activer
`UseExceptionHandler` en prod, masquer les détails.
- `OPTIONS` révèle `DELETE`/`PUT` sur une route qu'on croyait GET-only
→ vérifier l'autorisation côté serveur (les verbes ne devraient pas
dépendre que du routage).
- Endpoint non documenté en 200 → inventorier et fermer si inutile.

View file

@ -0,0 +1,34 @@
# endpoints.txt — liste de chemins à énumérer (un par ligne).
# Lignes vides et commençant par # sont ignorées.
# Adaptez à la surface d'API d'Yavsc (voir docs/scope.md).
# Tout chemin est préfixé par TARGET_BASE_URL.
# --- Auth / IdentityServer ---
/account/login
/account/register
/account/forgotpassword
/connect/token
/connect/authorize
/connect/userinfo
/connect/logout
/.well-known/openid-configuration
/.well-known/jwks
# --- API business (host API, si exposé via la même preprod) ---
/api/v1/estimate
/api/v1/front/query
/api/v1/activity
/api/v1/profiles
# --- Blogs / FS ---
/api/v1/fs
/api/v1/blogs
# --- Surfaces de doc / debug ---
/swagger
/swagger/index.html
/swagger/v1/swagger.json
/openapi.json
/health
/healthz
/diagnostics

83
tools/api-fuzzer/fuzz.sh Executable file
View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# fuzz.sh — énumère les endpoints d'une API et cartographie les codes
# de réponse, pour repérer les routes ouvertes, les fuites d'info dans
# les messages d'erreur, et les variations de comportement.
#
# USAGE
# ./fuzz.sh [targets.env] [endpoints.txt]
#
# Lit une liste de chemins (un par ligne) depuis le 2e argument, sinon
# FUZZ_PATHS, sinon ./endpoints.txt à côté du script. Pour chaque
# chemin, émet GET et OPTIONS (pour détecter les verbes autorisés via
# l'en-tête Allow).
#
# Journalise dans $OUTPUT_DIR/api-fuzz.log.
COMMON_SH="$(cd "$(dirname "$0")/../.." && pwd)/lib/common.sh"
# shellcheck disable=SC1091
source "$COMMON_SH"
load_target "${1:-}"
LIST="${2:-${FUZZ_PATHS:-$(dirname "$0")/endpoints.txt}}"
[[ -f "$LIST" ]] \
|| die "Liste d'endpoints introuvable : $LIST (créez endpoints.txt ou réglez FUZZ_PATHS)"
mkdir -p "${OUTPUT_DIR:-./output}"
LOG="${OUTPUT_DIR:-./output}/api-fuzz.log"
: > "$LOG"
info "Fuzz de $(wc -l < "$LIST") endpoints sur ${#AUTHORIZED_HOSTS[@]} hôte(s) du périmètre"
info "Liste : $LIST"
info "Journal : $LOG"
# Endpoints **intentionnellement publics** : un 200 dessus est normal,
# pas une anomalie. Ex : la discovery OIDC d'IdentityServer, que les
# clients (PostIt) interrogent pour découvrir les endpoints d'auth.
EXPECTED_PUBLIC=(
"/.well-known/openid-configuration"
"/.well-known/jwks"
)
is_expected_public() {
local p="$1"
local e
for e in "${EXPECTED_PUBLIC[@]}"; do [[ "$e" == "$p" ]] && return 0; done
return 1
}
while IFS= read -r base; do
info "→ $base"
while IFS= read -r p || [[ -n "$p" ]]; do
[[ -z "$p" || "$p" == \#* ]] && continue
url="${base}${p}"
http_probe GET "$url"
code="$LAST_CODE"
size=0
[[ -n "$LAST_BODY" && -s "$LAST_BODY" ]] && size=$(wc -c < "$LAST_BODY")
printf '%s\tGET\t%s\t%s\t%8d\t%s\n' "$(date -Is)" "$base" "$code" "$size" "$p" >> "$LOG"
# Verbes autorisés : la réponse à OPTIONS peut révéler POST/DELETE
# même sur une route GET-only côté serveur.
allow="$(set +e; curl -s -D - -o /dev/null -X OPTIONS \
--connect-timeout "${CURL_TIMEOUT:-10}" --max-time "${CURL_MAX_TIME:-30}" \
"$url" | tr -d '\r' | awk -F': ' 'tolower($1)=="allow"{print $2}')"
[[ -n "$allow" ]] && printf '%s\tOPT\t%s\tAllow: %s\t%s\n' "$(date -Is)" "$base" "$allow" "$p" >> "$LOG"
case "$code" in
200)
if is_expected_public "$p"; then
ok "200 $base$p — public attendu (OIDC discovery)"
else
warn "200 $base$p — accessible"
fi
;;
401|403) ;;
404) ;;
500) err "500 $base$p — erreur serveur (fuite d'info possible)" ;;
*) warn "$code $base$p" ;;
esac
drop_body
pace
done < "$LIST"
done < <(each_scope_url)
info "Terminé. Voir $LOG."

View file

@ -0,0 +1,43 @@
# brute-login
Teste la résistance de l'authentification preprod par énumération de
mots de passe sur un utilisateur connu.
## Principe
Pour chaque mot de passe d'une wordlist, émet un `POST` sur
`LOGIN_PATH` et compare le code HTTP renvoyé à `LOGIN_SUCCESS_CODE`.
S'arrête à la première réussite. Le but est de **mesurer** la
robustesse (wordlist courte = compromise rapide → authentification
fragile ; rate-limit / lockout / 2FA = résistance), pas de produire
un déni de service.
## Préparation
1. `cp targets.example.env targets.env` et renseignez
`TARGET_SCOPE_ACK="yes"`, `TARGET_BASE_URL`, `LOGIN_PATH`,
`LOGIN_USERNAME`, `LOGIN_WORDLIST`.
2. Placez votre wordlist dans `wordlists/passwords.txt` (ignoré par
git) ou pointez `LOGIN_WORDLIST` vers un chemin absolu.
## Lancement
```bash
./brute-login.sh
```
## Sortie
- stdout : progression + verdict (réussite/échec).
- `output/brute-login.log` : une ligne par tentative
(`horodatage \t code \t mot-de-passe`).
## Interprétation
- **Réussite rapide** sur une wordlist triviale → politique de mots
de passe trop faible. Corriger : longueur minimale, complexité,
audit contre une liste de mots de passe courants.
- **429 / lockout** → rate-limiting actif (bon). Notez le seuil.
- **Même code d'échec partout** → impossible de distinguer réussite
d'échec par le code seul ; envisagez un indiceur de succès plus
fin (taille du corps, présence d'un jeton) en adaptant le script.

130
tools/brute-login/brute-login.sh Executable file
View file

@ -0,0 +1,130 @@
#!/usr/bin/env bash
# brute-login.sh — teste la robustesse de l'authentification de la
# preprod par énumération de mots de passe sur un utilisateur connu.
#
# USAGE
# ./brute-login.sh [targets.env]
#
# Comportement :
# - lit la wordlist LOGIN_WORDLIST (un mot de passe par ligne) ;
# - pour chaque entrée, émet POST LOGIN_PATH avec {{USER}}/{{PASS}} ;
# - considère réussite si le code HTTP == LOGIN_SUCCESS_CODE ;
# - s'arrête à la première réussite (ou à LOGIN_MAX_ATTEMPTS) ;
# - journalise les tentatives dans $OUTPUT_DIR/brute-login.log.
#
# Le délai REQUEST_DELAY espace les requêtes (voir docs/scope.md).
# Voir targets.example.env pour tous les paramètres.
COMMON_SH="$(cd "$(dirname "$0")/../.." && pwd)/lib/common.sh"
# shellcheck disable=SC1091
source "$COMMON_SH"
load_target "${1:-}"
mkdir -p "${OUTPUT_DIR:-./output}"
LOG="${OUTPUT_DIR:-./output}/brute-login.log"
: > "$LOG"
[[ -f "${LOGIN_WORDLIST}" ]] \
|| die "Wordlist introuvable : ${LOGIN_WORDLIST} (réglez LOGIN_WORDLIST dans targets.env)"
# URL absolue de login (LOGIN_PATH peut être relatif ou absolu dans le périmètre).
login_url() {
case "$LOGIN_PATH" in
http*) printf '%s' "$LOGIN_PATH" ;;
/*) printf '%s%s' "$TARGET_BASE_URL" "$LOGIN_PATH" ;;
*) printf '%s/%s' "$TARGET_BASE_URL" "$LOGIN_PATH" ;;
esac
}
URL="$(login_url)"
assert_in_scope "$URL"
info "Cible : $URL"
info "Utilisateur : ${LOGIN_USERNAME}"
info "Wordlist : ${LOGIN_WORDLIST}"
info "Succès si code == ${LOGIN_SUCCESS_CODE}"
info "Délai : ${REQUEST_DELAY}s entre requêtes"
info "Journal : $LOG"
# Construit le corps de la requête pour un couple (user, pass).
# Deux formats selon LOGIN_CONTENT_TYPE : "json" ou "form".
build_body() {
local user="$1" pass="$2"
local ct="${LOGIN_CONTENT_TYPE:-json}"
case "$ct" in
form)
if [[ -n "${LOGIN_BODY_FORM:-}" ]]; then
printf '%s' "${LOGIN_BODY_FORM//\{\{USER\}\}/$user}" \
| sed "s/{{PASS}}/$(printf '%s' "$pass" | sed 's/[&/\]/\\&/g')/"
else
# form-urlencoded : grant_type=password + user + pass.
# Encodage minimal des caractères spéciaux pour URL.
local u p
u="$(printf '%s' "$user" | sed 's/[&+/@=%]/\\&/g')"
p="$(printf '%s' "$pass" | sed 's/[&+/@=%]/\\&/g')"
printf 'grant_type=password&%s=%s&%s=%s' \
"$LOGIN_USER_FIELD" "$u" "$LOGIN_PASS_FIELD" "$p"
fi
;;
json|*)
if [[ -n "${LOGIN_BODY_JSON:-}" ]]; then
printf '%s' "${LOGIN_BODY_JSON//\{\{USER\}\}/$user}" \
| sed "s/{{PASS}}/$(printf '%s' "$pass" | sed 's/[&/\]/\\&/g')/"
else
# Construction JSON simple, échappement basique.
local u p
u="$(printf '%s' "$user" | sed 's/[\\"]/\\&/g')"
p="$(printf '%s' "$pass" | sed 's/[\\"]/\\&/g')"
printf '{"%s":"%s","%s":"%s"}' \
"$LOGIN_USER_FIELD" "$u" "$LOGIN_PASS_FIELD" "$p"
fi
;;
esac
}
# En-tête Content-Type selon le format.
content_type_header() {
case "${LOGIN_CONTENT_TYPE:-json}" in
form) printf 'application/x-www-form-urlencoded' ;;
*) printf 'application/json' ;;
esac
}
count=0
found=0
while IFS= read -r pass || [[ -n "$pass" ]]; do
# ignore lignes vides et commentaires
[[ -z "$pass" || "$pass" == \#* ]] && continue
count=$((count + 1))
if [[ "${LOGIN_MAX_ATTEMPTS}" != "0" && "$count" -gt "${LOGIN_MAX_ATTEMPTS}" ]]; then
info "Limite LOGIN_MAX_ATTEMPTS=${LOGIN_MAX_ATTEMPTS} atteinte, arrêt."
break
fi
body="$(build_body "$LOGIN_USERNAME" "$pass")"
http_probe POST "$URL" \
-H "Content-Type: $(content_type_header)" \
--data "$body"
code="$LAST_CODE"
printf '%s\t%s\t%s\n' "$(date -Is)" "$code" "$pass" >> "$LOG"
drop_body
if [[ "$code" == "${LOGIN_SUCCESS_CODE}" ]]; then
ok "Connexion réussie avec : $pass (code $code après $count tentatives)"
found=1
break
fi
# Anti-lockout : si on voit un 429 (trop de requêtes) ou un 403
# persistant, on prévient plutôt que de marteler.
if [[ "$code" == "429" ]]; then
warn "Code 429 (rate limit) reçu — la cible limite les tentatives. Pause + longue."
sleep 5
fi
pace
done < "${LOGIN_WORDLIST}"
if [[ "$found" -eq 0 ]]; then
warn "Aucun mot de passe trouvé après $count tentatives. Voir $LOG."
exit 1
fi

View file

@ -0,0 +1,34 @@
# config-scan
Détecte les fichiers de configuration et surfaces sensibles
accidentellement exposés publiquement sur la preprod.
## Principe
Émet des `GET` sur une liste de chemins courants (`/.git/HEAD`,
`/.env`, `/appsettings.json`, `/swagger`, `/openapi.json`, etc.) et
signale tout ce qui répond 200 (exposé) ou 401/403 (présent mais
protégé — bonne pratique confirmant l'existence, à garder sous
œil).
## Lancement
```bash
./scan.sh
```
## Sortie
- stdout : chemins notables (200 en vert = exposé, 403 en jaune).
- `output/config-scan.log` : une ligne par chemin testé
(`horodatage \t code \t taille \t chemin`).
## Actions typiques
- `/.git/HEAD` en 200 → dépôt git servi statiquement : bloquer
l'accès aux répertoires `.*` au niveau du reverse proxy.
- `/appsettings*.json` en 200 → fuite potentielle de chaînes de
connexion / secrets : déplacer hors racine web, servir hors du
static.
- `/swagger` exposé en production → désactiver en prod, garder en
preprod uniquement derrière auth.

99
tools/config-scan/scan.sh Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env bash
# scan.sh — recherche les fichiers de configuration et éléments sensibles
# accidentellement exposés sur la preprod.
#
# USAGE
# ./scan.sh [targets.env]
#
# Vérifie la présence et la lisibilité de chemins courants :
# - dépôts git exposés (/.git/...)
# - fichiers d'env / settings (.env, appsettings.json, web.config)
# - surfaces de doc API (swagger, openapi)
# - répertoires de listing
#
# Journalise dans $OUTPUT_DIR/config-scan.log. Signale tout ce qui
# répond 200 (ou 403 = présent mais protégé) pour un passage en revue.
COMMON_SH="$(cd "$(dirname "$0")/../.." && pwd)/lib/common.sh"
# shellcheck disable=SC1091
source "$COMMON_SH"
load_target "${1:-}"
mkdir -p "${OUTPUT_DIR:-./output}"
LOG="${OUTPUT_DIR:-./output}/config-scan.log"
: > "$LOG"
# Chemins à tester (relatifs à TARGET_BASE_URL). Pas de cible tierce.
PATHS=(
"/.git/HEAD"
"/.git/config"
"/.env"
"/.env.local"
"/appsettings.json"
"/appsettings.Development.json"
"/appsettings.Production.json"
"/web.config"
"/connectionstrings.json"
"/swagger"
"/swagger/v1/swagger.json"
"/swagger/index.html"
"/openapi.json"
"/openapi/v1.json"
"/robots.txt"
"/.well-known/openid-configuration"
"/elmah.axd"
"/trace.axd"
"/wp-config.php"
"/server-status"
"/.DS_Store"
"/backup"
"/.vscode"
)
info "Scan de ${#PATHS[@]} chemins sur ${#AUTHORIZED_HOSTS[@]} hôte(s) du périmètre"
info "Journal : $LOG"
# Endpoints **intentionnellement publics** (un 200 est normal, pas une
# fuite) : la discovery OIDC d'IdentityServer, requise par les clients
# (PostIt) pour se connecter.
EXPECTED_PUBLIC=(
"/.well-known/openid-configuration"
"/.well-known/jwks"
)
is_expected_public() {
local p="$1"
local e
for e in "${EXPECTED_PUBLIC[@]}"; do [[ "$e" == "$p" ]] && return 0; done
return 1
}
while IFS= read -r base; do
info "→ $base"
for p in "${PATHS[@]}"; do
url="${base}${p}"
# http_probe gère déjà assert_in_scope.
http_probe GET "$url"
code="$LAST_CODE"
size=0
[[ -n "$LAST_BODY" && -s "$LAST_BODY" ]] && size=$(wc -c < "$LAST_BODY")
printf '%s\t%s\t%s\t%8d\t%s\n' "$(date -Is)" "$base" "$code" "$size" "$p" >> "$LOG"
case "$code" in
200)
if is_expected_public "$p"; then
ok "200 $base$p (${size} o) — public attendu (OIDC discovery)"
else
ok "200 $base$p (${size} o) — EXPOSÉ, à corriger"
fi
;;
401|403) warn "$code $base$p — présent mais protégé" ;;
301|302) warn "$code $base$p — redirection (vérifier la cible)" ;;
404) ;;
*) warn "$code $base$p" ;;
esac
drop_body
pace
done
done < <(each_scope_url)
info "Terminé. Voir $LOG."

View file

@ -0,0 +1,41 @@
# rate-limit-probe
Mesure si l'endpoint d'authentification (ou tout endpoint sensible)
de la preprod met en place un rate-limiting / verrouillage contre le
bruteforce.
## Principe
Envoie `RATE_PROBE_COUNT` requêtes (20 par défaut) avec un mauvais
mot de passe sur `LOGIN_PATH` et observe les codes de réponse :
- **429** → rate-limiting actif (bon). Le seuil (après combien de
requêtes) se lit dans le journal.
- **changement soudain de code** (ex : 401 → 403/423) → verrouillage
(lockout) après N échecs.
- **rien de tout ça** (toujours 401/400) → AUCUN rate-limit :
vulnérabilité, le bruteforce n'est pas freiné.
## Lancement
```bash
./probe.sh
```
Paramètres (dans `targets.env`) :
- `RATE_PROBE_COUNT` — nombre de requêtes (défaut 20).
- `RATE_PROBE_DELAY` — délai entre requêtes en secondes (0 = rafale ;
pour tester le rate-limit on veut justement une rafale, mais
gardez un count modéré).
## Sortie
- stdout : verdict (rate-limit actif / lockout / absent).
- `output/rate-limit.log` : `horodatage \t n/N \t code`.
## Actions typiques
- Aucun rate-limit → en ajouter un au reverse proxy (ex : `limit_req`
nginx) ou côté application (bucket par IP+utilisateur).
- Rate-limit trop agressif (429 dès la 2e requête) → risque de bloquer
les légitimes ; ajuster le seuil.

96
tools/rate-limit-probe/probe.sh Executable file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env bash
# probe.sh — mesure la mise en place d'un rate-limiting / lockout sur
# l'endpoint d'authentification (ou tout endpoint sensible) de la
# preprod.
#
# USAGE
# ./probe.sh [targets.env]
#
# Envoie RATE_PROBE_COUNT requêtes rapides (identiques, échec volontaire)
# sur LOGIN_PATH et observe l'évolution des codes HTTP :
# - 429 trop tôt → rate-limit agressif (pe-être trop pour les
# légitimes) ;
# - jamais de 429, toujours 401/400 → AUCUN rate-limit : vulnérabilité
# (bruteforce non freiné) ;
# - verrouillage après N échecs (code change, ex 403/423) → lockout.
#
# Journalise dans $OUTPUT_DIR/rate-limit.log.
COMMON_SH="$(cd "$(dirname "$0")/../.." && pwd)/lib/common.sh"
# shellcheck disable=SC1091
source "$COMMON_SH"
load_target "${1:-}"
mkdir -p "${OUTPUT_DIR:-./output}"
LOG="${OUTPUT_DIR:-./output}/rate-limit.log"
: > "$LOG"
RATE_PROBE_COUNT="${RATE_PROBE_COUNT:-20}"
RATE_PROBE_DELAY="${RATE_PROBE_DELAY:-0}" # 0 = rafale, par défaut
# URL de login (même logique que brute-login).
login_url() {
case "${LOGIN_PATH:-}" in
http*) printf '%s' "$LOGIN_PATH" ;;
/*) printf '%s%s' "$TARGET_BASE_URL" "${LOGIN_PATH}" ;;
*) printf '%s/%s' "$TARGET_BASE_URL" "${LOGIN_PATH}" ;;
esac
}
URL="$(login_url)"
assert_in_scope "$URL"
info "Rate-limit probe : $RATE_PROBE_COUNT requêtes sur $URL"
info "Délai entre requêtes : ${RATE_PROBE_DELAY}s (0 = rafale)"
info "Journal : $LOG"
# Corps d'échec volontaire (mauvais mot de passe). On ne cherche pas
# à réussir ici, juste à saturer pour observer le rate-limit.
build_bad_body() {
case "${LOGIN_CONTENT_TYPE:-json}" in
form)
local u; u="$(printf '%s' "${LOGIN_USERNAME:-nobody}" | sed 's/[&+/@=%]/\\&/g')"
printf 'grant_type=password&%s=%s&%s=definitely-wrong-pass-%%42' \
"${LOGIN_USER_FIELD:-username}" "$u" "${LOGIN_PASS_FIELD:-password}"
;;
*)
printf '{"%s":"%s","%s":"definitely-wrong-pass"}' \
"${LOGIN_USER_FIELD:-username}" "${LOGIN_USERNAME:-nobody}" \
"${LOGIN_PASS_FIELD:-password}"
;;
esac
}
ct() { case "${LOGIN_CONTENT_TYPE:-json}" in form) printf 'application/x-www-form-urlencoded';; *) printf 'application/json';; esac; }
body="$(build_bad_body)"
seen_429=0
seen_lock=0
first_change=""
prev_code=""
for i in $(seq 1 "$RATE_PROBE_COUNT"); do
http_probe POST "$URL" -H "Content-Type: $(ct)" --data "$body"
code="$LAST_CODE"
printf '%s\t%3d/%d\t%s\n' "$(date -Is)" "$i" "$RATE_PROBE_COUNT" "$code" >> "$LOG"
drop_body
[[ "$code" == "429" ]] && seen_429=$((seen_429 + 1))
# 423 Locked, ou un changement soudain de code après des 401 → lockout possible.
if [[ "$code" == "423" || ( -n "$prev_code" && "$code" != "$prev_code" && "$code" != "429" ) ]]; then
seen_lock=$((seen_lock + 1))
[[ -z "$first_change" ]] && first_change="req $i : $prev_code → $code"
fi
prev_code="$code"
[[ "${VERBOSE:-0}" == "1" ]] && printf ' %3d %s\n' "$i" "$code"
[[ "$RATE_PROBE_DELAY" != "0" ]] && sleep "$RATE_PROBE_DELAY"
done
info "Résultats : $RATE_PROBE_COUNT requêtes, $seen_429 code(s) 429, $seen_lock changement(s) de code."
if [[ -n "$first_change" ]]; then
warn "Changement observé : $first_change (lockout possible)"
fi
if [[ "$seen_429" -eq 0 && -z "$first_change" ]]; then
err "AUCUN rate-limit détecté (pas de 429, pas de verrouillage). Endpoint vulnérable au bruteforce."
else
ok "Rate-limiting actif ($seen_429 réponses 429). Voir $LOG pour le seuil."
fi