first commit
This commit is contained in:
parent
883dd76931
commit
2e22667181
17 changed files with 889 additions and 5 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Fichier de cible et secrets — JAMAIS committés
|
||||
targets.env
|
||||
*.env
|
||||
!targets.example.env
|
||||
|
||||
# Wordlists (une par ligne) — locales, hors dépôt
|
||||
wordlists/*
|
||||
!wordlists/.gitkeep
|
||||
|
||||
# Sorties des outils (journaux, résultats)
|
||||
output/*
|
||||
!output/.gitkeep
|
||||
|
||||
# Journaux divers
|
||||
*.log
|
||||
*.out
|
||||
|
||||
# Jetons / cookies de session capturés
|
||||
*.token
|
||||
*.cookie
|
||||
.session
|
||||
|
|
@ -13,10 +13,10 @@ modulaires (un outil par sous-dossier).
|
|||
La preprod Yavsc est multi-hôtes :
|
||||
|
||||
| Hôte | Domaine | Rôle |
|
||||
|-------------|----------------------------------|------------------------------|
|
||||
|--------------|----------------------------------|------------------------------|
|
||||
| `Yavsc.Org` | `preprod-yavsc.pschneider.fr` | MVC + IdentityServer (auth) |
|
||||
| `Yavsc.Api` | `preprod-api.pschneider.fr` | API business (JSON) |
|
||||
| `Yavsc.Blogs` | `preprod-blogs.pschneider.fr` | Blogs + stockage perso |
|
||||
| `Yavsc.Blogs`| `preprod-blogs.pschneider.fr` | Blogs + stockage perso |
|
||||
|
||||
Les outils restreignent leurs requêtes à ce périmètre via
|
||||
`assert_in_scope` (`lib/common.sh`) ; toute dérive vers un autre hôte
|
||||
|
|
|
|||
45
docs/scope.md
Normal file
45
docs/scope.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Périmètre et autorisation
|
||||
|
||||
## Propriétaire et cible
|
||||
|
||||
Cette suite d'outils est destinée **exclusivement** au test de
|
||||
l'environnement de **préproduction** de Paul Schneider
|
||||
(`pschneider.fr`), infrastructure qui lui appartient.
|
||||
|
||||
**Aucun outil de ce dépôt ne doit être exécuté contre un tiers.**
|
||||
Les scripts intègrent un garde-fou (`assert_in_scope` dans
|
||||
`lib/common.sh`) qui restreint les requêtes au domaine déclaré dans
|
||||
`targets.env` (`TARGET_BASE_URL`). Toute dérive vers un autre hôte
|
||||
provoque un abandon immédiat.
|
||||
|
||||
## Conditions d'utilisation
|
||||
|
||||
1. **Autorisation** : vous ne testez que vos propres systèmes. Le
|
||||
fichier `targets.env` exige `TARGET_SCOPE_ACK="yes"` pour confirmer
|
||||
cette autorisation avant tout lancement.
|
||||
2. **Périmètre** : uniquement le domaine de `TARGET_BASE_URL` et ses
|
||||
sous-domaines. Pas de scan aveugle d'IP tierces, pas de rebond.
|
||||
3. **Modération** : utilisez `REQUEST_DELAY` pour espacer les requêtes
|
||||
et éviter de saturer la preprod. Un bruteforce n'a pas besoin
|
||||
d'être un déni de service — l'objectif est de mesurer la résistance
|
||||
de l'authentification, pas de l'abattre.
|
||||
4. **Secrets** : `targets.env`, les wordlists et les sorties ne sont
|
||||
**jamais** committés (voir `.gitignore`). Les jetons éventuels
|
||||
restent locaux.
|
||||
5. **Conséquences** : ces outils peuvent révéler des faiblesses
|
||||
réelles. Corrigez ce qu'ils trouvent sur votre preprod avant toute
|
||||
mise en production.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
- Toute cible n'appartenant pas à `pschneider.fr`.
|
||||
- Les environnements de production (la préprod peut partager des
|
||||
données : restez prudent sur ce que vous soumettez).
|
||||
- Les tiers hébergeurs, DNS, CDN en tant que cibles (on les traverse
|
||||
seulement pour atteindre la preprod).
|
||||
|
||||
## Remarque
|
||||
|
||||
Ces outils sont volontairement simples (Bash + curl) et lisibles. Le
|
||||
but est pédagogique et opérationnel : mesurer la robustesse de votre
|
||||
preprod, pas de fournir un framework d'attaque sophistiqué.
|
||||
1
gitleaks-report.json
Normal file
1
gitleaks-report.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
147
lib/common.sh
Normal file
147
lib/common.sh
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#!/usr/bin/env bash
|
||||
# common.sh — bibliothèque partagée des outils de pentest preprod.
|
||||
#
|
||||
# Fournit : chargement de la cible (targets.env), vérification
|
||||
# d'autorisation (scope), client HTTP (curl) avec journalisation,
|
||||
# sortie colorée, gestion du délai entre requêtes.
|
||||
#
|
||||
# Toutes les fonctions supposent que TARGET_BASE_URL est défini.
|
||||
# Charger avec : source "$(dirname "$0")/../lib/common.sh"
|
||||
# depuis un outil sous tools/<nom>/, ou adapter COMMON_SH ci-dessous.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# --- Couleurs (désactivées si pas de TTY) -------------------------------
|
||||
if [[ -t 1 ]]; then
|
||||
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||
C_CYAN=$'\033[36m'; C_DIM=$'\033[2m'; C_RESET=$'\033[0m'
|
||||
else
|
||||
C_RED=""; C_GREEN=""; C_YELLOW=""; C_CYAN=""; C_DIM=""; C_RESET=""
|
||||
fi
|
||||
|
||||
info() { printf '%s[INFO]%s %s\n' "$C_CYAN" "$C_RESET" "$*"; }
|
||||
ok() { printf '%s[OK]%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
|
||||
warn() { printf '%s[WARN]%s %s\n' "$C_YELLOW" "$C_RESET" "$*"; }
|
||||
err() { printf '%s[ERR]%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
# --- Chargement de la cible --------------------------------------------
|
||||
# Résout le répertoire racine de la suite (parent du dossier tools/).
|
||||
# Usage depuis tools/<outil>/script.sh : COMMON_SH pointe sur ../../lib
|
||||
suite_root() {
|
||||
# $BASH_SOURCE[0] = ce fichier (lib/common.sh) → racine = son parent
|
||||
local this; this="${BASH_SOURCE[0]}"
|
||||
local dir; dir="$(cd "$(dirname "$this")" && pwd)"
|
||||
printf '%s' "$(dirname "$dir")"
|
||||
}
|
||||
|
||||
# Charge targets.env. Refuse de tourner sans fichier de cible.
|
||||
# Argument optionnel : chemin vers un targets.env alternatif.
|
||||
load_target() {
|
||||
local env_file="${1:-$(suite_root)/targets.env}"
|
||||
[[ -f "$env_file" ]] \
|
||||
|| die "Aucun fichier de cible trouvé ($env_file). Copiez targets.example.env → targets.env et renseignez vos valeurs."
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
|
||||
[[ -n "${TARGET_BASE_URL:-}" ]] \
|
||||
|| die "targets.env: TARGET_BASE_URL est requis."
|
||||
[[ -n "${TARGET_SCOPE_ACK:-}" ]] \
|
||||
|| die "targets.env: TARGET_SCOPE_ACK doit valoir 'yes' (confirmation que la cible vous appartient et que le test est autorisé)."
|
||||
[[ "$TARGET_SCOPE_ACK" == "yes" ]] \
|
||||
|| die "targets.env: TARGET_SCOPE_ACK != 'yes'. Outil bloqué tant que l'autorisation n'est pas confirmée."
|
||||
|
||||
# Hôtes autorisés. Par défaut : l'hôte de TARGET_BASE_URL. Si
|
||||
# TARGET_ALLOWED_HOSTS est défini (liste séparée par espaces), il
|
||||
# étend le périmètre — utile quand la preprod a plusieurs hôtes
|
||||
# (ex : Yavsc.Org / Yavsc.Api / Yavsc.Blogs). Voir docs/scope.md.
|
||||
local base_host; base_host="$(printf '%s' "$TARGET_BASE_URL" \
|
||||
| sed -E 's#^[a-zA-Z]+://##; s#/.*$##; s#:.*$##')"
|
||||
# Construction + déduplication (l'hôte de base peut aussi figurer
|
||||
# dans TARGET_ALLOWED_HOSTS).
|
||||
local -A _seen=()
|
||||
AUTHORIZED_HOSTS=()
|
||||
for h in "$base_host" ${TARGET_ALLOWED_HOSTS:-}; do
|
||||
[[ -n "$h" && -z "${_seen[$h]:-}" ]] || continue
|
||||
_seen[$h]=1
|
||||
AUTHORIZED_HOSTS+=( "$h" )
|
||||
done
|
||||
export TARGET_BASE_URL TARGET_ALLOWED_HOSTS AUTHORIZED_HOSTS
|
||||
}
|
||||
|
||||
# Vérifie qu'une URL donnée appartient bien au périmètre autorisé.
|
||||
# Empêche un outil de dériver vers un tiers par maladresse.
|
||||
assert_in_scope() {
|
||||
local url="$1"
|
||||
local host; host="$(printf '%s' "$url" \
|
||||
| sed -E 's#^[a-zA-Z]+://##; s#/.*$##; s#:.*$##')"
|
||||
local h
|
||||
for h in "${AUTHORIZED_HOSTS[@]}"; do
|
||||
[[ "$host" == "$h" || "$host" == *".$h" ]] && return 0
|
||||
done
|
||||
die "Hors périmètre : $host (autorisé : ${AUTHORIZED_HOSTS[*]}). Abandon."
|
||||
}
|
||||
|
||||
# --- Client HTTP -------------------------------------------------------
|
||||
# Globales positionnées par http_probe. Initialisées ici pour ne pas
|
||||
# déclencher set -u au premier appel.
|
||||
LAST_CODE=""
|
||||
LAST_BODY=""
|
||||
|
||||
# http_probe METHOD URL [curl-args...]
|
||||
# Émet une requête et positionne les globales LAST_CODE (code HTTP, ou
|
||||
# "000" si curl a échoué) et LAST_BODY (chemin d'un fichier temporaire
|
||||
# contenant le corps de la réponse, ou "" si échec).
|
||||
#
|
||||
# IMPORTANT : ne PAS appeler dans une substitution de commande
|
||||
# (code=$(http_probe ...)) : la substitution s'exécute dans un
|
||||
# sous-shell et les globales ne remonteraient pas. Appeler
|
||||
# directement, puis lire $LAST_CODE / $LAST_BODY, et appeler
|
||||
# drop_body une fois le corps traité.
|
||||
http_probe() {
|
||||
local method="$1"; shift
|
||||
local url="$1"; shift
|
||||
assert_in_scope "$url"
|
||||
local code body_file
|
||||
body_file="$(mktemp)"
|
||||
code="$(set +e; curl -s -o "$body_file" -w '%{http_code}' \
|
||||
-X "$method" \
|
||||
--connect-timeout "${CURL_TIMEOUT:-10}" \
|
||||
--max-time "${CURL_MAX_TIME:-30}" \
|
||||
"$@" "$url")"
|
||||
local rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
warn "curl a échoué (rc=$rc) sur $method $url"
|
||||
rm -f "$body_file"
|
||||
LAST_CODE="000"
|
||||
LAST_BODY=""
|
||||
return 0
|
||||
fi
|
||||
if [[ "${VERBOSE:-0}" == "1" ]]; then
|
||||
printf '%s%s %s%s → %s%s\n' "$C_DIM" "$method" "$url" "$C_RESET" "$C_CYAN" "$code"
|
||||
[[ -s "$body_file" ]] && head -c 200 "$body_file" && printf '\n---\n'
|
||||
fi
|
||||
LAST_CODE="$code"
|
||||
LAST_BODY="$body_file"
|
||||
}
|
||||
|
||||
drop_body() { [[ -n "${LAST_BODY:-}" ]] && rm -f "$LAST_BODY"; LAST_BODY=""; }
|
||||
|
||||
# Renvoie les URL de base (une par hôte autorisé) en conservant le
|
||||
# schéma de TARGET_BASE_URL. Les scanners itèrent dessus pour couvrir
|
||||
# tout le périmètre multi-hôtes.
|
||||
each_scope_url() {
|
||||
local scheme; scheme="$(printf '%s' "$TARGET_BASE_URL" \
|
||||
| sed -E 's#://.*$##')"
|
||||
[[ -z "$scheme" ]] && scheme="https"
|
||||
local h
|
||||
for h in "${AUTHORIZED_HOSTS[@]}"; do
|
||||
printf '%s://%s\n' "$scheme" "$h"
|
||||
done
|
||||
}
|
||||
|
||||
# Délai entre requêtes (anti-DoS involontaire). Pacing obligatoire.
|
||||
pace() {
|
||||
local d="${REQUEST_DELAY:-0}"
|
||||
[[ "$d" != "0" ]] && sleep "$d"
|
||||
}
|
||||
0
output/.gitkeep
Normal file
0
output/.gitkeep
Normal file
74
targets.example.env
Normal file
74
targets.example.env
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# targets.example.env — copiez ce fichier en targets.env et renseignez
|
||||
# vos valeurs. targets.env est ignoré par git (.gitignore) : il ne doit
|
||||
# JAMAIS être committé (il contient votre cible et vos jetons).
|
||||
#
|
||||
# cp targets.example.env targets.env
|
||||
|
||||
# --- Autorisation (OBLIGATOIRE) ----------------------------------------
|
||||
# Confirmez que la cible vous appartient et que ce test est autorisé.
|
||||
# Les outils refusent de tourner tant que cette valeur n'est pas "yes".
|
||||
# Voir docs/scope.md pour le périmètre exact.
|
||||
TARGET_SCOPE_ACK="no"
|
||||
|
||||
# --- Cible -------------------------------------------------------------
|
||||
# URL de base de la preprod. Tous les outils restent dans ce domaine
|
||||
# (assert_in_scope rejette tout ce qui en dérive).
|
||||
# Preprod Yavsc — host Yavsc.Org (MVC + IdentityServer, porte l'auth).
|
||||
TARGET_BASE_URL="https://preprod-yavsc.pschneider.fr"
|
||||
|
||||
# Hôtes additionnels du périmètre (séparés par espaces). La preprod
|
||||
# Yavsc est multi-hôtes :
|
||||
# Yavsc.Org → preprod-yavsc.pschneider.fr (auth, MVC)
|
||||
# Yavsc.Api → preprod-api.pschneider.fr (API business, JSON)
|
||||
# Yavsc.Blogs → preprod-blogs.pschneider.fr (blogs + stockage perso)
|
||||
# assert_in_scope autorise ces hôtes en plus de TARGET_BASE_URL.
|
||||
TARGET_ALLOWED_HOSTS="preprod-yavsc.pschneider.fr preprod-api.pschneider.fr preprod-blogs.pschneider.fr"
|
||||
|
||||
# Timeout curl (secondes)
|
||||
CURL_TIMEOUT=10
|
||||
CURL_MAX_TIME=30
|
||||
|
||||
# Délai entre requêtes (secondes). 0 = aucune pause. Gardez un délai
|
||||
# raisonnable pour ne pas saturer la preprod.
|
||||
REQUEST_DELAY=0.2
|
||||
|
||||
# Verbeux : 1 = affiche chaque requête + début du corps
|
||||
VERBOSE=0
|
||||
|
||||
# Dossier de sortie (journaux, résultats). Ignoré par git.
|
||||
OUTPUT_DIR="./output"
|
||||
|
||||
# --- brute-login -------------------------------------------------------
|
||||
# Endpoint de connexion (relatif à TARGET_BASE_URL ou URL absolue dans
|
||||
# le périmètre). Yavsc.Org expose l'auth via IdentityServer4 — la page
|
||||
# de login MVC (POST /account/login) et le endpoint token OAuth/OIDC
|
||||
# (POST /connect/token). Ciblez le endpoint que vous voulez éprouver.
|
||||
# Exemples :
|
||||
# LOGIN_PATH="/account/login" # formulaire MVC (HTML)
|
||||
# LOGIN_PATH="/connect/token" # token OAuth2 (form-urlencoded)
|
||||
LOGIN_PATH="/connect/token"
|
||||
# Champ du nom d'utilisateur dans le corps JSON de la requête.
|
||||
LOGIN_USER_FIELD="username"
|
||||
# Champ du mot de passe.
|
||||
LOGIN_PASS_FIELD="password"
|
||||
# Utilisateur ciblé (le bruteforceur fait varier le mot de passe).
|
||||
LOGIN_USERNAME="test"
|
||||
# Wordlist de mots de passe (un par ligne). Ne PAS committer la vôtre ;
|
||||
# utilisez le dossier wordlists/ (ignoré) ou un chemin absolu hors dépôt.
|
||||
LOGIN_WORDLIST="./wordlists/passwords.txt"
|
||||
# Nombre maximum de tentatives (0 = illimité, déconseillé).
|
||||
LOGIN_MAX_ATTEMPTS=0
|
||||
# Indice de succès : code HTTP renvoyé en cas de connexion réussie
|
||||
# (typiquement 200). Les autres codes sont considérés comme échec.
|
||||
LOGIN_SUCCESS_CODE=200
|
||||
# Type de corps de la requête de connexion :
|
||||
# "json" → application/json (API JSON)
|
||||
# "form" → application/x-www-form-urlencoded (OAuth2 /connect/token,
|
||||
# formulaire MVC)
|
||||
# IdentityServer /connect/token attend "form" avec grant_type=password.
|
||||
LOGIN_CONTENT_TYPE="form"
|
||||
# Pour OAuth2 password grant, le corps form doit inclure grant_type.
|
||||
# Si LOGIN_BODY_FORM est défini, il est utilisé tel quel (placeholders
|
||||
# {{USER}} / {{PASS}}) ; sinon le script construit le form à partir
|
||||
# des champs ci-dessus + grant_type=password.
|
||||
#LOGIN_BODY_FORM='grant_type=password&username={{USER}}&password={{PASS}}&client_id=ro_client&scope=api'
|
||||
36
tools/api-fuzzer/README.md
Normal file
36
tools/api-fuzzer/README.md
Normal 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.
|
||||
34
tools/api-fuzzer/endpoints.txt
Normal file
34
tools/api-fuzzer/endpoints.txt
Normal 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
83
tools/api-fuzzer/fuzz.sh
Executable 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."
|
||||
43
tools/brute-login/README.md
Normal file
43
tools/brute-login/README.md
Normal 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
130
tools/brute-login/brute-login.sh
Executable 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
|
||||
34
tools/config-scan/README.md
Normal file
34
tools/config-scan/README.md
Normal 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
99
tools/config-scan/scan.sh
Executable 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."
|
||||
41
tools/rate-limit-probe/README.md
Normal file
41
tools/rate-limit-probe/README.md
Normal 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
96
tools/rate-limit-probe/probe.sh
Executable 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
|
||||
0
wordlists/.gitkeep
Normal file
0
wordlists/.gitkeep
Normal file
Loading…
Add table
Add a link
Reference in a new issue