Docker、これまでで最も深刻な cp コマンドの脆弱性CVE-2019-14271を修正

This post is also available in: English (英語)

概要

ここ数年、Docker、Podman、Kubernetesを含むさまざまなコンテナプラットフォームで、copyコマンド(cp)の脆弱性が複数確認されてきました。それらの中で最も深刻なものはこの7月というごく最近発見・開示されたものです。驚くべきことに、CVEの説明内容があいまいだったこと、公開されたエクスプロイトがなかったことなどの理由から、本脆弱性は公開直後にはほとんど注意を引きませんでした。

しかしながら、CVE-2019-14271が攻撃者に悪用された場合、Dockerの実装するcpコマンドは、完全なコンテナブレイクアウトにつながりうるセキュリティ上の問題を引き起こします。この脆弱性は、2月に発見されたrunC脆弱性以降で初めての完全なコンテナブレイクアウトです。

この脆弱性が悪用されるにあたっての前提条件となるのが、対象のコンテナがすでに別の攻撃(他の脆弱性や漏えいした秘密など)によって侵害済みであること、あるいは、ユーザーが信頼できないソース(レジストリそのほか)からの悪意のあるコンテナイメージを実行していることです。これらの条件下でユーザーが脆弱なcpコマンドを実行して侵害済みコンテナからファイルをコピーするコマンドを実行した場合、攻撃者はコンテナとホストの分離制限を回避し、当該コンテナを実行するホストと同ホスト内の他のすべてのコンテナの完全なroot制御を取得することができます。

CVE-2019-14271は「Critical (深刻)」と評価されており、Dockerバージョン19.03.1で修正されています。本稿ではCVE-2019-14271の概要と本脆弱性については初となる概念実証(PoC)について考察します。

ここのところ、筆者とAriel Zelivanskyは、主要なコンテナプラットフォームにおけるcopyコマンドの脆弱性急増をたんねんに追跡調査してきました。私たちはこの調査結果を2019年11月20日にサンディエゴで開催されるKubeCon + CloudNativeCon 2019で発表する予定です (訳注: 米国時間)。私たちは、過去の脆弱性、さまざまな実装、そしてこの比較的単純なコマンドの実装がなぜ驚くほど難しいのか、その根本的な理由について調査をしてきました。本調査に取り組むためにビルドしたクールな新しいカーネル機能についても説明していますので、コンテナセキュリティに関心があるかたはぜひチェックしてみてください。

docker cpコマンド

コピーコマンドを使うと、ホスト-コンテナ間、またはコンテナ-コンテナ間でファイルをコピーすることができます。その構文は標準的なUnixのcpコマンドとよく似ています。たとえば、/var/logsをコンテナから抜き出してコピーするなら、その構文は

docker cp container_name:/var/logs /some/host/path

となります。

下の図からもわかるとおり、コンテナから抜き出したファイルをコピーするさい、Dockerはdocker-tarと呼ばれるヘルパープロセスを使います。

図1 ファイルをコンテナからコピー
図1 ファイルをコンテナからコピーしているところ。psコマンドで確認するとdocker-tarがヘルパープロセスとして使われている

docker-tarは、対象コンテナにchrootする(下図参照)、要求されたファイルとディレクトリをアーカイブする、アーカイブされたtarファイルをDockerデーモンに返してホスト上の対象ディレクトリに展開してもらう、という流れで動作します。

図2 docker-tarでコンテナにchrootしたところ
図2 docker-tarでコンテナにchrootしたところ

chrootを行う主な理由は、ホストプロセスがコンテナ上のファイルにアクセスしようとしたときに発生しうるsymlink(シンボリックリンク)の問題を回避することにあります。symlinkの問題とは、アクセスしたファイルのなかにsymlinkが含まれていると、それがホスト側のルートに誤って解決されてしまう可能性がある、という問題です。攻撃者の制御下にあるコンテナの場合、これがdocker cpを騙してコンテナではなくホスト側のファイル読み書きをさせる突破口となってしまいます。去年もこのsymlink関連の問題で複数のCVEがDockerPodmanに割り当てられていました。コンテナのルートにchrootすることにより、本来ならdocker-tarはすべてのsymlinkがうまくコンテナのルートの下に解決されることを保証してくれるはずです。

残念ながらこれが、コンテナからファイルをコピーするさいのさらに深刻な問題発生につながりました。

CVE-2019-14271

DockerはGo言語で書かれています。具体的にいうと、脆弱なバージョンのDockerはGoのバージョン1.11でコンパイルされています。このGoのバージョンには、埋め込みのCコード(cgo)を含み、実行時に共有ライブラリを動的にロードするパッケージがあります。こうしたパッケージには、docker-tarが利用するnetやos/userも含まれており、両パッケージとも実行時にlibnss_*.soライブラリを動的にロードしています。通常、ライブラリはホスト側のファイルシステムからロードされますが、docker-tarの場合、コンテナにchrootすることから、コンテナ側のファイルシステムからライブラリをロードするようになっています。つまり、docker-tarは「コンテナ側で生成・制御したコードをロードして実行する」ということです。

分かりやすくいえば、コンテナ側のファイルシステムにchrootされるという点をのぞき、docker-tarはコンテナ化されていないのです。ですからdocker-tarは、ホスト側の名前空間で実行され、すべてのルート機能を備え、cgroupsやseccompからの制約を受けません。したがって、コードをdocker-tarに挿入しさえすれば、悪意のあるコンテナがホストへの完全なルートアクセス権限を取得できることになります。

ここで考えられる攻撃シナリオは、次のいずれかの環境でファイルをコピーするDockerユーザーでしょう。

  • 悪意のあるイメージを実行し、悪意のあるlibnss_*.soライブラリを含んでいるコンテナ
  • すでに侵害を受けて攻撃者がlibnss_*.soライブラリを差し替えてあるコンテナ

