Перейти к содержимому
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Как создать изолированную сеть Docker для лаборатории

Создайте сеть Docker без выхода в интернет, запустите в ней OWASP Juice Shop и опубликуйте копию для браузера только на 127.0.0.1. Проверка на каждом шаге.

Начальный11 мин чтенияАвтор: NotHoly

Лаборатория безопасна настолько, насколько безопасны пути в неё и из неё. Если уязвимое приложение может выйти в интернет, оно способно обратиться туда, куда вы вовсе не собирались. Если оно слушает все интерфейсы вашей машины, до него дотянется любой в той же сети Wi-Fi. В Docker оба пути контролируются несколькими командами. В этом руководстве мишенью будет OWASP Juice Shop, и мы пройдём каждую команду по порядку.

Вы создадите сеть без выхода наружу, запустите в ней Juice Shop, обратитесь к приложению из второго контейнера и докажете, что сеть не достаёт до интернета. Затем запустите отдельную копию для браузера, опубликованную только на loopback-интерфейсе, чтобы её не было видно из локальной сети. Изоляция — первая мера защиты любой лаборатории, и от неё зависят все последующие упражнения, от сканирования до отработки эксплуатации.

Если вы ещё не запускали Juice Shop, начните со статьи OWASP Juice Shop в Docker. Здесь предполагается знание основ работы с контейнерами из того руководства.

Почему сначала изоляция

В лаборатории безопасности работает софт, созданный для того, чтобы его ломали. В Juice Shop намеренно оставлены уязвимости, в этом его смысл. Риск исходит от другого: уязвимость, ошибка конфигурации или опечатка может открыть приложению или инструменту, который вы против него запускаете, дорогу куда-то ещё.

Вот три случая, когда что-то идёт не так:

  • В приложении есть функция загрузки по URL, и тестируемый payload заставляет его обратиться к хосту, которого вы не планировали. Без контроля исходящего трафика запрос покидает вашу машину.
  • Контейнер с инструментом выполняет команду по адресу с опечаткой. Без сетевой границы этот адрес может принадлежать вашему роутеру, работодателю или серверу постороннего человека.
  • Вы публикуете приложение простым -p 3000:3000. Тогда Docker принимает соединения на всех интерфейсах хоста, и другие устройства в вашей сети могут достучаться до уязвимого приложения.

Изоляция закрывает каждый из этих случаев. У внутренней сети нет маршрута наружу. У порта, привязанного к loopback, нет пути через LAN. Выделенная сеть отделяет контейнеры лаборатории от других ваших проектов. Ни один из этих шагов не сложен, но в спешке о них легко забыть, поэтому в руководстве вы проверяете каждый.

Сети Docker за пять минут

Чтобы команды обрели смысл, нужно понимать три вещи.

Сети bridge

Docker подключает контейнеры к виртуальным сетям. Драйвер по умолчанию — bridge. Каждый контейнер в сети bridge получает собственный IP-адрес в частной подсети. В Linux мост находится на самом хосте. В Windows и macOS Docker Desktop запускает движок внутри виртуальной машины Linux, поэтому мост находится внутри этой ВМ.

Контейнеры в стандартном bridge видят друг друга по IP-адресу, но не по имени. Сеть bridge, созданная вами через docker network create, разрешает имена контейнеров по DNS с помощью встроенного в Docker резолвера. Поэтому контейнер с инструментом в этом руководстве может обращаться к http://juice:3000 и не зависит от IP, который меняется после перезапуска.

Внутренние сети

Когда вы создаёте сеть с флагом --internal, Docker ограничивает к ней внешний доступ. Документация Docker описывает это так: контейнеры во внутренней сети могут общаться друг с другом, но ни с чем снаружи. В том числе с интернетом. Вы увидите это на шаге 4, когда не удастся разрешить имя внешнего хоста.

Опубликованные порты

По умолчанию порт контейнера не публикуется для других машин. Это меняет опция -p или --publish. Она связывает адрес и порт хоста с портом контейнера, а выбранный адрес определяет, какие интерфейсы принимают соединения.

