Хакерские лабы
Зачем использовать NotHoly для CTF и лабораторий
Модель, что пишет exploit для вашего задания и объясняет, почему он работает, статический triage бинарника, и solve в вашей лаборатории.
Создайте сеть Docker без выхода в интернет, запустите в ней OWASP Juice Shop и опубликуйте копию для браузера только на 127.0.0.1. Проверка на каждом шаге.
Лаборатория безопасна настолько, насколько безопасны пути в неё и из неё. Если уязвимое приложение может выйти в интернет, оно способно обратиться туда, куда вы вовсе не собирались. Если оно слушает все интерфейсы вашей машины, до него дотянется любой в той же сети Wi-Fi. В Docker оба пути контролируются несколькими командами. В этом руководстве мишенью будет OWASP Juice Shop, и мы пройдём каждую команду по порядку.
Вы создадите сеть без выхода наружу, запустите в ней Juice Shop, обратитесь к приложению из второго контейнера и докажете, что сеть не достаёт до интернета. Затем запустите отдельную копию для браузера, опубликованную только на loopback-интерфейсе, чтобы её не было видно из локальной сети. Изоляция — первая мера защиты любой лаборатории, и от неё зависят все последующие упражнения, от сканирования до отработки эксплуатации.
Если вы ещё не запускали Juice Shop, начните со статьи OWASP Juice Shop в Docker. Здесь предполагается знание основ работы с контейнерами из того руководства.
В лаборатории безопасности работает софт, созданный для того, чтобы его ломали. В Juice Shop намеренно оставлены уязвимости, в этом его смысл. Риск исходит от другого: уязвимость, ошибка конфигурации или опечатка может открыть приложению или инструменту, который вы против него запускаете, дорогу куда-то ещё.
Вот три случая, когда что-то идёт не так:
-p 3000:3000. Тогда Docker принимает соединения на всех интерфейсах хоста, и другие устройства в вашей сети могут достучаться до уязвимого приложения.Изоляция закрывает каждый из этих случаев. У внутренней сети нет маршрута наружу. У порта, привязанного к loopback, нет пути через LAN. Выделенная сеть отделяет контейнеры лаборатории от других ваших проектов. Ни один из этих шагов не сложен, но в спешке о них легко забыть, поэтому в руководстве вы проверяете каждый.
Чтобы команды обрели смысл, нужно понимать три вещи.
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 info должен вывести данные о сервере без ошибок. На машине, где готовилось это руководство, Docker Engine 29.8.2 работал на Windows 11 через Docker Desktop.-w в одинарных кавычках нужно другое экранирование.Проверьте имена, прежде чем их выбирать. Другие проекты на вашей машине могут уже использовать имена вроде juice или juicenet. Сначала выполните:
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'Если в одном из списков есть juice, juice-local или juicenet, выберите другие имена или сначала удалите старые объекты. Не удаляйте то, что создали не вы.
Проверьте и образ, который собираетесь запустить. Juice Shop публикует метки с версией в своём образе:
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'Эта команда завершится ошибкой, пока образа нет локально, потому что скачивает его первый docker run. На машине, где готовилось это руководство, образ сообщил версию 20.3.0. У вас версия может быть другой, так что запишите её.
docker network create --internal juicenetDocker выводит ID новой сети. У вас он будет другим. Вот что он вывел на тестовой машине:
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9Убедитесь, что флаг сработал:
docker network inspect juicenet --format '{{.Internal}}'trueЕсли вывод false, вы создали сеть без --internal. Удалите её командой docker network rm juicenet и создайте заново. Не продолжайте, пока не увидите true: все дальнейшие проверки исходят из того, что флаг включён.
docker run -d --name juice --network juicenet bkimminich/juice-shopУ каждой опции своя задача. -d запускает контейнер в фоне. --name juice даёт ему DNS-имя, которое могут разрешить другие контейнеры в juicenet. --network juicenet подключает его только к этой внутренней сети. Опции -p здесь нет, поэтому контейнер не публикует порт на хост.
Docker выводит ID контейнера:
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8Приложение внутри слушает порт 3000. После запуска контейнера Juice Shop нужно время, чтобы подняться, поэтому не проверяйте сразу. Используйте цикл, который смотрит, получает ли контейнер с инструментом HTTP 200:
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.
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000200Разберём каждую часть. --rm удаляет контейнер с инструментом после завершения. --network juicenet помещает инструмент в ту же внутреннюю сеть, что и приложение. curlimages/curl — маленький образ с curl. http://juice:3000 использует имя контейнера, которое Docker разрешает внутри juicenet. Флаг -s скрывает индикатор прогресса, -o /dev/null отбрасывает тело ответа, а -w выводит только код статуса.
Этот приём пригодится вам и дальше. Сканер, прокси или скрипт подключается к сети лаборатории и обращается к контейнеру по имени. На хосте маршрут к приложению не нужен.
Тег образа здесь не закреплён, поэтому Docker скачивает то, что сегодня считается latest. Если лабораторию вы планируете повторять, закрепите тег после проверки.
Успешная проверка что-то значит, только если рядом с ней стоит неуспешная. Шаг 3 показал, что контейнер с инструментом достаёт до приложения, значит, сеть работает. Теперь проверьте обратное направление:
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"curl: (28) Resolving timed out after 5000 milliseconds
exit=28Флаг -m 5 ограничивает запрос пятью секундами. -sS скрывает индикатор прогресса, но оставляет ошибки видимыми. Код завершения 28 у curl означает таймаут. Сбой происходит на этапе разрешения имени: на запрос example.com не приходит ответ, и curl даже не открывает соединение. Именно этого и следует ожидать от сети без маршрута наружу.
Вам нужен как раз такой результат. Лаборатория достаёт до своих контейнеров и не достаёт до внешнего мира. Сохраните вывод шагов 3 и 4 в заметках вместе с датой и версией Docker. Эта запись пригодится, когда вы вернётесь и что-то окажется другим.
Браузер не может разрешить имя juice, а контейнер juice не публикует порт, поэтому с хоста до него не добраться. Чтобы открыть приложение в браузере, запустите вторую копию и опубликуйте её на loopback:
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, поэтому от интернета она не изолирована. Для копии, которую вы просто открываете в браузере, это сознательный компромисс. Если нужно что-то строже, придётся спроектировать и проверить отдельную конфигурацию.
Копии нужно немного времени на запуск. Используйте такой же цикл, направив его на хост:
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 с десятой попытки. Теперь проверьте, к чему привязан порт:
docker port juice-local3000/tcp -> 127.0.0.1:3000Дальше проверьте сторону LAN. Узнайте IPv4-адрес машины в локальной сети через ipconfig на Windows или ip addr на Linux. Затем запросите приложение по этому адресу:
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, чтобы увидеть оба контейнера рядом:
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'NAMES PORTS
juice-local 127.0.0.1:3000->3000/tcp
juice 3000/tcpВ строке juice указан 3000/tcp без адреса хоста. Порт есть внутри сети, но на хост не проброшен. В строке juice-local видна привязка к хосту.
Остановите и удалите лабораторию. Контейнеры удаляются раньше сети, потому что Docker отказывается удалять сеть, к которой ещё подключены контейнеры:
docker rm -f juice juice-local && docker network rm juicenetjuice
juice-local
juicenetDocker выводит имена удалённых объектов. Затем убедитесь, что ничего не осталось:
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 она не подтверждает. Открывать к нему доступ небезопасно, поэтому его и изолируют.
Когда лаборатория заработает, её можно сделать полезнее несколькими небольшими изменениями:
juicenet второй сервис, например базу данных, и убедитесь, что приложение достаёт до него по имени. Так строятся лаборатории из нескольких контейнеров.juicenet подходит в качестве первой цели, потому что все контейнеры в ней под вашим контролем.Хакерские лабы
Модель, что пишет exploit для вашего задания и объясняет, почему он работает, статический triage бинарника, и solve в вашей лаборатории.
Хакерские лабы
Структура отчёта о пентесте и одна находка, которую сможет воспроизвести другой человек: улики, импакт, вектор CVSS v4.0 и исправление.
Хакерские лабы
Домашняя лаборатория, которую вам разрешено атаковать: файл скоупа, порты Docker только на loopback, изолированная сеть, журнал и чистая разборка в конце.