
Детальный технический анализ и рабочий эксплойт для 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