-p 3000:3000 слушает все интерфейсы хоста, включая ваш адрес в LAN. -p 127.0.0.1:3000:3000 слушает только loopback, поэтому подключиться могут только программы на этой же машине. Именно такая форма приведена в документации Docker о публикации портов. Разница всего в одном префиксе, но от него зависит, кто сможет подключиться.

Что вы построите

В лаборатории пять частей:

  • juicenet, внутренняя сеть без маршрута наружу.
  • juice, Juice Shop, подключённый только к juicenet, без опубликованного порта.
  • Одноразовый контейнер curlimages/curl в juicenet, который обращается к juice по имени.
  • Тот же одноразовый контейнер, который пытается достучаться до example.com, и это должно закончиться неудачей.
  • juice-local, второй экземпляр Juice Shop, опубликованный на 127.0.0.1:3000 для браузера.

Изолируете вы контейнер juice. juice-local — копия для удобного просмотра. Она работает в сети по умолчанию, поэтому может выйти в интернет. Помните об этом и не вводите в приложение настоящие учётные данные или личную информацию.

Перед началом

Вам понадобятся:

  • Установленный Docker с запущенным движком. docker info должен вывести данные о сервере без ошибок. На машине, где готовилось это руководство, Docker Engine 29.8.2 работал на Windows 11 через Docker Desktop.
  • Свободный порт 3000 на хосте для копии в браузере.
  • Командная оболочка. В примерах используется Git Bash на Windows 11. В оболочках macOS и Linux команды работают так же. В PowerShell строкам формата -w в одинарных кавычках нужно другое экранирование.

Проверьте имена, прежде чем их выбирать. Другие проекты на вашей машине могут уже использовать имена вроде juice или juicenet. Сначала выполните:

bash
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'

Если в одном из списков есть juice, juice-local или juicenet, выберите другие имена или сначала удалите старые объекты. Не удаляйте то, что создали не вы.

Проверьте и образ, который собираетесь запустить. Juice Shop публикует метки с версией в своём образе:

bash
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'

Эта команда завершится ошибкой, пока образа нет локально, потому что скачивает его первый docker run. На машине, где готовилось это руководство, образ сообщил версию 20.3.0. У вас версия может быть другой, так что запишите её.

Шаг 1: создайте внутреннюю сеть

bash
docker network create --internal juicenet

Docker выводит ID новой сети. У вас он будет другим. Вот что он вывел на тестовой машине:

text
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9

Убедитесь, что флаг сработал:

bash
docker network inspect juicenet --format '{{.Internal}}'
text
true

Если вывод false, вы создали сеть без --internal. Удалите её командой docker network rm juicenet и создайте заново. Не продолжайте, пока не увидите true: все дальнейшие проверки исходят из того, что флаг включён.

Шаг 2: запустите Juice Shop во внутренней сети

bash
docker run -d --name juice --network juicenet bkimminich/juice-shop

У каждой опции своя задача. -d запускает контейнер в фоне. --name juice даёт ему DNS-имя, которое могут разрешить другие контейнеры в juicenet. --network juicenet подключает его только к этой внутренней сети. Опции -p здесь нет, поэтому контейнер не публикует порт на хост.

Docker выводит ID контейнера:

text
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8

Приложение внутри слушает порт 3000. После запуска контейнера Juice Shop нужно время, чтобы подняться, поэтому не проверяйте сразу. Используйте цикл, который смотрит, получает ли контейнер с инструментом HTTP 200:

bash
for i in $(seq 1 30); do
  s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
  echo "try $i: $s"
  [ "$s" = "200" ] && break
  sleep 3
done

Пока приложение запускается, цикл выводит 000: curl не смог подключиться. На тестовой машине он напечатал 000 четыре раза, а затем 200 с пятой попытки. У вас попыток может быть больше или меньше. Цикл сдаётся после 30 попыток. Если 200 так и не появился, посмотрите ошибки в docker logs juice.

Шаг 3: обратитесь к приложению из контейнера с инструментом

bash
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000
text
200

