Docker image injection
TL;DR
overlayfsでマウントされるディレクトリの内容は普通に上書きできるし,Dockerはビルド時に再確認とかしないのでちゃんと守ろう.
agenda
about
Dockerのイメージをビルドする際,主にDockerfileの行毎に操作がキャッシュされる.
高速化の為になるべく一行に操作をまとめる(例: apt-get install -y hoge fuga piyo foo bar baz qux quux foobar...
)など,普段からキャッシュを意識することは多いだろう.
しかし,そのキャッシュがどこでどのように管理され,使われているのか.という点については,ありがたい事に意識せずともDockerを使えている.
本記事では,そのキャッシュの保存先と使われ方,そしてキャッシュに対するAttack Vectorについて説明する.
検証環境は以下
Linux ubuntu-jammy 5.13.0-19-generic #19-Ubuntu SMP Thu Oct 7 21:58:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
Docker version 20.10.7, build 20.10.7-0ubuntu5
cache
aboutで説明したように,Dockerはイメージのビルド時にキャッシュを行っている.
変更された部分とそれ以降の操作のみが更新される.
今回は以下のような非常に簡単なコンテナを例に説明を行う.
Dockerfile
FROM nginx:1.21.4
COPY content.html /usr/share/nginx/html
content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a general web page.</h1>
</body>
</html>
静的なWebページをnginxを使って配信するようなもの.
これ自体はサンプルのような非常に単純なものだが,ここから学べることは多い.
実際にimageをbuildしてみる.
root@ubuntu-jammy:~/docker_test# docker image build -t example .
Sending build context to Docker daemon 3.072kB
Step 1/2 : FROM nginx:1.21.4
1.21.4: Pulling from library/nginx
7d63c13d9b9b: Pull complete
15641ef07d80: Pull complete
392f7fc44052: Pull complete
8765c7b04ad8: Pull complete
8ddffa52b5c7: Pull complete
353f1054328a: Pull complete
Digest: sha256:6ff52ff9299052a1454df88f6a46adefedac67dd7350cfaf510b9f1fdd1dafab
Status: Downloaded newer image for nginx:1.21.4
---> 04661cdce581
Step 2/2 : COPY content.html /usr/share/nginx/html
---> 2dde5de6508f
Successfully built 2dde5de6508f
Successfully tagged example:latest
一度目のビルドでは,特筆するような点も無く,レジストリからベースとなるイメージをpullし,Dockerfileに書かれている通り,content.html
をコピーしている(このコピー先が後々重要になる).
コンテナを実行してみると,想定通りの挙動をしているのがわかる.
root@ubuntu-jammy:~/docker_test# docker run -td --rm -p 80:80 example
53f148284a653b403d2480c4759f62edc7abf495b2166020143747ecfe06420e
root@ubuntu-jammy:~/docker_test# curl http://localhost:80/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a general web page.</h1>
</body>
</html>
さて,ここからが本題のcacheの話.
一度コンテナをkillし,content.html
を書き換えてみる.
こんな貧相なページが今時一般的を名乗るのはどうかと思うので,general
からexample
に書き換えてみる.
root@ubuntu-jammy:~/docker_test# sed -i 's/general/example/g' content.html
root@ubuntu-jammy:~/docker_test# cat content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a example web page.</h1>
</body>
</html>
root@ubuntu-jammy:~/docker_test# docker image build -t example .
Sending build context to Docker daemon 3.072kB
Step 1/2 : FROM nginx:1.21.4
---> 04661cdce581
Step 2/2 : COPY content.html /usr/share/nginx/html
---> 49cba37526ed
Successfully built 49cba37526ed
Successfully tagged example:latest
コンテナを実行し,接続してみると
root@ubuntu-jammy:~/docker_test# docker run -td --rm -p 80:80 example
6d5c67f97f0717d7d20f75c7eac5ccdb351d6dfdb77c88d02d3c4890ceffc6a7
root@ubuntu-jammy:~/docker_test# curl http://localhost:80/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a example web page.</h1>
</body>
</html>
しっかりとCOPY
対象となるファイルの更新も追従し,一致しない場合は更新されている事がわかる.
さて,これらのファイルがどこに保存されているのかを探してみる.
root@ubuntu-jammy:~/docker_test# find /var/lib/docker/ -name "content.html"
/var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/merged/usr/share/nginx/html/content.html
/var/lib/docker/overlay2/64663e6ea64ffa64d2cc4159ced8aef9aad1b1c7ff3101bcebe494aba500c13b/diff/usr/share/nginx/html/content.html
/var/lib/docker/overlay2/1d12bd16862c49663e06dd54d0760899751c4dc6751a5a1d5e0af1a8a0681504/diff/usr/share/nginx/html/content.html
わかりやすい名前をつけたので,これを元にfind
してみると,/var/lib/docker/overlay2
以下にあることがわかった.
mount
前章で見つけたoverlay2
にあるディレクトリがどうやって使われるかというと,名前の通りoverlayfs
によってコンテナにマウントされる.
DockerとOverlayFSについては,ドキュメントを読むとわかる.
overlayfsには
lowerdir
- 下層となるディレクトリ.ReadOnly
upperdir
- 上層ディレクトリ.書き込み可能
workdir
- ワーキングディレクトリ.空
merged
- overlayfsのマウント先となるディレクトリ
の4つのディレクトリがあり
これらを重ね合わせて一つのディレクトリのように振る舞う.
Dockerでの割り当ては,非常にわかりやすいドキュメントの図を借りる.
実行中のコンテナにマウントされているoverlayfsの情報は,
root@ubuntu-jammy:~/docker_test# mount |grep overlay
overlay on /var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/PY5YN23JCAOJGMIQCYRULF2UUG:/var/lib/docker/overlay2/l/SFTJ7ZNUPJWBAX5S7JQNSKQKSI:/var/lib/docker/overlay2/l/VH73CF5WOJCTM5NL2XCJQ4VBTL:/var/lib/docker/overlay2/l/EAP2AAJERWAXYJUUAQ4H6AK77Q:/var/lib/docker/overlay2/l/FODK5RVEJKMG2IXFYMX54AMGZD:/var/lib/docker/overlay2/l/YVOWKUSMURD23MIDARBBP224IE:/var/lib/docker/overlay2/l/JSSCHZGUXNN655DO4UUH656VW5:/var/lib/docker/overlay2/l/UQEORYLGWWOACPZRMIV6EIFDFU,upperdir=/var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/diff,workdir=/var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/work)
のように取得できる.
attack
マウントされるディレクトリがわかったので,その内容を見たり書き換えたりしたくなるのは好奇心旺盛なオタクとしては当然の事.
改めて,mountの情報をちょっとわかりやすくすると
overlay on /var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/merged
- lowerdir
/var/lib/docker/overlay2/l/PY5YN23JCAOJGMIQCYRULF2UUG
/var/lib/docker/overlay2/l/SFTJ7ZNUPJWBAX5S7JQNSKQKSI
/var/lib/docker/overlay2/l/VH73CF5WOJCTM5NL2XCJQ4VBTL
/var/lib/docker/overlay2/l/EAP2AAJERWAXYJUUAQ4H6AK77Q
/var/lib/docker/overlay2/l/FODK5RVEJKMG2IXFYMX54AMGZD
/var/lib/docker/overlay2/l/YVOWKUSMURD23MIDARBBP224IE
/var/lib/docker/overlay2/l/JSSCHZGUXNN655DO4UUH656VW5
/var/lib/docker/overlay2/l/UQEORYLGWWOACPZRMIV6EIFDFU
- upperdir
/var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/diff
- workdir
/var/lib/docker/overlay2/3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/work
といった感じで,lowerdirが多いのがわかる
lowerdirの情報を見てみると,それぞれ/var/lib/docker/overlay2/
以下のディレクトリへのリンクとなっている
root@ubuntu-jammy:~/docker_test# ls -l /var/lib/docker/overlay2/l
total 40
lrwxrwxrwx 1 root root 72 Nov 15 11:37 EAP2AAJERWAXYJUUAQ4H6AK77Q -> ../6632727f6a71360d32c979af4f307fbc98e0115ad1bd1d8f7776a0a8224a7812/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:37 FODK5RVEJKMG2IXFYMX54AMGZD -> ../1cef8729ff1d4c130aa199087771082adea6483da3639d46debba2fd2f5c24ae/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:37 JSSCHZGUXNN655DO4UUH656VW5 -> ../868df438860763112d80d2f3f7d00c340ec9fdb264db072baa15783f09b87d69/diff
lrwxrwxrwx 1 root root 72 Nov 25 14:08 L3ZCGP35TEETJO76AEONNB4SOL -> ../3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72/diff
lrwxrwxrwx 1 root root 77 Nov 25 14:08 PY5YN23JCAOJGMIQCYRULF2UUG -> ../3a1becce555355ab763134eacb1f50e716895c2f28141eea1a2826be43ee7a72-init/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:41 SFTJ7ZNUPJWBAX5S7JQNSKQKSI -> ../1d12bd16862c49663e06dd54d0760899751c4dc6751a5a1d5e0af1a8a0681504/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:37 UQEORYLGWWOACPZRMIV6EIFDFU -> ../7083a6ff9aa49604626eae2eea3b4749a028129ca86de974f1cb76123351f90a/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:37 V7AVUGDX3ZLGPBFLVJVZ2L45FZ -> ../64663e6ea64ffa64d2cc4159ced8aef9aad1b1c7ff3101bcebe494aba500c13b/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:37 VH73CF5WOJCTM5NL2XCJQ4VBTL -> ../979b5dc3733647e285e2f22436c11aba2b6a30eaec3449ce0946008b6f4ead68/diff
lrwxrwxrwx 1 root root 72 Nov 15 11:37 YVOWKUSMURD23MIDARBBP224IE -> ../ef66da561d73f85ce62231f2674b14f8bbe6794a8f167c24b4b451fcd3be113c/diff
content.html
がある64663e6ea64ffa64d2cc4159ced8aef9aad1b1c7ff3101bcebe494aba500c13b/diff
,1d12bd16862c49663e06dd54d0760899751c4dc6751a5a1d5e0af1a8a0681504/diff
も含まれているのがわかる.
それぞれ内容を見てみると,変更に対応しているのがわかる.
root@ubuntu-jammy:~/docker_test# cat /var/lib/docker/overlay2/64663e6ea64ffa64d2cc4159ced8aef9aad1b1c7ff3101bcebe494aba500c13b/diff/usr/share/nginx/html/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a general web page.</h1>
</body>
</html>
root@ubuntu-jammy:~/docker_test# cat /var/lib/docker/overlay2/1d12bd16862c49663e06dd54d0760899751c4dc6751a5a1d5e0af1a8a0681504/diff/usr/share/nginx/html/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a example web page.</h1>
</body>
</html>
おそらく差分毎にディレクトリが生えてきてキャッシュされているんじゃないかと推測できる.
楽しそうなのでこのキャッシュを書き換えてみる
仮説
DockerfileでCOPYしているファイルの変更は検知している上,そちらの方が優先順位は高そうなので,ベースとなるキャッシュとの違いがあればビルド用の内容で上書きされるのでは?
実証
root@ubuntu-jammy:~/docker_test# sed -i 's/example/hacked/g' /var/lib/docker/overlay2/1d12bd16862c49663e06dd54d0760899751c4dc6751a5a1d5e0af1a8a0681504/diff/usr/share/nginx/html/content.html
root@ubuntu-jammy:~/docker_test# cat /var/lib/docker/overlay2/1d12bd16862c49663e06dd54d0760899751c4dc6751a5a1d5e0af1a8a0681504/diff/usr/share/nginx/html/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a hacked web page.</h1>
</body>
</html>
root@ubuntu-jammy:~/docker_test# docker run -td --rm -p 80:80 example
8d064de9a8d4a64eccea80eb32714bacddb82914d78f2fd4ca60568f960ace82
root@ubuntu-jammy:~/docker_test# curl http://localhost:80/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a hacked web page.</h1>
</body>
</html>
ここまでは想定通り.
マウントされるディレクトリを書き換えているので,当然コンテナ内のファイルも書き換わっている.
改めてimageをbuildしてみる.ここでは上書きされてほしい.
root@ubuntu-jammy:~/docker_test# docker image build -t example .
Sending build context to Docker daemon 3.072kB
Step 1/2 : FROM nginx:1.21.4
---> 04661cdce581
Step 2/2 : COPY content.html /usr/share/nginx/html
---> Using cache
---> 49cba37526ed
Successfully built 49cba37526ed
Successfully tagged example:latest
root@ubuntu-jammy:~/docker_test# docker image build -t example .
Sending build context to Docker daemon 3.072kB
Step 1/2 : FROM nginx:1.21.4
---> 04661cdce581
Step 2/2 : COPY content.html /usr/share/nginx/html
---> Using cache
---> 49cba37526ed
Successfully built 49cba37526ed
Successfully tagged example:latest
root@ubuntu-jammy:~/docker_test# docker run -td --rm -p 80:80 example
08a2134b097becbb39bf5e72dde6ab378bbf62b13f482c10cb471868143190f7
root@ubuntu-jammy:~/docker_test# curl http://localhost:80/content.html
<!DOCTYPE HTML>
<html>
<body>
<h1>I'm a hacked web page.</h1>
</body>
</html>
Oops!
Using cache
の文言で怪しさはあったが,残念ながらマウントされるディレクトリの変更は検知できていないようだ.
まだコードを読んでないので想像にすぎないが,docker image build
で検証されるcacheは,image buildのコンテキストだけで,実際に/var/lib/docker/overlay2
へ変更を加えるのは検証後なのでは?
影響
攻撃への転用はまだ確認されていないらしい(security@docker.com
に連絡したりした).
そもそもroot
が必要なので割と軽視されがちだが,Post Exploitの永続化等,攻撃に転用される可能性は0では無い.
緩和策
とりあえずimageのbuild時にcacheを使用しないようにする(--no-cache
)とか,/var/lib/docker/overlay2/
を監視(ex: fantom)するとか.
この記事はIPFactory Advent Calendar 2021の12/01分です.
IPFactoryというサークルについてはこちらをご覧ください.
明日はn01e0による,なにかです.
Comments