どちらの場合でも、攻撃者はホストのルートコード実行権限を取得できます。

こぼれ話: この脆弱性は実はGitHubの不具合報告で見つかったものです。あるユーザーが「debian:buster-slimコンテナからファイルをコピーしようとしたがdocker cpが繰り返し失敗する」という問題を報告しました。原因はこの特定のイメージがlibnssライブラリを含んでいないことにありました。このため同ユーザーがdocker cpやdocker-tarプロセスを実行しても、コンテナファイル側のシステムからlibnssをロードしようとしては失敗し、クラッシュしていたのです。

エクスプロイト

CVE-2019-14271を悪用するには、悪意のあるlibnssライブラリをビルドしなければなりません。本調査では、適当にlibnss_files.soを選んでビルドしました。同ライブラリのソースファイルをダウンロードし、このなかの1つにrun_at_link()という関数を1つ追加します。そのさい、この関数にconstructor属性をもたせて定義します。constructor属性(GCC独自の構文)を指定すると、プロセスがライブラリをロードするさい、私の作ったrun_at_link関数がその初期化関数として実行されるようになります。これにより、docker-tarプロセスが悪意のあるライブラリを動的にロードすると、run_at_link関数が実行されます。以下がそのrun_at_link関数のコードです(簡潔にするために短くしてあります)。

run_at_link関数はまず、自身がdocker-tarのコンテキストで実行されていることを確認します。ライブラリは別の通常のコンテナプロセスからもロードされる可能性があるからです。この確認には、/procディレクトリのチェックを使っています。run_at_link関数がdocker-tarのコンテキストで実行されているなら、/procディレクトリは空になるはずだからです。/procのprocfsマウントは、コンテナのマウント名前空間内にのみ存在します。

次にrun_at_link関数は、libnssの元ライブラリを悪意のあるものと差し替えます。これにより、エクスプロイトで後から実行されるプロセスが誤って悪意のあるバージョンをロードしなおして、run_at_link関数を繰り返し実行することを防いでいます。

それから、エクスプロイトを容易にするために、run_at_linkはコンテナの/breakoutというパスで実行可能ファイルの実行を試みます。これにより、エクスプロイトの残りの部分を、Cではなくbashを使って記述できるようになりますし、残りのコードをrun_at_linkから外に出すことで、エクスプロイトの変更のつど、悪意のあるライブラリを再コンパイルする必要もなくなり、ただ/breakoutのバイナリを変更するだけですみます。

以下のエクスプロイト実演ビデオでは、Dockerユーザーが、改変済みlibnss_files.soライブラリを含む悪意のあるイメージを実行し、コンテナからログをいくつかコピーしようとしています。イメージ内の/breakoutバイナリは、ホスト側のファイルシステムをコンテナの/host_fsにマウントし、ホスト上の/evilにメッセージ("Hello from within the container!")を書き込む、という単純なbashスクリプトです。

ビデオ1 CVE-2019-14271をエクスプロイトしたDockerからのブレイクアウト

以下は、この実演ビデオで使用されている/breakoutスクリプトのソースです。ホストのルートファイルシステムへのリファレンスを取得するため、このスクリプトは/procにprocfsをマウントしています。docker-tarはホスト側のPID名前空間で実行されていますので、マウントされたprocfsにはホストプロセスのデータが含まれています。そこで、このスクリプトでは単にホスト側のPID 1のルートをマウントしています。

修正

修正では、問題のあるGoパッケージから任意の関数を呼び出すさいのdocker-tarのinit関数が修正されています。これにより、libnssコンテナにchrootする前のライブラリ、したがってホスト側のファイルシステム上のライブラリをdocker-tarに強制的にロードさせます。

図3 CVE-2019-14271の修正
図3 CVE-2019-14271の修正

結論

ホストでルートコードを実行できる脆弱性は非常に危険です。実行中のDockerのバージョンが当該セキュリティ問題の修正を含む新しいバージョン、19.03.1またはそれ以上であることを確認してください。この種の攻撃による攻撃対象領域を減らすには、信頼できないイメージを実行しないことを強くお勧めします。

このほか、どうしてもrootが必要でないかぎり、コンテナは非rootユーザーとして実行することも強くお勧めします。セキュリティが向上し、コンテナエンジンやカーネルで今後も見つかるはずの不具合の多くが、攻撃者から悪用されにくくなります。このCVE-2019-14271についても、コンテナが非rootユーザーで実行されていればユーザーが保護されますし、たとえ攻撃者がコンテナを侵害済みでもコンテナのlibnssライブラリはルートが所有しているので脆弱性を悪用できません。それでも納得がいかないなら、Ariel Zelivanskyによるこちらの投稿をご一読ください。非rootでコンテナを実行することのセキュリティ上の利点に得心がいき気が変わるかもしれません。

Prisma Cloudをご利用中のパロアルトネットワークスのお客様は、次の機能でさらに同脆弱性の脅威から保護されています。

  • Trusted Imagesにより、開発者は確実に検証済み・承認済みのイメージ用ソースを利用できるようになります。
  • ご利用環境内で実行されている脆弱なパッケージを含むコンテナについて、Host Vulnerability Scanningアラート(ホストの脆弱性スキャンアラート)により、悪用される可能性が最も高いCVEが強調表示されます。これにより、コンテナは脆弱なコードを実行することがなくなり、修正パッチ公開初日(ワンデイ)に発生する攻撃を防ぎます。
  • Prisma Cloud Runtime Securityは、悪意のある攻撃者によるコンテナへのアクセスや侵害を特定してブロックします。