#!/bin/bash # 25 - Container & Kubernetes Runtime Security # Tests: Docker API exposure, container escape vectors, K8s runtime misconfigs, # privileged endpoints, image registry, pod security, capabilities leakage SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../config.sh" OUT_FILE="$OUT/25_container_security.txt" echo "=== 25. Container & Kubernetes Runtime Security ===" | tee "$OUT_FILE" echo "Target: $TARGET" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" HOST=$(echo "$TARGET" | sed 's|https\?://||' | cut -d/ -f1) IP=$(dig +short "$HOST" | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) [ -z "$IP" ] && IP="$HOST" echo "Resolved $HOST → $IP" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" do_port() { local label="$1"; local port="$2" if timeout 3 bash -c "echo >/dev/tcp/$IP/$port" 2>/dev/null; then printf '[OPEN] %s (port %s)\n' "$label" "$port" | tee -a "$OUT_FILE" else printf '[CLOSED] %s (port %s)\n' "$label" "$port" | tee -a "$OUT_FILE" fi } do_test() { local label="$1"; shift local code code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 "$@") printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" } do_body() { local label="$1"; local pattern="$2"; shift 2 local body code body=$(curl -sk --max-time 5 "$@") code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 "$@") printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" if [ -n "$pattern" ] && echo "$body" | grep -qiE "$pattern"; then echo " [WARN] Pattern matched: $(echo "$body" | grep -ioE "$pattern" | head -2 | tr '\n' ' ')" | tee -a "$OUT_FILE" fi } # ── 1. Container Management Port Exposure ───────────────────────────────────── echo "--- Container Management Port Scan ---" | tee -a "$OUT_FILE" do_port "Docker daemon (unauth)" 2375 do_port "Docker daemon (TLS)" 2376 do_port "Kubernetes API server" 6443 do_port "Kubernetes API (HTTP)" 8080 do_port "Kubernetes kubelet" 10250 do_port "Kubernetes kubelet RO" 10255 do_port "Kubernetes etcd" 2379 do_port "Kubernetes etcd peer" 2380 do_port "Container Registry" 5000 do_port "Portainer" 9000 do_port "Rancher" 8443 do_port "Podman API" 8888 echo "" | tee -a "$OUT_FILE" # ── 2. Docker Daemon API (HTTP) ─────────────────────────────────────────────── echo "--- Docker Daemon API ---" | tee -a "$OUT_FILE" DOCKER_HTTP="http://$IP:2375" do_body "Docker version endpoint" '"Version"' "$DOCKER_HTTP/version" do_body "Docker containers list" '"Id"' "$DOCKER_HTTP/containers/json" do_body "Docker images list" '"RepoTags"' "$DOCKER_HTTP/images/json" do_body "Docker info (privileged?)" '"Swarm"\|"Privileged"' "$DOCKER_HTTP/info" do_body "Docker exec endpoint" '"Id"' -X POST -H 'Content-Type: application/json' \ -d '{"AttachStdout":true,"Cmd":["id"]}' "$DOCKER_HTTP/containers/$(hostname)/exec" echo "" | tee -a "$OUT_FILE" # ── 3. Kubernetes API Server ────────────────────────────────────────────────── echo "--- Kubernetes API Server ---" | tee -a "$OUT_FILE" K8S="https://$IP:6443" K8S_HTTP="http://$IP:8080" do_body "K8s API root (unauthenticated)" '"paths"' "$K8S/" do_body "K8s API root (HTTP insecure)" '"paths"' "$K8S_HTTP/" do_body "K8s namespaces (unauth)" '"items"' "$K8S/api/v1/namespaces" do_body "K8s pods (unauth)" '"items"' "$K8S/api/v1/pods" do_body "K8s secrets (unauth)" '"items"' "$K8S/api/v1/secrets" do_body "K8s service accounts" '"items"' "$K8S/api/v1/serviceaccounts" do_body "K8s version disclosure" '"gitVersion"' "$K8S/version" do_body "K8s healthz" 'ok' "$K8S/healthz" echo "" | tee -a "$OUT_FILE" # ── 4. Kubelet API (node-level) ─────────────────────────────────────────────── echo "--- Kubelet API ---" | tee -a "$OUT_FILE" KUBELET="https://$IP:10250" KUBELET_RO="http://$IP:10255" do_body "Kubelet /pods (unauth)" '"items"' "$KUBELET/pods" do_body "Kubelet /run exec probe" '.' -X POST "$KUBELET/run/default/testpod/container" do_body "Kubelet read-only /pods" '"items"' "$KUBELET_RO/pods" do_body "Kubelet read-only /metrics" 'kubelet_' "$KUBELET_RO/metrics" do_body "Kubelet /metrics (auth)" 'kubelet_' "$KUBELET/metrics" echo "" | tee -a "$OUT_FILE" # ── 5. etcd (cluster secrets store) ────────────────────────────────────────── echo "--- etcd Cluster Store ---" | tee -a "$OUT_FILE" ETCD="http://$IP:2379" do_body "etcd health check" '"health"' "$ETCD/health" do_body "etcd v2 keys (root)" '"node"' "$ETCD/v2/keys/" do_body "etcd v2 keys /registry" '"node"' "$ETCD/v2/keys/registry" do_body "etcd v3 range (gRPC-Web)" '.' -X POST -H 'Content-Type: application/json' \ -d '{"key":"Lg==","range_end":"Lw=="}' "$ETCD/v3/kv/range" echo "" | tee -a "$OUT_FILE" # ── 6. Container Registry ───────────────────────────────────────────────────── echo "--- Container Image Registry ---" | tee -a "$OUT_FILE" REGISTRY="http://$IP:5000" do_body "Registry catalog (unauthenticated)" '"repositories"' "$REGISTRY/v2/_catalog" do_body "Registry API v2 ping" '{}' "$REGISTRY/v2/" do_body "Registry manifests probe" '"schemaVersion"' "$REGISTRY/v2/${PROJECT_NAME}/manifests/latest" # Check public Docker Hub namespace do_test "Docker Hub pqcrypta namespace" "https://hub.docker.com/v2/repositories/${PROJECT_NAME}/?page_size=1" echo "" | tee -a "$OUT_FILE" # ── 7. Container Escape Surface (via proxy/app) ─────────────────────────────── echo "--- Container Escape Vectors (via application) ---" | tee -a "$OUT_FILE" # procfs access via path traversal do_test "procfs via traversal /proc/self/environ" \ -A "$BROWSER_UA" "$TARGET/../../../../proc/self/environ" do_test "procfs cgroup namespace check" \ -A "$BROWSER_UA" "$TARGET/../../../../proc/1/cgroup" do_test "Docker socket path traversal" \ -A "$BROWSER_UA" "$TARGET/../../../../var/run/docker.sock" do_test "hostPath /etc/shadow via traversal" \ -A "$BROWSER_UA" "$TARGET/../../../../etc/shadow" do_test "hostPath /etc/kubernetes/admin.conf" \ -A "$BROWSER_UA" "$TARGET/../../../../etc/kubernetes/admin.conf" echo "" | tee -a "$OUT_FILE" # ── 8. Container Metadata & Runtime Info ───────────────────────────────────── echo "--- Container Metadata Endpoints ---" | tee -a "$OUT_FILE" # Kubernetes service account token paths via SSRF do_test "K8s serviceaccount token probe (SSRF)" \ -A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('file:///var/run/secrets/kubernetes.io/serviceaccount/token'))")" do_test "K8s namespace probe (SSRF)" \ -A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('file:///var/run/secrets/kubernetes.io/serviceaccount/namespace'))")" # Internal K8s service DNS do_test "K8s API via internal DNS (SSRF)" \ -A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('https://kubernetes.default.svc/api/v1/namespaces'))")" do_test "K8s API via internal IP (SSRF)" \ -A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('https://10.96.0.1/api/v1/namespaces'))")" echo "" | tee -a "$OUT_FILE" # ── 9. Pod Security / Capability Leakage via App ───────────────────────────── echo "--- Privileged Container Detection (App Response Analysis) ---" | tee -a "$OUT_FILE" # If app is in a container, check what it reveals about its environment APP_RESP=$(curl -sk -A "$BROWSER_UA" --max-time 5 "$API_TARGET/status" 2>/dev/null) for FIELD in container docker kubernetes pod namespace node_name host_ip; do if echo "$APP_RESP" | grep -qi "\"$FIELD\""; then echo " [WARN] Field '$FIELD' exposed in API status response" | tee -a "$OUT_FILE" fi done # Check if hostname in response looks like a pod name (random suffix) HOSTNAME_IN=$(echo "$APP_RESP" | grep -ioP '"hostname"\s*:\s*"[^"]+"' | head -1) [ -n "$HOSTNAME_IN" ] && echo " Hostname field: $HOSTNAME_IN" | tee -a "$OUT_FILE" || \ echo " [OK] No hostname field in API response" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" echo "=== 25. Container Security COMPLETE ===" | tee -a "$OUT_FILE"