Un cluster kubernetes de l'entreprise a signalé une forte augmentation de la consommation de ses ressources, l'événement intervient bizarrement peu de temps après les précédents.
Durant les précédentes investigations, nous avons reçu des alertes concernant une consommation excessive des ressources disponibles dans l’un des clusters Kubernetes de l’entreprise, suggérant une activité potentiellement malveillante.
À ce stade, nous ne savons pas s’il s’agit d’une coïncidence ou non, mais les incidents précédents ayant révélé une intrusion ainsi que des mécanismes de déplacement latéral sur le système d’information, nous devons approfondir cette alerte. De plus, la personne ayant été compromise disposait d’accès à ce cluster.
Votre mission est d’analyser le cluster Kubernetes afin de déterminer s’il existe des activités suspectes et, le cas échéant, d’identifier les ressources qui auraient été créées ou modifiées par un attaquant.
Nous avons exporté la plupart des ressources Kubernetes auxquelles cette personne avait accès à l’aide de la commande suivante : `kubectl get $(kubectl api-resources --verbs=list -o name | tr '\n' ',' | sed 's/,$//') -o yaml > output.txt`.
Le cluster fonctionne sur un OS immuable et est composé de 3 noeuds, jouant à la fois les rôles de master et de worker.
Pour résoudre ce challenge, une bonne maîtrise de Kubernetes est nécessaire.
Chaque flag peut être découvert selon plusieurs approches ; nous en présentons ici une parmi d’autres.
À partir des éléments fournis, on peut rapidement déduire qu’il s’agit d’un crypto-miner dissimulé dans le cluster Kubernetes, un type d’attaque courant et bien documenté dans ce genre d’environnement.
En parcourant les pods, il faut prêter attention aux anomalies : namespace inhabituel, images suspectes, containers exécutés avec des privilèges élevés, ou exécutions à des heures non conventionnelles. Dans ce cas, le pod intéressant se trouve dans le namespace hera et exécute un container avec securityContext.privileged: true, ce qui confère des droits très sensibles au conteneur. L’inspection des logs des opérations kube-apiserver confirme des exécutions à des horaires anormaux, preuve d’exploitation. Le flag à récupérer à cette étape est l'heure de cette exécution.
Les indications montrent que l’attaquant a pu accéder à un nœud et y mener des opérations. Toutefois, les nœuds étant sur des systèmes immuables et ayant été redémarrés par l’équipe, les modifications locales non persistantes ont disparu après redémarrage. La persistance la plus probable reste donc au niveau Kubernetes (ex. ressources créées dans le cluster).
Une recherche systématique des ressources créées par l’attaquant ne révèle cependant rien d’évident : ni DaemonSet, ni CronJob, ni autre manifeste suspect détectable. Cette piste devient alors une impasse, si aucune ressource Kubernetes persistante n’est trouvée et que les nœuds sont immuables, il est probable que l’attaquant n’ait pas obtenu de mécanisme de persistance durable au niveau du cluster. C’est exactement ce qu’ils aimeraient qu’on croie. La piste paraît close, mais le silence peut être un leurre.
La première étape consiste à restaurer la sauvegarde etcd :
sudo etcdctl snapshot restore "/home/charon/fic/etcd.snap" \
--data-dir="/tmp/etcd-restore" \
--name=restored-etcd
Cette commande reconstruit un nouveau nœud etcd à partir de l’instantané, dans un répertoire de données propre.
Dans un second temps, il faut générer les certificats clients qui permettront à kubectl de s’authentifier auprès du kube-apiserver.
On crée pour cela une petite ICP locale :
mkdir -p ./kas-pki
openssl genrsa -out ./kas-pki/ca.key 4096
openssl req -x509 -new -nodes -key ./kas-pki/ca.key \
-subj "/CN=kas-client-ca" -days 3650 -out ./kas-pki/ca.crt
openssl genrsa -out ./kas-pki/user.key 2048
openssl req -new -key ./kas-pki/user.key \
-subj "/CN=charon/O=system:masters" \
-out ./kas-pki/user.csr
openssl x509 -req -in ./kas-pki/user.csr \
-CA ./kas-pki/ca.crt -CAkey ./kas-pki/ca.key -CAcreateserial \
-out ./kas-pki/user.crt -days 365
Ici :
ca.crt est le CA client que le kube-apiserver acceptera pour valider les certificats utilisateurs.user.crt + user.key constituent le certificat client utilisé par le kubectl.Ensuite, il faut générer la clé de ServiceAccount, utilisée par le kube-apiserver pour signer les tokens qu’il émet :
mkdir -p ./kas-sa
openssl genrsa -out ./kas-sa/sa.key 2048
Sans cette clé, le kube-apiserver ne peut pas fonctionner correctement, même en mode minimal.
Nous pouvons maintenant lancer les conteneurs etcd et kube-apiserver via un docker-compose.yaml :
networks:
net:
driver: bridge
volumes:
kas-certs: {}
services:
etcd:
image: gcr.io/etcd-development/etcd:v3.5.23
container_name: etcd-restore
command:
- /usr/local/bin/etcd
- --name=restored-etcd
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2479
- --advertise-client-urls=http://etcd:2479
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-cluster=restored-etcd=http://127.0.0.1:2380
- --initial-cluster-token=restore
volumes:
- /tmp/etcd-restore/:/etcd-data
ports:
- "2479:2479"
networks: [net]
restart: unless-stopped
apiserver:
image: registry.k8s.io/kube-apiserver:v1.30.8
container_name: kas-restore
depends_on:
etcd:
condition: service_healthy
command:
- kube-apiserver
- --etcd-servers=http://etcd:2479
- --secure-port=6444
- --bind-address=0.0.0.0
- --advertise-address=127.0.0.1
- --authorization-mode=AlwaysAllow
- --client-ca-file=/kas-pki/ca.crt
- --service-cluster-ip-range=10.200.0.0/16
- --cert-dir=/var/run/kas-certs
- --service-account-issuer=local
- --service-account-key-file=/kas-sa/sa.key
- --service-account-signing-key-file=/kas-sa/sa.key
ports:
- "6444:6444"
volumes:
- kas-certs:/var/run/kas-certs
- ./kas-sa/sa.key:/kas-sa/sa.key:ro
- ./kas-pki:/kas-pki:ro
networks: [net]
restart: unless-stopped
Lancement :
sudo docker compose up -d
Ensuite, nous générons un kubeconfig qui utilise notre certificat client :
KCFG=/tmp/kas-kubeconfig
kubectl config --kubeconfig="$KCFG" set-cluster kas \
--server=https://127.0.0.1:6444 --insecure-skip-tls-verify=true
kubectl config --kubeconfig="$KCFG" set-credentials charon \
--client-certificate=./kas-pki/user.crt --client-key=./kas-pki/user.key
kubectl config --kubeconfig="$KCFG" set-context kas --cluster=kas --user=charon
kubectl --kubeconfig="$KCFG" config use-context kas
Test :
kubectl --kubeconfig=/tmp/kas-kubeconfig get ns
Si cette commande fonctionne, c’est que le kube-apiserver peut bien interroger la base restaurée d’etcd.
À partir de là, nous pouvons analyser l’état du cluster tel qu’il était au moment de la sauvegarde :
kubectl --kubeconfig=/tmp/kas-kubeconfig get pods
Cette commande permet immédiatement d’identifier plusieurs pods ayant des noms peu conventionnels dans le namespace default.
Ces pods sont hautement suspects, car ils ne correspondent à aucun des manifestes présents dans le fichier censé rassembler l’ensemble des ressources du cluster.
Cette incohérence justifie de poursuivre l’investigation en profondeur afin de déterminer leur origine et leur rôle exact dans l’environnement compromis.
À ce stade le joueur à tous les éléments en main pour pouvoir répondre aux nombreuses questions concernant cette attaque.