PoC BunkerWeb 1.6.17 : docker-compose sur Proxmox, déployé par Ansible
Stack BunkerWeb (scheduler, UI, MariaDB, Redis), sites de test (nginx, Juice Shop, go-httpbin), filtrage DOCKER-USER, script de tests, README et schéma d'architecture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
099584c95c
42 files changed
+4260
No files matched your search
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#!/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 <motif>]
|
||||
# --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 <motif>
|
||||
#
|
||||
# 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.<domaine> (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:-192.168.200.61}"
|
||||
DOMAIN="${DOMAIN:-bwpoc.internal}"
|
||||
APPS_IP="${APPS_IP:-192.168.200.62}"
|
||||
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 "<code_http> <url_de_redirection>".
|
||||
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 : <script>alert(1)</script>" "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 <<EOF
|
||||
|
||||
Votre IP ($MYIP) est bannie de api.$DOMAIN pendant BAD_BEHAVIOR_BAN_TIME.
|
||||
Pour lever le bannissement : UI BunkerWeb, menu Bans, ou sur la VM WAF :
|
||||
docker exec bw-scheduler bwcli unban $MYIP
|
||||
EOF
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in new issue
Block a user