| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
|
| 6 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 7 |
source "$SCRIPT_DIR/../config.sh" |
| 8 |
|
| 9 |
OUT_FILE="$OUT/25_container_security.txt" |
| 10 |
echo "=== 25. Container & Kubernetes Runtime Security ===" | tee "$OUT_FILE" |
| 11 |
echo "Target: $TARGET" | tee -a "$OUT_FILE" |
| 12 |
echo "" | tee -a "$OUT_FILE" |
| 13 |
|
| 14 |
HOST=$(echo "$TARGET" | sed 's|https\?://||' | cut -d/ -f1) |
| 15 |
IP=$(dig +short "$HOST" | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) |
| 16 |
[ -z "$IP" ] && IP="$HOST" |
| 17 |
echo "Resolved $HOST โ $IP" | tee -a "$OUT_FILE" |
| 18 |
echo "" | tee -a "$OUT_FILE" |
| 19 |
|
| 20 |
do_port() { |
| 21 |
local label="$1"; local port="$2" |
| 22 |
if timeout 3 bash -c "echo >/dev/tcp/$IP/$port" 2>/dev/null; then |
| 23 |
printf '[OPEN] %s (port %s)\n' "$label" "$port" | tee -a "$OUT_FILE" |
| 24 |
else |
| 25 |
printf '[CLOSED] %s (port %s)\n' "$label" "$port" | tee -a "$OUT_FILE" |
| 26 |
fi |
| 27 |
} |
| 28 |
|
| 29 |
do_test() { |
| 30 |
local label="$1"; shift |
| 31 |
local code |
| 32 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 "$@") |
| 33 |
printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" |
| 34 |
} |
| 35 |
|
| 36 |
do_body() { |
| 37 |
local label="$1"; local pattern="$2"; shift 2 |
| 38 |
local body code |
| 39 |
body=$(curl -sk --max-time 5 "$@") |
| 40 |
code=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 "$@") |
| 41 |
printf '[%s] %s\n' "$code" "$label" | tee -a "$OUT_FILE" |
| 42 |
if [ -n "$pattern" ] && echo "$body" | grep -qiE "$pattern"; then |
| 43 |
echo " [WARN] Pattern matched: $(echo "$body" | grep -ioE "$pattern" | head -2 | tr '\n' ' ')" | tee -a "$OUT_FILE" |
| 44 |
fi |
| 45 |
} |
| 46 |
|
| 47 |
| 48 |
echo "--- Container Management Port Scan ---" | tee -a "$OUT_FILE" |
| 49 |
do_port "Docker daemon (unauth)" 2375 |
| 50 |
do_port "Docker daemon (TLS)" 2376 |
| 51 |
do_port "Kubernetes API server" 6443 |
| 52 |
do_port "Kubernetes API (HTTP)" 8080 |
| 53 |
do_port "Kubernetes kubelet" 10250 |
| 54 |
do_port "Kubernetes kubelet RO" 10255 |
| 55 |
do_port "Kubernetes etcd" 2379 |
| 56 |
do_port "Kubernetes etcd peer" 2380 |
| 57 |
do_port "Container Registry" 5000 |
| 58 |
do_port "Portainer" 9000 |
| 59 |
do_port "Rancher" 8443 |
| 60 |
do_port "Podman API" 8888 |
| 61 |
echo "" | tee -a "$OUT_FILE" |
| 62 |
|
| 63 |
| 64 |
echo "--- Docker Daemon API ---" | tee -a "$OUT_FILE" |
| 65 |
DOCKER_HTTP="http://$IP:2375" |
| 66 |
do_body "Docker version endpoint" '"Version"' "$DOCKER_HTTP/version" |
| 67 |
do_body "Docker containers list" '"Id"' "$DOCKER_HTTP/containers/json" |
| 68 |
do_body "Docker images list" '"RepoTags"' "$DOCKER_HTTP/images/json" |
| 69 |
do_body "Docker info (privileged?)" '"Swarm"\|"Privileged"' "$DOCKER_HTTP/info" |
| 70 |
do_body "Docker exec endpoint" '"Id"' -X POST -H 'Content-Type: application/json' \ |
| 71 |
-d '{"AttachStdout":true,"Cmd":["id"]}' "$DOCKER_HTTP/containers/$(hostname)/exec" |
| 72 |
echo "" | tee -a "$OUT_FILE" |
| 73 |
|
| 74 |
| 75 |
echo "--- Kubernetes API Server ---" | tee -a "$OUT_FILE" |
| 76 |
K8S="https://$IP:6443" |
| 77 |
K8S_HTTP="http://$IP:8080" |
| 78 |
do_body "K8s API root (unauthenticated)" '"paths"' "$K8S/" |
| 79 |
do_body "K8s API root (HTTP insecure)" '"paths"' "$K8S_HTTP/" |
| 80 |
do_body "K8s namespaces (unauth)" '"items"' "$K8S/api/v1/namespaces" |
| 81 |
do_body "K8s pods (unauth)" '"items"' "$K8S/api/v1/pods" |
| 82 |
do_body "K8s secrets (unauth)" '"items"' "$K8S/api/v1/secrets" |
| 83 |
do_body "K8s service accounts" '"items"' "$K8S/api/v1/serviceaccounts" |
| 84 |
do_body "K8s version disclosure" '"gitVersion"' "$K8S/version" |
| 85 |
do_body "K8s healthz" 'ok' "$K8S/healthz" |
| 86 |
echo "" | tee -a "$OUT_FILE" |
| 87 |
|
| 88 |
| 89 |
echo "--- Kubelet API ---" | tee -a "$OUT_FILE" |
| 90 |
KUBELET="https://$IP:10250" |
| 91 |
KUBELET_RO="http://$IP:10255" |
| 92 |
do_body "Kubelet /pods (unauth)" '"items"' "$KUBELET/pods" |
| 93 |
do_body "Kubelet /run exec probe" '.' -X POST "$KUBELET/run/default/testpod/container" |
| 94 |
do_body "Kubelet read-only /pods" '"items"' "$KUBELET_RO/pods" |
| 95 |
do_body "Kubelet read-only /metrics" 'kubelet_' "$KUBELET_RO/metrics" |
| 96 |
do_body "Kubelet /metrics (auth)" 'kubelet_' "$KUBELET/metrics" |
| 97 |
echo "" | tee -a "$OUT_FILE" |
| 98 |
|
| 99 |
| 100 |
echo "--- etcd Cluster Store ---" | tee -a "$OUT_FILE" |
| 101 |
ETCD="http://$IP:2379" |
| 102 |
do_body "etcd health check" '"health"' "$ETCD/health" |
| 103 |
do_body "etcd v2 keys (root)" '"node"' "$ETCD/v2/keys/" |
| 104 |
do_body "etcd v2 keys /registry" '"node"' "$ETCD/v2/keys/registry" |
| 105 |
do_body "etcd v3 range (gRPC-Web)" '.' -X POST -H 'Content-Type: application/json' \ |
| 106 |
-d '{"key":"Lg==","range_end":"Lw=="}' "$ETCD/v3/kv/range" |
| 107 |
echo "" | tee -a "$OUT_FILE" |
| 108 |
|
| 109 |
| 110 |
echo "--- Container Image Registry ---" | tee -a "$OUT_FILE" |
| 111 |
REGISTRY="http://$IP:5000" |
| 112 |
do_body "Registry catalog (unauthenticated)" '"repositories"' "$REGISTRY/v2/_catalog" |
| 113 |
do_body "Registry API v2 ping" '{}' "$REGISTRY/v2/" |
| 114 |
do_body "Registry manifests probe" '"schemaVersion"' "$REGISTRY/v2/${PROJECT_NAME}/manifests/latest" |
| 115 |
| 116 |
do_test "Docker Hub pqcrypta namespace" "https://hub.docker.com/v2/repositories/${PROJECT_NAME}/?page_size=1" |
| 117 |
echo "" | tee -a "$OUT_FILE" |
| 118 |
|
| 119 |
| 120 |
echo "--- Container Escape Vectors (via application) ---" | tee -a "$OUT_FILE" |
| 121 |
| 122 |
do_test "procfs via traversal /proc/self/environ" \ |
| 123 |
-A "$BROWSER_UA" "$TARGET/../../../../proc/self/environ" |
| 124 |
do_test "procfs cgroup namespace check" \ |
| 125 |
-A "$BROWSER_UA" "$TARGET/../../../../proc/1/cgroup" |
| 126 |
do_test "Docker socket path traversal" \ |
| 127 |
-A "$BROWSER_UA" "$TARGET/../../../../var/run/docker.sock" |
| 128 |
do_test "hostPath /etc/shadow via traversal" \ |
| 129 |
-A "$BROWSER_UA" "$TARGET/../../../../etc/shadow" |
| 130 |
do_test "hostPath /etc/kubernetes/admin.conf" \ |
| 131 |
-A "$BROWSER_UA" "$TARGET/../../../../etc/kubernetes/admin.conf" |
| 132 |
echo "" | tee -a "$OUT_FILE" |
| 133 |
|
| 134 |
| 135 |
echo "--- Container Metadata Endpoints ---" | tee -a "$OUT_FILE" |
| 136 |
| 137 |
do_test "K8s serviceaccount token probe (SSRF)" \ |
| 138 |
-A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('file:///var/run/secrets/kubernetes.io/serviceaccount/token'))")" |
| 139 |
do_test "K8s namespace probe (SSRF)" \ |
| 140 |
-A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('file:///var/run/secrets/kubernetes.io/serviceaccount/namespace'))")" |
| 141 |
| 142 |
do_test "K8s API via internal DNS (SSRF)" \ |
| 143 |
-A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('https://kubernetes.default.svc/api/v1/namespaces'))")" |
| 144 |
do_test "K8s API via internal IP (SSRF)" \ |
| 145 |
-A "$BROWSER_UA" "$TARGET/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('https://10.96.0.1/api/v1/namespaces'))")" |
| 146 |
echo "" | tee -a "$OUT_FILE" |
| 147 |
|
| 148 |
| 149 |
echo "--- Privileged Container Detection (App Response Analysis) ---" | tee -a "$OUT_FILE" |
| 150 |
| 151 |
APP_RESP=$(curl -sk -A "$BROWSER_UA" --max-time 5 "$API_TARGET/status" 2>/dev/null) |
| 152 |
for FIELD in container docker kubernetes pod namespace node_name host_ip; do |
| 153 |
if echo "$APP_RESP" | grep -qi "\"$FIELD\""; then |
| 154 |
echo " [WARN] Field '$FIELD' exposed in API status response" | tee -a "$OUT_FILE" |
| 155 |
fi |
| 156 |
done |
| 157 |
| 158 |
HOSTNAME_IN=$(echo "$APP_RESP" | grep -ioP '"hostname"\s*:\s*"[^"]+"' | head -1) |
| 159 |
[ -n "$HOSTNAME_IN" ] && echo " Hostname field: $HOSTNAME_IN" | tee -a "$OUT_FILE" || \ |
| 160 |
echo " [OK] No hostname field in API response" | tee -a "$OUT_FILE" |
| 161 |
echo "" | tee -a "$OUT_FILE" |
| 162 |
|
| 163 |
echo "=== 25. Container Security COMPLETE ===" | tee -a "$OUT_FILE" |
| 164 |
|