#!/usr/bin/env bash # ===================================================================== # Tests fonctionnels du PoC BunkerWeb # Lance une série de requêtes légitimes et malveillantes au travers du # WAF et compare le code HTTP obtenu au comportement attendu. # # Usage : bwpoc-tests.sh [--direct] [--only ] # --direct rejoue les attaques directement sur les applis, sans WAF # (nécessite apps_direct_access_sources, voir README) # --only ne lance que les tests dont le titre contient # # Variables (fichier bwpoc-tests.env à côté du script, ou environnement) : # WAF_IP, DOMAIN, APPS_IP, PORT_WWW, PORT_SHOP, PORT_API # # ATTENTION : les derniers tests provoquent volontairement le # bannissement de votre IP sur api. (BAD_BEHAVIOR_BAN_TIME). # ===================================================================== set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" # shellcheck source=/dev/null [ -f "$SCRIPT_DIR/bwpoc-tests.env" ] && . "$SCRIPT_DIR/bwpoc-tests.env" WAF_IP="${WAF_IP:?WAF_IP non défini (bwpoc-tests.env ou environnement)}" DOMAIN="${DOMAIN:-bwpoc.internal}" APPS_IP="${APPS_IP:-}" PORT_SHOP="${PORT_SHOP:-8082}" PORT_API="${PORT_API:-8083}" DIRECT=0 ONLY="" while [ $# -gt 0 ]; do case "$1" in --direct) DIRECT=1 ;; --only) ONLY="$2"; shift ;; -h|--help) sed -n '2,18p' "$0"; exit 0 ;; *) echo "Option inconnue : $1" >&2; exit 2 ;; esac shift done if [ -t 1 ]; then G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; B=$'\e[1m'; N=$'\e[0m'; else G=; R=; Y=; B=; N=; fi PASS=0; FAIL=0; INFO=0 UA="Mozilla/5.0 (X11; Linux x86_64) bwpoc-tests" # Requête au travers du WAF : --resolve évite tout besoin de DNS. # Affiche " ". waf() { local host="$1" path="$2"; shift 2 curl -sk -o /dev/null -w '%{http_code} %{redirect_url}' --max-time 10 \ --resolve "$host.$DOMAIN:443:$WAF_IP" --resolve "$host.$DOMAIN:80:$WAF_IP" \ -A "$UA" "$@" "https://$host.$DOMAIN$path" } # Vérifie qu'un code fait partie de la liste attendue (séparateur |) check() { local title="$1" expected="$2" got="$3" level="${4:-strict}" if [ -n "$ONLY" ] && [[ "$title" != *"$ONLY"* ]]; then return; fi local code="${got%% *}" if [[ "|$expected|" == *"|$code|"* ]]; then printf ' %s[OK]%s %-58s %s\n' "$G" "$N" "$title" "$code"; PASS=$((PASS + 1)) elif [ "$level" = info ]; then printf ' %s[INFO]%s %-58s %s (attendu %s)\n' "$Y" "$N" "$title" "$code" "$expected"; INFO=$((INFO + 1)) else printf ' %s[KO]%s %-58s %s (attendu %s)\n' "$R" "$N" "$title" "$code" "$expected"; FAIL=$((FAIL + 1)) fi } section() { printf '\n%s== %s ==%s\n' "$B" "$1" "$N"; } # --------------------------------------------------------------------- if [ "$DIRECT" -eq 1 ]; then section "Sans WAF : attaques envoyées directement à $APPS_IP" for t in \ "SQLi Juice Shop (recherche produits)|http://$APPS_IP:$PORT_SHOP/rest/products/search?q=%27))%20OR%201%3D1--" \ "XSS réfléchie|http://$APPS_IP:$PORT_SHOP/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" \ "Méthode PUT sur l'API|http://$APPS_IP:$PORT_API/put"; do title="${t%%|*}"; url="${t#*|}" if [[ "$title" == *PUT* ]]; then code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -X PUT "$url") else code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url") fi printf ' %-60s %s\n' "$title" "$code" done echo echo "Code 000 = port filtré (seul le WAF est autorisé). Sinon, comparez avec le passage par le WAF." exit 0 fi echo "${B}PoC BunkerWeb${N} : WAF $WAF_IP, domaine $DOMAIN" section "Transport et socle" check "HTTP redirigé vers HTTPS (api)" "301|302|307|308" \ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 --resolve "api.$DOMAIN:80:$WAF_IP" "http://api.$DOMAIN/get")" check "Host inconnu refusé (DISABLE_DEFAULT_SERVER)" "000|400|403|404|444" \ "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 --resolve "inconnu.$DOMAIN:443:$WAF_IP" "https://inconnu.$DOMAIN/")" check "Requête légitime sur l'API" "200" "$(waf api /get)" section "En-têtes et compression" HDRS=$(curl -sk -D - -o /dev/null --max-time 10 --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/get" | tr -d '\r' | tr 'A-Z' 'a-z') for h in strict-transport-security x-frame-options x-content-type-options content-security-policy referrer-policy; do if grep -q "^$h:" <<<"$HDRS"; then check "En-tête $h ajouté" "yes" "yes"; else check "En-tête $h ajouté" "yes" "no"; fi done if grep -q "^server:" <<<"$HDRS"; then check "En-tête Server masqué" "masqué" "présent" info; else check "En-tête Server masqué" "masqué" "masqué"; fi XFF=$(curl -sk --max-time 10 --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/headers" | jq -r '.headers["X-Forwarded-For"][0] // empty' 2>/dev/null) if [ -n "$XFF" ]; then check "Le backend reçoit X-Forwarded-For ($XFF)" "yes" "yes"; else check "Le backend reçoit X-Forwarded-For" "yes" "no"; fi for enc in br gzip; do CE=$(curl -sk -D - -o /dev/null --max-time 10 -H "Accept-Encoding: $enc" --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/html" \ | tr -d '\r' | awk -F': ' 'tolower($1)=="content-encoding"{print $2}') check "Compression $enc" "$enc" "${CE:-aucune}" done section "Antibot (www)" RES=$(waf www /) if [[ "$RES" == *challenge* ]] || [[ "${RES%% *}" == 200 && $(curl -sk --max-time 10 --resolve "www.$DOMAIN:443:$WAF_IP" "https://www.$DOMAIN/" | grep -ci challenge) -gt 0 ]]; then check "Un client sans JavaScript reste bloqué sur /challenge" "yes" "yes" else check "Un client sans JavaScript reste bloqué sur /challenge" "yes" "no ($RES)" fi section "WAF ModSecurity + OWASP CRS 4 (shop)" check "Page d'accueil Juice Shop" "200" "$(waf shop /)" check "SQLi : recherche produits ')) OR 1=1--" "403" "$(waf shop "/rest/products/search?q=%27))%20OR%201%3D1--")" check "XSS : " "403" "$(waf shop "/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E")" check "Traversée de répertoire ../../etc/passwd" "403" "$(waf shop "/?file=..%2F..%2F..%2F..%2Fetc%2Fpasswd")" check "Injection de commande ;cat /etc/passwd" "403" "$(waf api "/get?cmd=%3Bcat%20%2Fetc%2Fpasswd")" check "Règle ModSecurity maison ?poc=attack" "403" "$(waf api "/get?poc=attack")" section "Listes noires" check "User-Agent maison PoC-EvilBot (BLACKLIST_USER_AGENT)" "403" "$(waf shop / -A "PoC-EvilBot/1.0")" check "User-Agent sqlmap (liste communautaire)" "403" "$(waf shop / -A "sqlmap/1.8#stable (https://sqlmap.org)")" info section "Contraintes protocolaires (api)" check "Méthode PUT interdite (ALLOWED_METHODS)" "405" "$(waf api /put -X PUT -d x=1)" check "Corps de 2 Mo refusé (MAX_CLIENT_SIZE=1m)" "413" \ "$(head -c 2097152 /dev/zero | tr '\0' 'a' | curl -sk -o /dev/null -w '%{http_code}' --max-time 20 \ --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" -H 'Content-Type: text/plain' --data-binary @- "https://api.$DOMAIN/post")" section "Limitation de débit puis bad behavior (api)" echo " Envoi de 40 requêtes en parallèle (LIMIT_REQ_RATE=5r/s)..." CODES=$(seq 1 40 | xargs -P 20 -I{} curl -sk -o /dev/null -w '%{http_code}\n' --max-time 10 \ --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/get?n={}" | sort | uniq -c | tr '\n' ' ') echo " Répartition des codes : $CODES" if grep -qE '\b429\b' <<<"$CODES"; then check "Des requêtes ont été limitées (429)" "yes" "yes"; else check "Des requêtes ont été limitées (429)" "yes" "no"; fi echo " Envoi de 12 requêtes en erreur 404 (BAD_BEHAVIOR_THRESHOLD=10)..." for i in $(seq 1 12); do waf api "/status/404?i=$i" >/dev/null; done sleep 2 check "IP bannie : requête légitime désormais refusée" "403" "$(waf api /get)" check "Bannissement limité au service api (shop répond)" "200" "$(waf shop /)" # --------------------------------------------------------------------- printf '\n%sBilan%s : %s%d OK%s, %s%d KO%s, %d info\n' "$B" "$N" "$G" "$PASS" "$N" "$R" "$FAIL" "$N" "$INFO" MYIP=$(curl -sk --max-time 5 --resolve "shop.$DOMAIN:443:$WAF_IP" -A "$UA" -o /dev/null -w '%{local_ip}' "https://shop.$DOMAIN/") cat <