Однажды была поставлена задача создать собственный стенд для проведения CTF-соревнований. Задачи были подготовлены в формате Docker-контейнеров, но для них была необходима сеть.
Возникает логичный вопрос «Зачем?». Сеть позволит выделить отдельный IP адрес каждой задаче. В принципе, конечно, можно обойтись и без сети, однако такой подход имеет несколько минусов, а именно:
Службы имеют нестандартные порты.
Все задачи развернуты на одном адресе, так что из задания уходит важная задача по сканированию портов. Участники сразу будут их знать, и останется только уточнить службу.
Раздача портов вручную. В моих задачах по большей части был не один сервис на задачу. Так что возникает необходимость логического разделения заданий. А также могут быть проблемы с автоматическим запуском задач.
Задачи подразумевают использование reverse shell, а если сервис разворачивать в интернете, то пользователи не смогут его использовать без белого адреса. Получается, что 99% пользователей не смогут решить задачу без использования, например, VPS.
Хотелось бы ограничить доступ в интернет задачам. Чтобы после проведения успешной атаки пользователь не смог использовать ресурсы машины в своих целях.
Кратко о том, что хотелось получить:
Задачи должны быть в изолированной сети, без доступа в интернет, но при необходимости должна быть возможность его выдать.
Каждый участник должен иметь индивидуальный IP адрес.
Каждая задача должна иметь индивидуальный IP адрес.
Не должно быть ограничений на использование портов у каждой задачи.
После решения задачи пользователь не должен использовать ресурсы в своих целях.
Один конфиг VPN для всех участников соревнования.
В статье рассматривается возможность работы как с Docker-контейнерами, так и виртуальными машинами. Но, если какой-то пункт не нужен, его можно пропустить.
Софт и железо
В качестве гипервизора использовался Proxmox 8.1.4, а в качестве маршрутизатора — виртуальная машина Debian 12.
Для понимания структуры проекта ниже представлена схема сети:
Может возникнуть вопрос: «Почему именно OpenVPN?».
Как альтернативу можно было бы рассмотреть Wireguard, однако:
Запуск его конфигов несколько сложнее для пользователя.
Также OpenVPN позволяет подключить несколько клиентов по одному конфигу.
OpenVPN уже является стандартом во время проведения CTF-соревнований.
Базовая настройка. Обновление системы
За основу была взята Debian 12. Обновляем систему и ставим софт для базовой работы:
sudo apt update && sudo apt upgrade -y && sudo apt install -y curl vim nano wget net-tools iptables-persistent
IP Forward
Далее надо включить IP forward в системе:
sudo nano /etc/sysctl.conf
Редактируем параметр: net.ipv4.ip_forward = 1.
Нужно поставить значение 1
Статический адрес в сети
Так как наше устройство будет выполнять роль сервера и маршрутизатора, то получать адрес по DHCP будет не самым правильным решением. Следовательно, необходимо прописать статический адрес:
Редактируем файл /etc/network/interfaces в любом удобном текстовом редакторе:
sudo nano /etc/network/interfaces
Добавим следующие строки для настройки статического IP адреса:
auto ens18
iface ens18 inet static
address 192.168.0.155
netmask 255.255.255.0
gateway 192.168.0.1
dns-nameservers 8.8.8.8
После перезагрузки сервер получит адрес 192.168.0.155
sudo systemctl restart NetworkManager.service
Установка OpenVPN
За основу был взят этот проект: github.com/kylemanna/docker-openvpn
Но разворачивать его будем сразу на основной системе для удобства работы с правилами:
Сменим пользователя на пользователя root:
sudo su -
Устанавливаем нужные пакеты:
sudo apt install -y openvpn iptables bash git easy-rsa pamtester
Пропишем ссылку для использования файлов из /usr/share/easy-rsa/easyrsa
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin
Пропишем переменные окружения для работы скриптов:
echo "export OPENVPN=/etc/openvpn" >> ~/.bashrc
echo "export EASYRSA=/usr/share/easy-rsa"
>> ~/.bashrcecho "export EASYRSA_CRL_DAYS=3650" >> ~/.bashrc echo "export EASYRSA_PKI=$OPENVPN/pki" >> ~/.bashrc
Активируем конфиг
~/.bashrc
, чтобы переменные окружения стали активны:source ~/.bashrc
Клонируем репозиторий и переходим в директорию:
git clone https://github.com/kylemanna/docker-openvpn cd docker-openvpn
Копируем нужные файлы и выдаем права:
cp ./bin/* /usr/local/bin
cp ./otp/openvpn /etc/pam.d/ chmod a+x /usr/local/bin/* cd
Далее выполняем конфигурацию. Вместо (ТУТ_НУЖНО_УКАЗАТЬ_IP_АДРЕС_СЕРВЕРА), указываем IP адрес сервера:
ovpn_genconfig -N -d -u udp://ТУТ_НУЖНО_УКАЗАТЬ_IP_АДРЕС_СЕРВЕРА
Генерируем ключи:
ovpn_initpki
Пример ключа:
Qwertyqwertyqwerty123
Далее следуем текстовым инструкциям:
Генерируем конфиги для участников. При создании будет запрошен ключ, который мы вводили выше:
easyrsa build-client-full ctf-team nopass
Сохраняем конфиг в файл:
ovpn_getclient ctf-team > ./ctf-team.ovpn
Так как нам нужно, чтобы участники могли подключаться через один конфиг к сети, отредактируем конфиг:
vim /etc/openvpn/openvpn.conf
Добавляем строку:
duplicate-cn
Запускаем OpenVPN:
ovpn_run
Скачиваем конфиг сервера по пути
/root/ctf-team.ovpn
к себе на компьютер.Это может выглядеть так:
scp root@SERVER_IP:/root/ctf-team.ovpn ~
И пробуем запустить.
Запускам на основной системе:
sudo openvpn ctf-team.ovpn
Если все работает, то пробуем перезапустить сервер и проверяем снова. OpenVPN должен запуститься самостоятельно.
Настройка Docker
Установим Docker: sudo apt install -y docker.io docker-compose
Выдаем права пользователю на работу с docker:
sudo usermod -aG docker НАШ_ЮЗЕР
Настройка сети
Вновь заходим в консоль, чтобы права обновились, и создаем docker сеть: docker network create --subnet=192.168.158.0/24 ctf-net
Подробнее про работу с Docker network можно почитать тут.
Открываем конфиг для VPN, который мы скачали с сервера, и добавляем в него следующие строки:
route 192.168.255.0 255.255.255.0
route 192.168.158.0 255.255.255.0
После чего подключаемся к OpenVPN на своем компьютере и вводим:
ip route
Если есть эти две строки, то все сделано правильно:
192.168.158.0/24 via 192.168.255.5 dev tun0
192.168.255.0/24 via 192.168.255.5 dev tun0
Правила iptables
Прописываем правила для iptables:
sudo iptables -A FORWARD -i tun0 -s 192.168.255.0/24 -d 192.168.158.0/24 -j ACCEPT
sudo iptables -A FORWARD -o tun0 -s 192.168.158.0/24 -d 192.168.255.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Разрешим трафик от машин (192.168.158.0/24) к сети (192.168.255.0/24) sudo iptables -A FORWARD -o tun0 -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
sudo iptables -A FORWARD -i tun0 -s 192.168.255.0/24 -d 192.168.158.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
Тесты сети
Тесты сети до контейнера
На сервере поднимем тестовый контейнер с nginx:
docker run --network=ctf-net -d --rm --name nginx_test nginx
После чего получаем адрес этого контейнера в docker сети:
docker inspect nginx_test | grep IPAddress
В данном случае это 192.168.158.2 , так что на основной системе при подключенном VPN в браузере захожу на сайт 192.168.158.2
Тест сети от контейнера
Сделаем проверку в обратную сторону.
Получаю свой адрес через команду ifconfig:
docker exec -ti nginx_test /bin/bash
После чего получаю консоль внутри контейнера от пользователя root.
По стандарту в базовом контейнере nginx нет ping, поэтому его необходимо поставить:
apt update && apt install iputils-ping
После чего проверяем доступ клиентов к VPN:
Все работает, так что выходим из контейнера командой exit
Сохранить правила iptables
Теперь нужно сохранить правила iptables.
Переключаемся на пользователя root на сервере:
sudo su -
И сохраняем:
iptables-save > /etc/iptables/rules.v4
Запуск CTF-заданий
Тут все просто: любой контейнер, запущенный с параметром: --network=ctf-net
, сразу будет настроен и работать с нужной нам сети.
Так что, запуск заданий будет выглядеть вот так: docker run --network=ctf-net -d --rm --name CONTAINER_NAME IMAGES_NAME
Настройка доступа до изолированной сети виртуальных машин
Docker – это отлично, но также задачи могут быть развёрнуты на виртуальных машинах, так что их тоже необходимо подключить.
Для этого в нашем гипервизоре создадим виртуальную сеть.
Затем подключим нашу машину новым сетевым интерфейсом в эту сеть.
Здесь мы можем видеть, что ens19 остался без адреса, так как DHCP сервера в изолированной сети нет. Следовательно, нужно прописать статистический адрес на интерфейс и поставить DHCP сервер.
Уже известным образом прописываем адрес на интерфейс (пункт
Статистический адрес в сети
)
Установка DHCP (Этот модуль можно свернуть)
sudo apt install isc-dhcp-server
Редактируем конфиг:
sudo nano /etc/dhcp/dhcpd.conf
Добавляем вот это в пустое место:
subnet 10.0.50.0 netmask 255.255.255.0 {
range 10.0.50.10 10.0.50.240;
option domain-name-servers 8.8.8.8;
option subnet-mask 255.255.255.0;
option routers 10.0.50.1;
default-lease-time 600;
max-lease-time 7200;
}
***Хотелось бы обратить внимание на range 10.0.50.10 10.0.50.240 .
Тут устанавливается диапазон раздачи IP адресов DHCP сервером. То есть, 2-10 и 240- 254 можно использовать в качестве сервисных адресов. Например,службы мониторинга или сервисы с обязательно статическим адресом.
Редактируем следующий файл:
sudo nano /etc/default/isc-dhcp-server
Нужно указать интерфейс, на который будет раздавать адреса DHCP сервер.
Перезагружаем службу:
sudo systemctl restart isc-dhcp-server
Проверяем ее статус:
sudo systemctl status isc-dhcp-server
Теперь стоит проверить ее в деле.
Для этого создадим виртуальную машину и подключим ее в той же сети. Машина должна получить IP адрес.
Правила iptables
Добавляем маршрут в конфиг OpenVPN, который скачали с сервера.
Прописываем правила для iptables. (Выполнять от root)
Обратите внимание на название интерфейсов. Вероятно, они будут называться по-другому.
iptables -A FORWARD -i tun0 -o ens19 -s 192.168.255.0/24 -d 10.0.50.0/24 -j ACCEPT
iptables -A FORWARD -i ens19 -o tun0 -s 10.0.50.0/24 -d 192.168.255.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Разрешим трафик от машин (10.0.50.0/24) к сети (192.168.255.0/24) iptables -A FORWARD -i ens19 -o tun0 -s 10.0.50.0/24 -d 192.168.255.0/24 -j ACCEPT
iptables -A FORWARD -i tun0 -o ens19 -s 192.168.255.0/24 -d 10.0.50.0/24 -m state --state RELATED,ESTABLISHED -j ACCEPT
После чего пробуем с виртуальной машины пинговать клиента, подключенного к VPN:
Сохраняем новые правила:
iptables-save > /etc/iptables/rules.v4
На этом данный этап настройки для виртуальных машин можно считать оконченным.
Дополнительные правила iptables
Обратите внимания на названия интерфейсов. Вероятно, они будут называться по-другому.
Желательно сделать бекап виртуальной машины, чтобы ничего случайно не сломать.
Разрешим подключения по ssh:
sudo iptables -A INPUT -i ens18 -s 192.168.0.1/24 -p tcp --dport 22 -j ACCEPT
Откроем доступ до udp порта VPN:
sudo iptables -A INPUT -i ens18 -p udp --dport 1194 -j ACCEPT
Применим политику по умолчанию:
sudo iptables -P FORWARD DROP
Запрет на выход в интернет для tun:
iptables -I FORWARD -i tun0 -o ens18 -j DROP
iptables -I FORWARD -i ens18 -o tun0 -j DROP
Запрет на выход в интернет для Docker
Контейнер по стандарту имеет возможность ходить в интернет, но нас это не устраивает, поэтому нужно добавить правило запрещающие это:
iptables -I FORWARD -s 192.168.158.0/24 -j DROP
iptables -I FORWARD -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
Запрет на выход в интернет для VM
Так как у нас нет правил разрешающих движение трафика в интернет, то виртуальная машина по стандарту не сможет выйти в интернет.
Разрешение на выход в интернет для Docker
Чтобы разрешить выход в интернет, нужно снять правило.
Это можно сделать вот так:
iptables -D FORWARD -s 192.168.158.0/24 -j DROP
Вернуть правило обратно и заблокировать доступ к интернету:
iptables -D FORWARD -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
iptables -I FORWARD -s 192.168.158.0/24 -j DROP
iptables -I FORWARD -s 192.168.158.0/24 -d 192.168.255.0/24 -j ACCEPT
Разрешение на выход в интернет для VM
Здесь немного сложнее. Нужно активировать пересылку пакетов, сделать это можно используя MASQUERADE. А также разрешить пересылку пакетов для адресов 10.0.50.0/24
iptables -t nat -A POSTROUTING -o ens18 -s 10.0.50.0/24 -j MASQUERADE
iptables -A FORWARD -i ens19 -o ens18 -s 10.0.50.0/24 -j ACCEPT
iptables -A FORWARD -i ens18 -o ens19 -d 10.0.50.0/24 -j ACCEPT
Обращаю внимание,что ens18 – название интерфейса, который смотрит в интернет.
Также обратная задача, а именно – вернуть запрет на трафик:
iptables -t nat -D POSTROUTING -o ens18 -s 10.0.50.0/24 -j MASQUERADE
iptables -D FORWARD -i ens19 -o ens18 -s 10.0.50.0/24 -j ACCEPT
iptables -D FORWARD -i ens18 -o ens19 -d 10.0.50.0/24 -j ACCEPT
Сохранение правил
После нужных нам изменений сохранили правила: iptables-save > /etc/iptables/rules.v4
Так как при перезагрузке правила, которые не были сохранены, автоматически будут удалены, то это очень удобно использовать.
На данном стенде контейнеры и виртуальные машины по стандарту не имеют выхода в интернет, но если мне нужно что-то скачать для настройки, то я просто прописываю правило для этого, а после работы выключаю стенд.
В итоге, когда команда приходит атаковать стенд, интернета на задачах по стандарту нет, и не нужно помнить, было разрешение на вход или нет.
Вывод и немного мыслей
Стенд для проведения CTF-соревнований или проведения экспериментов собрать не составит особого труда.
Изначально проект делался для проведения соревнований в университете, а в итоге получился действительно удобным, и было развернуто несколько копий под разные задачи.
(Одна стоит даже дома, для локальных экспериментов с друзьями. Так как к такой сети можно подключиться удаленно и иметь свой стенд для тренировок.)
Как было сказано ранее, проводились соревнования в институте, так что очень хотелось следить за нагрузкой в реальном времени. Соревнования полностью проходили на Docker-контейнерах, а для отслеживания их ресурсов прекрасно подходит проект domolo.
Просто скачиваем и запускаем через docker-compose. В итоге имеем статистику по ресурсам у каждого контейнера. Так что, если кто-то запустит условный майнер в контейнере, то это сразу будет видно, и этот контейнер можно будет быстро уничтожить.