Разберём каждую часть. --rm удаляет контейнер с инструментом после завершения. --network juicenet помещает инструмент в ту же внутреннюю сеть, что и приложение. curlimages/curl — маленький образ с curl. http://juice:3000 использует имя контейнера, которое Docker разрешает внутри juicenet. Флаг -s скрывает индикатор прогресса, -o /dev/null отбрасывает тело ответа, а -w выводит только код статуса.

Этот приём пригодится вам и дальше. Сканер, прокси или скрипт подключается к сети лаборатории и обращается к контейнеру по имени. На хосте маршрут к приложению не нужен.

Тег образа здесь не закреплён, поэтому Docker скачивает то, что сегодня считается latest. Если лабораторию вы планируете повторять, закрепите тег после проверки.

Шаг 4: докажите, что у сети нет интернета

Успешная проверка что-то значит, только если рядом с ней стоит неуспешная. Шаг 3 показал, что контейнер с инструментом достаёт до приложения, значит, сеть работает. Теперь проверьте обратное направление:

bash
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"
text
curl: (28) Resolving timed out after 5000 milliseconds
exit=28

Флаг -m 5 ограничивает запрос пятью секундами. -sS скрывает индикатор прогресса, но оставляет ошибки видимыми. Код завершения 28 у curl означает таймаут. Сбой происходит на этапе разрешения имени: на запрос example.com не приходит ответ, и curl даже не открывает соединение. Именно этого и следует ожидать от сети без маршрута наружу.

Вам нужен как раз такой результат. Лаборатория достаёт до своих контейнеров и не достаёт до внешнего мира. Сохраните вывод шагов 3 и 4 в заметках вместе с датой и версией Docker. Эта запись пригодится, когда вы вернётесь и что-то окажется другим.

Шаг 5: запустите копию только на loopback для браузера

Браузер не может разрешить имя juice, а контейнер juice не публикует порт, поэтому с хоста до него не добраться. Чтобы открыть приложение в браузере, запустите вторую копию и опубликуйте её на loopback:

bash
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shop

Опция -p 127.0.0.1:3000:3000 связывает порт 3000 на loopback-интерфейсе с портом 3000 внутри контейнера. Эта копия использует стандартный bridge вместо juicenet, поэтому от интернета она не изолирована. Для копии, которую вы просто открываете в браузере, это сознательный компромисс. Если нужно что-то строже, придётся спроектировать и проверить отдельную конфигурацию.

Копии нужно немного времени на запуск. Используйте такой же цикл, направив его на хост:

bash
for i in $(seq 1 30); do
  s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
  echo "try $i: $s"
  [ "$s" = "200" ] && break
  sleep 3
done

На тестовой машине копия ответила 200 с десятой попытки. Теперь проверьте, к чему привязан порт:

bash
docker port juice-local
text
3000/tcp -> 127.0.0.1:3000

Дальше проверьте сторону LAN. Узнайте IPv4-адрес машины в локальной сети через ipconfig на Windows или ip addr на Linux. Затем запросите приложение по этому адресу:

bash
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://YOUR-LAN-IP:3000/

Замените YOUR-LAN-IP на свой адрес. На тестовой машине это напечатало 000, и curl завершился с кодом 7, то есть не смог подключиться. Такой же запрос к 127.0.0.1 вернул 200. Ради этой разницы порт и привязывают к loopback.

Теперь откройте http://127.0.0.1:3000 в браузере. Вы должны увидеть главную страницу Juice Shop. Пользуйтесь приложением только с этой же машины.

Выполните docker ps, чтобы увидеть оба контейнера рядом:

bash
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'
text
NAMES         PORTS
juice-local   127.0.0.1:3000->3000/tcp
juice         3000/tcp

В строке juice указан 3000/tcp без адреса хоста. Порт есть внутри сети, но на хост не проброшен. В строке juice-local видна привязка к хосту.

Шаг 6: приберите за собой

Остановите и удалите лабораторию. Контейнеры удаляются раньше сети, потому что Docker отказывается удалять сеть, к которой ещё подключены контейнеры:

bash
docker rm -f juice juice-local && docker network rm juicenet
text
juice
juice-local
juicenet

Docker выводит имена удалённых объектов. Затем убедитесь, что ничего не осталось:

bash
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'

