
Kubernetes CVE-2020-8558 に関する情報、概念実証エクスプロイトを含む。
CVE-2020-8558 は、kube-proxy が localhost にバインドされたホストサービスを 予期せず ネットワーク上の他のノードに公開してしまうために公開された Kubernetes の脆弱性です。私は「予期せず」を強調しています。なぜなら、この脆弱性は実装上の欠陥(バグ)ではなく、設計上の欠陥(見落とし)に起因するからです。コードはその記述どおりに正確に動作しますが、私たちは皆、その決定がもたらすセキュリティ上の影響を認識できていませんでした。
ホストプロセスが 127.0.0.1(localhost)アドレス経由で NodePort サービスにアクセスできるようにするため、kube-proxy は net.ipv4.conf.all.route_localnet=1 という sysctl 設定を行います。カーネルのドキュメントによると、この設定によりカーネルは「ループバックアドレスを火星人(martian)と見なさなく」なります。その結果、これらのアドレスはネットワーク上の他のノードからアクセスされる可能性があります。localhost にバインドされていることだけが唯一の保護である、認証なしの機密サービスを運用している場合、これは重大な問題です。
この記事を書いている時点では、Kubernetes コミュニティは CVE-2020-8558 への最善の対処方法をまだ模索中です。明らかな選択肢は 2 つあります。1 つはそもそも route_localnet sysctl を設定しないこと、もう 1 つは iptables を使用して不適切にルーティングされた localnet パケットをブロックすることです。後者の戦略を用いた修正は、kubelet >= 1.18.4、1.17.7、または 1.16.11 ですでにリリースされています。この CVE に関する Kubernetes の Issue(下にリンク)を参照して、ご自身で適用することもできます。
なぜ net.ipv4.conf.all.route_localnet=1 の設定が CVE ID に値するのでしょうか? 主な理由は、これが IP ネットワークに関する私たちの直感に反するからです。
少なくとも 1989 年の RFC 1122 以来、localhost ネットワーク 127.0.0.1/8 からのパケットは特別に扱われ、「ホストの外部」に現れることが禁止されてきました。(127/8 の特別な性質に関するより古い言及をご存知の方は、Twitter でご連絡ください。)RFC 準拠のホストは本質的に、127.0.0.1(およびそのネットワーク内の他の IP -- まだやったことがなければ 127.127.127.127 への ping を試してみてください!)にバインドされたサービスへの外部アクセスをブロックする、暗黙的で削除できないファイアウォールルールを持っています。私たちはその動作に依存し、それを期待するようになっています。私たちは機密サービスを認証や暗号化なしで実行し、安全のために localhost にバインドすることがよくあります。たとえば、平文 HTTP バックエンド、redis キーバリューストア、そして残存する Kubernetes api-server の insecure ポートは、通常すべてこの方法で侵入から保護されています。私たちはこの動作に慣れすぎており、それが IP ホストであることの意味についての直感的な感覚に深く組み込まれています。このように考えると、多くの専門家がこの欠陥を長い間見落としていたのも理解できます。
これはどのように機能するのでしょうか?
IP アドレスを持つ任意のエンティティを「ノード」と呼びましょう。IP パケットは、パケットヘッダー内の送信元 IP アドレスと宛先 IP アドレスによって識別され、あるノードから別のノードへ送信されます。すべての IP ノードは、ルーター(RFC1122 ではゲートウェイと呼ばれる)またはホストのいずれかです。主な違いは、ホストが他の誰かのアドレス宛てのパケットを受信した場合、それを無視することです。ルーターはルーティングテーブルを参照し、パケットを最終的な宛先に近づけるために再送信(転送)します。ホストはローカルに接続された一部のノードを知っています。他のノードにアクセスするには、ローカルに接続されたルーターにパケットを送信する必要があります。これらのローカル接続は、ポイントツーポイント(PPP リンクや一部の仮想ネットワークなど)または共有メディア(イーサネットなど)の場合があります。
あなたの郵便受けは、自宅と地元の郵便局の間のポイントツーポイントリンクとして概念化できます。ポイントツーポイントリンク経由でパケットをルーティングするには、ホストは正しい宛先アドレスを適用してパケットを送信するだけで済みます。(これは OSI モデルのレイヤー 3 で行われます。)共有メディアリンク経由でパケットをルーティングするには、ホストはまず共有メディア全体に仮想ポイントツーポイント回線を構築する必要があります。イーサネット/IP ネットワークでは、これは OSI モデルのレイヤー 2 で ARP によって行われます。基本的に、ARP パケットを送信できれば、別のホストに「ねえ、私はここにいるよ」と伝えることができ、そのホストはあなたを信じるでしょう。(これが不適切に行われると、ARP キャッシュポイズニングと呼ばれます。)その後、パケットに適切なイーサネットの送信元アドレスと宛先アドレスを設定することで通信できます。
通常のノードは、RFC 1122 のため、宛先アドレスが 127.0.0.1 のパケットを送信することは決してありません。通常のノードが宛先アドレス 127.0.0.1 のパケットを受信した場合も、やはり RFC 1122 のため、それを無視(ドロップ)します。net.ipv4.conf.all.route_localnet=1 を設定するとそれが変わります。127.0.0.1 のパケットが特別なものではないかのように送受信できるようになります。
したがって、攻撃者が net.ipv4.conf.all.route_localnet=1 が設定されたターゲットノードへのローカル接続を持っている場合、攻撃者は宛先アドレスを 127.0.0.1 としたパケットを送信でき、ターゲットノードは 127.0.0.1 がまったく普通のアドレスであるかのように適切に応答します。現在、ターゲットノードへのローカル接続を持つ最も一般的な 2 つの方法は、ターゲットと同じイーサネットネットワーク(ブロードキャストドメイン)上にいることか、ターゲット上で動作するコンテナであることです。
通常の設定では、Linux は攻撃者ノードが 127.0.0.1 宛ての通常のパケットを送信することを許可しないことに注意してください。これは、攻撃者の Linux ノードを再設定するか(root アクセスがある場合)、raw ソケットを使用してパケットを偽造することで回避できます。raw ソケットには Linux カーネルのケーパビリティ CAP_NET_RAW だけが必要で、これはデフォルトで非特権コンテナに付与されます。つまり、攻撃者が制御する非特権コンテナは CVE-2020-8558 を悪用できる可能性があります。
要するに、kube-proxy を使用しているか、net.ipv4.conf.*.route_localnet を使って巧妙なことをしている場合、あなたは危険にさらされています。その露出が自分にとってどれほどリスクがあるかを判断し、適切な緩和戦略を計画するために、時間をかけて脅威モデリングを行うべきです。
基本的に、net.ipv4.conf.all.route_localnet=1 が設定されているすべての Linux ホストは脆弱です。その脆弱性が攻撃者にとって魅力的であるかどうかは、いくつかの要因に依存します:
CVE-2020-8558 を評価するには、さまざまな能力を持つ攻撃者を想定し、それらの攻撃者の視点からこれらの質問に答える必要があります。(Adam Shostack の著書『Threat Modeling: Designing for Security』はこのプロセスを非常に詳細に説明しています。)必ず考慮すべき 2 つの関連する攻撃者は、あなたのイーサネットネットワーク上にノードを持つ攻撃者と、あなたのホスト上の非特権ポッドでコードを実行できる攻撃者です。環境やニーズに応じて、検討すべき他の興味深い攻撃者もいるかもしれません。
説明のために、途中まで進めた例を示します:
ホストは確かに両方の攻撃者からアクセス可能です。それぞれの場合でそれを前提としています。
パケットがフィルタリングされるかどうかは場合によります。あなた自身で確認する必要があります。多くのクラウド環境や厳格に管理されたオンプレミスネットワークでは、IP 宛先がネットワークによって期待されるイーサネット宛先と一致しない場合、パケットはブロックされます。これだけで、ノードを持つ攻撃者にとってはゲームオーバーになる可能性があります。すべてのノードに適切なローカルファイアウォールルール(更新された kubelet によって提供されるものなど)がある場合、両方の攻撃者は失敗するでしょう。
おそらく、あなたが考えている以上に魅力的なサービスがあります。明らかに、Kubernetes api-server の insecure ポートは非常に魅力的なターゲットであり、可能であれば無効にするべきです。127.0.0.0/8 ネットワーク内の IP アドレスにバインドしているすべてのプロセスを調査してください。それらは堅牢な認証を備えていますか? そうでない場合、それらは CVE-2020-8558 を介して露出する可能性があります。通常の localhost サービスがすべて安全であっても、一時的なサービスも懸念事項になり得ます。たとえば、SSH ポートフォワーディングは、一時的で承認された目的のためにネットワーク制限を回避するためによく使用されます。デフォルトでは、SSH フォワーディングされたポートは localhost にバインドされるため、一時的なアクセスは承認されたユーザーのみに許可されます。CVE-2020-8558 では、それらの「安全な」ポートフォワードも攻撃者が利用できます。
ターゲットと同じブロードキャストドメイン上の Linux マシンで root 権限を持っている場合、以下の設定で CVE-2020-8558 を悪用できるようになります:
ip addr add 127.0.0.2/8 dev lo
ip addr del 127.0.0.1/8 dev lo
ip route add 127.0.0.1/32 via YOUR-TARGET-HERE
sysctl net.ipv4.conf.all.route_localnet=1
いくつかの重要なサービス(ゴホッ、ゴホッ、systemd-resolved)が 127.0.0.0/8 アドレスで実行されているため、ホストを壊さないように新しいアドレスを追加します。次に、ホストにデフォルトの 127.0.0.1/8 アドレスを忘れさせます。次に、カーネルに 127.0.0.1 へのトラフィックをネットワーク経由でターゲットにルーティングするよう指示します。ターゲットは 127.0.0.1 へのアクセス方法を知っています。最後に、この構成が機能するのを妨げるであろう悪名高い sysctl を設定します。
raw パケットを送信して CVE-2020-8558 をテストするためのシンプルな Python スクリプトです。scapy のワンライナーでも可能ですが、もう少しだけ手軽さを加えたかったのです。ターゲット経由で 127.0.0.1 にパケットを送信し、応答があるかどうかを確認します。
偽造されたパケットを介して、通常の TCP または UDP クライアントアプリケーションがリモートの localhost IP と通信できるようにすることで、CVE-2020-8558 を悪用する Python スクリプトです。このスクリプトを実行し、通常の TCP または UDP クライアント(例: kubectl や nc)を使用して、fakedestination(デフォルトでは 198.51.100.1)に接続します。
fakedestination はパケットに決して応答しない IP アドレスである必要があり、そこへのルートはターゲットにアクセスするのと同じインターフェース経由である必要があることに注意してください。通常の場合、fakedestination とターゲットの両方がデフォルトゲートウェイインターフェース経由でアクセス可能であり、これは大きな問題にはなりません。
このスクリプトは raw ソケットを使用して「localhost」パケットを送受信するため、通常の非特権コンテナ内でも問題なく動作します。
GitHub 上のこの CVE に関する Kubernetes の Issue
Ian Coldwater、Brad Geesaman、Duffie Cooley、Laurent Bernaille に感謝を捧げます。皆さん、意見やアドバイス、そして笑いをありがとう。Honk the planet!