130 lines
No EOL
4.5 KiB
Bash
Executable file
130 lines
No EOL
4.5 KiB
Bash
Executable file
#!/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 |