#!/bin/bash # 11 - Timing Oracle Tests # Methodology: 30-sample mean + stddev per group; signal is real if |diff| > 3*stddev # Covers HTTP/1.1, HTTP/2, constant-time auth, padding oracle SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../config.sh" OUT_FILE="$OUT/11_timing_oracle.txt" # --local flag: tighten threshold when running inside same datacenter/VPN # Remote internet jitter is ~20-50ms; local jitter is ~1-5ms # Default threshold = 3σ with min 15ms floor # --local threshold = 3σ with min 3ms floor (catches subtle constant-time violations) LOCAL_MODE="${LOCAL_MODE:-0}" if [ "$LOCAL_MODE" = "1" ]; then TIMING_FLOOR=3 echo "Mode: LOCAL — timing floor 3ms (tight, for same-datacenter runs)" | tee -a "$OUT_FILE" else TIMING_FLOOR=15 echo "Mode: REMOTE — timing floor 15ms (guards against internet jitter)" | tee -a "$OUT_FILE" fi echo "=== 11. Timing Oracle Tests ===" | tee "$OUT_FILE" echo "Methodology: 50 samples per group, mean ± stddev, signal threshold = 3σ" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── timing harness ──────────────────────────────────────────────────────────── # Returns space-separated list of ms timings sample_times() { local n="$1"; shift local times="" for i in $(seq 1 "$n"); do T=$(python3 -c "import time; print(int(time.time()*1000))") curl -sk -o /dev/null --max-time 15 "$@" 2>/dev/null E=$(python3 -c "import time; print(int(time.time()*1000))") times="$times $((E - T))" done echo "$times" } # Compute mean and stddev from space-separated ms list stats() { python3 -c " import sys, math, statistics vals = [int(x) for x in sys.stdin.read().split() if x] if not vals: print('N=0 mean=0 stddev=0'); sys.exit() m = statistics.mean(vals) s = statistics.pstdev(vals) if len(vals) > 1 else 0 print(f'N={len(vals)} mean={m:.1f}ms stddev={s:.1f}ms min={min(vals)}ms max={max(vals)}ms') " } compare_groups() { # compare_groups "label_a" times_a "label_b" times_b local la="$1" ta="$2" lb="$3" tb="$4" python3 -c " import sys, math, statistics la,ta,lb,tb = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] a = [int(x) for x in ta.split() if x] b = [int(x) for x in tb.split() if x] if not a or not b: print(' [SKIP] insufficient data'); sys.exit() ma, mb = statistics.mean(a), statistics.mean(b) sa = statistics.pstdev(a) if len(a)>1 else 1 sb = statistics.pstdev(b) if len(b)>1 else 1 diff = abs(ma - mb) import os floor = int(os.environ.get("TIMING_FLOOR", "15")) threshold = 3 * max(sa, sb, floor) verdict = 'TIMING-LEAK' if diff > threshold else 'OK' print(f' [{verdict}] {la}: {ma:.1f}ms±{sa:.1f} vs {lb}: {mb:.1f}ms±{sb:.1f} diff={diff:.1f}ms threshold={threshold:.1f}ms') " "$la" "$ta" "$lb" "$tb" } SAMPLES=50 # ── 1. API Key Timing Oracle ────────────────────────────────────────────────── echo "--- API Key Timing Oracle (HTTP/2, $SAMPLES samples each) ---" | tee -a "$OUT_FILE" echo " Sampling invalid key..." | tee -a "$OUT_FILE" T_INVALID=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Authorization: Bearer INVALID_KEY_XXXXXXXXXXX' \ -H 'Content-Type: application/json' \ -d '{"data":"test","algorithm":"classical"}' \ "$API_TARGET/encrypt") echo " Invalid key: $(echo "$T_INVALID" | stats)" | tee -a "$OUT_FILE" echo " Sampling no-key (missing header)..." | tee -a "$OUT_FILE" T_NOKEY=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Content-Type: application/json' \ -d '{"data":"test","algorithm":"classical"}' \ "$API_TARGET/encrypt") echo " No key: $(echo "$T_NOKEY" | stats)" | tee -a "$OUT_FILE" echo " Sampling malformed key (wrong prefix)..." | tee -a "$OUT_FILE" T_MALFORMED=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Authorization: Basic INVALID_BASE64==' \ -H 'Content-Type: application/json' \ -d '{"data":"test","algorithm":"classical"}' \ "$API_TARGET/encrypt") echo " Malformed: $(echo "$T_MALFORMED" | stats)" | tee -a "$OUT_FILE" compare_groups "invalid-key" "$T_INVALID" "no-key" "$T_NOKEY" | tee -a "$OUT_FILE" compare_groups "invalid-key" "$T_INVALID" "malformed-key" "$T_MALFORMED" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── 2. Admin Login Username Enumeration ─────────────────────────────────────── echo "--- Admin Login Timing Oracle ($SAMPLES samples each) ---" | tee -a "$OUT_FILE" echo " Sampling likely-valid username 'admin'..." | tee -a "$OUT_FILE" T_ADMIN=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'username=admin&password=wrongpassword_xyz_pentest' \ "$TARGET/admin/") echo " 'admin': $(echo "$T_ADMIN" | stats)" | tee -a "$OUT_FILE" echo " Sampling nonexistent username..." | tee -a "$OUT_FILE" T_NOUSER=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'username=nonexistent_user_xyz_1234&password=wrongpassword_xyz_pentest' \ "$TARGET/admin/") echo " nonexistent: $(echo "$T_NOUSER" | stats)" | tee -a "$OUT_FILE" compare_groups "admin" "$T_ADMIN" "nonexistent" "$T_NOUSER" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── 3. Decrypt Padding Oracle ───────────────────────────────────────────────── echo "--- Decrypt Padding Oracle ($SAMPLES samples each) ---" | tee -a "$OUT_FILE" SHORT_B64="aGVsbG8=" # "hello" — too short, invalid ciphertext LONG_B64=$(python3 -c "import base64; print(base64.b64encode(b'X'*512).decode())") echo " Sampling short invalid ciphertext..." | tee -a "$OUT_FILE" T_SHORT=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Content-Type: application/json' \ -d "{\"data\":\"$SHORT_B64\",\"algorithm\":\"classical\"}" \ "$API_TARGET/decrypt") echo " Short (8b): $(echo "$T_SHORT" | stats)" | tee -a "$OUT_FILE" echo " Sampling long invalid ciphertext..." | tee -a "$OUT_FILE" T_LONG=$(sample_times $SAMPLES \ -X POST --http2 \ -H 'Content-Type: application/json' \ -d "{\"data\":\"$LONG_B64\",\"algorithm\":\"classical\"}" \ "$API_TARGET/decrypt") echo " Long (512b): $(echo "$T_LONG" | stats)" | tee -a "$OUT_FILE" compare_groups "short-ciphertext" "$T_SHORT" "long-ciphertext" "$T_LONG" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── 4. HTTP/1.1 vs HTTP/2 Timing Baseline ──────────────────────────────────── echo "--- Protocol Timing Baseline (HTTP/1.1 vs HTTP/2, $SAMPLES samples) ---" | tee -a "$OUT_FILE" echo " HTTP/1.1..." | tee -a "$OUT_FILE" T_H1=$(sample_times $SAMPLES --http1.1 "$TARGET/") echo " HTTP/1.1: $(echo "$T_H1" | stats)" | tee -a "$OUT_FILE" echo " HTTP/2..." | tee -a "$OUT_FILE" T_H2=$(sample_times $SAMPLES --http2 "$TARGET/") echo " HTTP/2: $(echo "$T_H2" | stats)" | tee -a "$OUT_FILE" compare_groups "HTTP/1.1" "$T_H1" "HTTP/2" "$T_H2" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" # ── 5. WAF Timing — blocked vs clean ───────────────────────────────────────── echo "--- WAF Timing Oracle (blocked vs clean, $SAMPLES samples) ---" | tee -a "$OUT_FILE" echo " Clean request..." | tee -a "$OUT_FILE" T_CLEAN=$(sample_times $SAMPLES --http2 -A "$BROWSER_UA" "$TARGET/") echo " Clean: $(echo "$T_CLEAN" | stats)" | tee -a "$OUT_FILE" echo " Blocked (SQLi) request..." | tee -a "$OUT_FILE" T_BLOCKED=$(sample_times $SAMPLES --http2 -A "$BROWSER_UA" \ "$TARGET/?q=$(python3 -c "import urllib.parse; print(urllib.parse.quote(\"' OR 1=1--\"))")") echo " Blocked: $(echo "$T_BLOCKED" | stats)" | tee -a "$OUT_FILE" compare_groups "clean" "$T_CLEAN" "waf-blocked" "$T_BLOCKED" | tee -a "$OUT_FILE" echo " (significant diff = WAF adds measurable latency — timing side-channel)" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" echo "=== 11. Timing Oracle COMPLETE ===" | tee -a "$OUT_FILE"