Le Timonier Fantôme d'EXE

Bilal-Rayane M., Nolan D., Louca G., Rayan C., Gabin R., Robin V.

← Le Timonier Fantôme d'EXE · Arrête-moi si tu peux / Import-ant Binaire suspect / Souvenirs Suspects / Orchestration Diabolique

Orchestration Diabolique

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.

Cote 95 pts

Téléchargements

Attention : les contenus téléchargeables peuvent contenir du contenu malveillant.
kubelet.log Taille : 358,53 kio b2sum : 7329b7ec6c99eac1ae55bd603875ecfa156fc660cf6ba3c9c7988744d8ebc323b1d1a622002c66ec7daa460159fe11a99061cc070b27c8c00da01d7ad250b601
containers.tar.gz Taille : 2011,83 kio b2sum : 143c6c06ea554c902e551366826dd9863e4633c46434a51be8893a9c0c5d76111644dc18a3d80235c6886e3a4e6f3b0aff8088d8cd07a6bd921350b29a70b197
k8s-node-2025-11-29-164146.pcap.gz Taille : 90097,19 kio b2sum : aad28329473baf32a3bf4da01adea97478237391210512f36ae0a5fe2d4456bdd1804a856f23561653d0bb6e177ed1d684fe20c2a7db97983c50c3f3cc171ed9
audit.tar.gz Taille : 130447,62 kio b2sum : 726800a8a1be2d226ac36944fb64290a50f3013ac1797eada0b22ecf1a051ebb637a57ff8ebd63cc40e60a1c6aef7f91be0f4b6f22358508313e17fc6efa9dbf
output.yaml Taille : 1071,57 kio b2sum : b1d4150a5278e38bb30ce5db0a6d09980602b76ac6afb454f023a76b93d7cabcc317eb25cefecdd00bb66a03c2e4e19db199cf24fb8bcfa98e419285197b6bd6
etcd-2025-11-29-170816.snap Taille : 32412,03 kio b2sum : 14c8a01253893652816e3499f2b43ef12dbb93800d173399d45fdcbda1527843542cf2248b67491107ad3edc4c353db33a390c2f9fa1d7ba719da7c75b6810c6
output-all.yaml Taille : 11561,86 kio b2sum : 31a8d1db4536d9930d54f4d0920276ec8ebb128208fa91679d2f774c38dbb527fbd5cb0f75c965bd6dbe5743f797c8761e3f18ff0498636f513a1b302d14533c
cni.log Taille : 949,10 kio b2sum : efe8dc623d315f3192ad275094f753f0a9c2380ef0150e251f455af40d149e03f500c557c4b0ad701c3a81bb12aff165ac6a00be32d08bf4a488e9a9fd1991e1

Indices

Control can sometimes be an illusion... But sometimes you need illusion to gain control
Control can sometimes be an illusion...

Faire son rapport

Résolution

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.

Étape 1

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.

Étape 2

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.