Обе команды не должны ничего вывести. Фильтры совпадают с частью имени, поэтому читайте вывод, прежде чем считать его полным, и удаляйте только то, что запускали сами. Учтите также, что --rm убирает только одноразовые контейнеры с инструментами. Контейнеры, запущенные с -d, остаются, пока вы их не удалите, для этого и нужен этот шаг.

Частые ошибки

Публикация без адреса. -p 3000:3000 слушает все интерфейсы хоста. Для любого лабораторного приложения, которое вы открываете с той же машины, используйте префикс 127.0.0.1: и проверяйте результат через docker port.

Забытый --internal. Сеть работает и без этого флага, и ничто вас не предупредит. Выполните docker network inspect juicenet --format '{{.Internal}}' и убедитесь, что там true.

Подключение приложения к сети по умолчанию. Контейнер, запущенный без --network, попадает в стандартный bridge, у которого есть выход наружу. Если приложение должно оставаться изолированным, указывайте сеть каждый раз.

Доверие к неудачной проверке. Сбой на шаге 4 может быть вызван DNS, прокси, VPN или опечаткой. Шаг 3 — контрольный: он показывает, что проверка измеряет нужное.

Использование имени с хоста. Имя juice разрешается только для контейнеров в juicenet. Браузер не может им воспользоваться. Для этого и существует копия для браузера.

Оставленные запущенные контейнеры. Фоновый контейнер продолжает расходовать ресурсы и удерживать свой порт. Заканчивайте каждую сессию шагом очистки и затем запускайте docker ps.

Что эта лаборатория не доказывает

Опция внутренней сети — мера контроля, и у неё есть пределы.

Она не мешает инструменту в juicenet обращаться к любому другому контейнеру этой сети. Контейнеры одной сети могут общаться друг с другом, и благодаря этому работает шаг 3. Не держите в сети лаборатории посторонние сервисы.

Публикация на loopback защищает от других машин в вашей LAN. От программ, уже работающих на вашем компьютере, она не защищает: они тоже могут подключиться к 127.0.0.1.

Проверка охватывает одно имя и один внешний хост. Она не доказывает, что закрыты все пути наружу. Поведение Docker может различаться в Docker Desktop, на движках Linux и в режиме rootless, поэтому повторите проверки на той машине, которой пользуетесь на самом деле.

И наконец, эта лаборатория показывает только, что работает изоляция. Безопасность открытого доступа к Juice Shop она не подтверждает. Открывать к нему доступ небезопасно, поэтому его и изолируют.

Что дальше

Когда лаборатория заработает, её можно сделать полезнее несколькими небольшими изменениями:

  • Напишите короткий скрипт, который выполняет проверки шагов с 3 по 5 и выводит по каждой pass или fail. Запускайте его при каждом поднятии лаборатории.
  • Добавьте в juicenet второй сервис, например базу данных, и убедитесь, что приложение достаёт до него по имени. Так строятся лаборатории из нескольких контейнеров.
  • Прочитайте документацию Docker по сетям о параметрах драйверов, которыми вы ещё не пользовались, например о пользовательских сетях bridge с собственными подсетями.
  • Изучите основы обнаружения хостов, прежде чем что-либо сканировать. Об этом следующая статья: основы Nmap в собственной лаборатории. Сканируйте только свои сети и хосты или те, на проверку которых у вас есть письменное разрешение. Сеть juicenet подходит в качестве первой цели, потому что все контейнеры в ней под вашим контролем.

Источники

Источники

  1. Docker Engine networking overview
  2. docker network create (CLI reference)
  3. Docker port publishing and mapping
  4. OWASP Juice Shop (GitHub repository)

Ещё по теме: Хакерские лабы

Хакерские лабы

Как написать хороший отчёт о пентесте

Структура отчёта о пентесте и одна находка, которую сможет воспроизвести другой человек: улики, импакт, вектор CVSS v4.0 и исправление.

Средний7 мин чтения

Хакерские лабы

Как собрать законную домашнюю лабораторию

Домашняя лаборатория, которую вам разрешено атаковать: файл скоупа, порты Docker только на loopback, изолированная сеть, журнал и чистая разборка в конце.

Начальный3 мин чтения