Desplegando instancias individuales con CTFd + Docker
“La mejor manera de aprender algo es… Enseñándolo” . No recuerdo dónde ni a quien le escuche estás palabras, pero nunca fueron tan ciertas como el día de hoy que escribimos este artículo. Recordamos nuestros inicios apoyando en iniciativas educativas en el área de ciberseguridad se dieron con la Fundación Comunidad Dojo de Panamá , en ese entonces mis colegas de Toadsec nos propusimos apoyar realizando el primer CTF (Capture The Flag) del dojo en conjunto con la UTP utilizando https://github.com/nginx-proxy/nginx-proxy y el famoso CTFd (plataforma para crear este tipo de juegos en el formato jeoperdy) para incentivar la competencia entre entusiastas de la ciberseguridad y porque no también para profesionales para que demostraran su habilidad, en ese entonces se realizo con el famoso Owasp Juiceshop. Luego fuimos perfeccionando y nuestro colega Elzer comparte este documento con la comunidad https://backtrackacademy.com/articulo/como-crear-un-ctf-con-owasp-juice-shop-y-traefik en esta ocasión utilizando un proxy reversible más amigable y con una interfaz como es Traefik.
Luego de esta corta historia, explicaremos el motivo de este artículo, en participaciones recientes de nuestros colegas en el CTF de Nahamcon observamos como se utilizaba la plataforma CTFd en conjunto con el lanzamiento de contenedores con diversos retos, el reto fue realizado por la empresa https://justhacking.com/, no es la primera vez que podemos ver este tipo de herramientas que apoyan al momento de organizar un CTF y en si le dan un aire más profesional como las plataformas de https://tryhackme.com/ o https://www.hackthebox.com/. Es importante mencionar que para talleres pues la infraestructura a utilizar la podemos desplegar en hipervisores como Proxmox, Vmware Esxi, etc, pero requiere de mucha personalización y no se estila que sean tipo retos CTF o competencia.
1. Multiples Problemas
Primero investigamos un poco dado que el organizador del evento Nahamcon había colaborado con otros influencer de ciberseguridad bastante conocido el señor John Hammond en el video del enlace enumera algunas de las herramientas utilizadas para desplegar su CTF las cuales mencionaremos
-
Docker
-
Docker-compose
-
NsJail
-
Ctfd
-
VPS en Digital Ocean
-
Una vez teniendo esto como base no pusimos manos a la obra e implementamos el ctfd el cual le instalamos el plugin de CTFd Docker, el único inconveniente hasta este punto era que habían que hacer unas correcciones en el código que el mismo repositorio indica https://github.com/offsecginger/CTFd-Docker-Challenges, no obstante no logramos comunicar el api de docker con el plugin de CTFd-Docker-Challenges
Probamos luego otro plugin llamado CTFd-Docker-Plugin el cual si logramos conectar con el api de docker pero solo llegamos hasta ese punto
En los retos si logramos ver las imágenes de los contenedores
En este punto ya habíamos perdida un poco las esperanzas puesto todo apuntaba a que tendríamos que realizar una implementación personalizada, pero no era nuestro objetivo inicial.
Al final logramos encontrar un plugin que parece era lo que estábamos buscando, desplegar instancias de contenedores individuales por usuarios. Optamos por usar el plugin CTFd-Whale, no entraremos en detalles pero de igual manera nos enfrentamos con varios errores porque deberíamos utilizar esta herramienta que la única documentación que encontramos esta en Chino y teníamos que usar la versión adaptada del CTFd del autor.
2. Requisitos
Para que todo este funcionando correctamente y se pueda implementar el CTFd-Whale debemos contar con lo siguiente:
- Docker y Docker-compose
- Docker Swarm (viene con docker explicaremos la configuración en la sección de instalación)
- Frps y Frpc (utilizaremos el docker-compose que trae la imagen)
- Habilitar el servicio de Docker API (no exponerlo en el firewall del vps)
- Recordar tener los puertos abierto 22,80, 443, 8000,28000-281000
- Un subdominio y cloudflare de ser posible
- VPS en cualquier proveedor de cloud con mínimo 2 vcpu y 8GiB RAM (nosotros usamos una instancia con 4vcpu y 16 GiB de RAM).
3. Instalación
Primero instalamos los requisitos y clonamos el repositorio
apt update && apt install docker.io -y && apt install docker-compose -y && apt install net-tools -y
git clone https://github.com/TomAPU/CTFd_with_CTFd-whale/ ctfd
Ahora vamos a configurar el manager del docker swarm, podemos seguir la siguiente guía, editamos el /etc/hosts y agregamos el ip del servidor en nuestro caso es 10.1.0.4
nano /etc/hosts
Probamos si le llegamos al manager con ping
Iniciamos el docker swarm
docker swarm init --advertise-addr 10.1.0.4
Configuramos la etiqueta que utilizara el FRP (fast reverse proxy)
docker node update --label-add='name=linux-1' $(docker node ls -q)
Ahora entramos al directorio que clonamos y necesitamos hacer unos ajustes en la versión del docker-compose.yml y los puertos a utilizar por el FRP, además de que debemos comentar la imagen de nginx ya que el ctfd tiene esa configuración preparada
nano docker-compose.yml
Debe quedarnos de la siguiente manera
version: '2.2'
services:
ctfd:
build: .
user: root
restart: always
ports:
- "80:8000"
environment:
- UPLOAD_FOLDER=/var/uploads
- DATABASE_URL=mysql+pymysql://ctfd:ctfd@db/ctfd
- REDIS_URL=redis://cache:6379
- WORKERS=1
- LOG_FOLDER=/var/log/CTFd
- ACCESS_LOG=-
- ERROR_LOG=-
- REVERSE_PROXY=true
volumes:
- .data/CTFd/logs:/var/log/CTFd
- .data/CTFd/uploads:/var/uploads
- .:/opt/CTFd:ro
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- db
networks:
default:
internal:
frp:
ipv4_address: 172.1.0.2
mem_limit: 450M
#nginx:
# image: nginx:1.17
# restart: always
# volumes:
# - ./conf/nginx/http.conf:/etc/nginx/nginx.conf
# ports:
# - 82:80
# depends_on:
# - ctfd
# networks:
# default:
# internal:
# mem_limit: 450M
db:
image: mariadb:10.4.12
restart: always
environment:
- MYSQL_ROOT_PASSWORD=ctfd
- MYSQL_USER=ctfd
- MYSQL_PASSWORD=ctfd
- MYSQL_DATABASE=ctfd
volumes:
- .data/mysql:/var/lib/mysql
networks:
internal:
# This command is required to set important mariadb defaults
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --wait_timeout=28800, --log-warnings=0]
mem_limit: 450M
cache:
image: redis:4
restart: always
volumes:
- .data/redis:/data
networks:
internal:
mem_limit: 450M
frps:
image: glzjin/frp:latest
restart: always
volumes:
- ./frps:/conf/
entrypoint:
- /usr/local/bin/frps
- -c
- /conf/frps.ini
ports:
- "28000-28100:28000-28100"
- "6490:6490"
networks:
frp:
ipv4_address: 172.1.0.4
default:
frpc:
image: glzjin/frp:latest
restart: always
volumes:
- ./frpc:/conf/
entrypoint:
- /usr/local/bin/frpc
- -c
- /conf/frpc.ini
networks:
frp:
ipv4_address: 172.1.0.3
frp-containers:
mem_limit: 250M
networks:
default:
internal:
internal: true
frp:
#attachable: true
driver: bridge
ipam:
config:
- subnet: 172.1.0.0/16
frp-containers:
driver: overlay
internal: false
#attachable: true
ipam:
config:
- subnet: 172.2.0.0/16
Ahora debemos editar el archivo de requerimientos antes de buildear la imagen y modificamos la versión de jinja2 de jinja2==2.11.2 a jinja2==2.11
nano requierements.txt
Por último debemos actualizar la versión del build del docker file para que funcione, el Dockerfile debe quedar de la siguiente manera
FROM python:3.9-slim-buster
WORKDIR /opt/CTFd
RUN mkdir -p /opt/CTFd /var/log/CTFd /var/uploads
# hadolint ignore=DL3008
RUN echo 'deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib \
deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib \
deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib \
deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib \
deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib \
deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib \
deb http://mirrors.aliyun.com/debian-security/ bullseye/updates main non-free contrib \
deb-src http://mirrors.aliyun.com/debian-security/ bullseye/updates main non-free contrib'> /etc/apt/sources.list && \
apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
default-mysql-client \
python3-dev \
libffi-dev \
libssl-dev \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /opt/CTFd/
RUN pip install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com --no-cache-dir
COPY . /opt/CTFd
# hadolint ignore=SC2086
RUN for d in CTFd/plugins/*; do \
if [ -f "$d/requirements.txt" ]; then \
pip install -r $d/requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com --no-cache-dir; \
fi; \
done;
RUN adduser \
--disabled-login \
-u 1001 \
--gecos "" \
--shell /bin/bash \
ctfd
RUN chmod +x /opt/CTFd/docker-entrypoint.sh \
&& chown -R 1001:1001 /opt/CTFd /var/log/CTFd /var/uploads
USER 1001
EXPOSE 8000
ENTRYPOINT ["/opt/CTFd/docker-entrypoint.sh"]
Nota: se cambio la versión de la imagen de python por la última del CTFD
Listo procedemos a construir la imagen
docker-compose build
Demorara un tiempo, cuando termine debemos ver los siguiente
Procedemos a iniciar el docker-compose.yml
docker-compose up
Podemos comprobar que todos los contenedores están arriba
docker-compose ps
docker ps
Ahora podemos ir a nuestro sitio y configurar el usuario admin https://ctfd.toadsec.xyz, procedemos a ir a Admin Panel –> Whale –>Docker
Y Ajustamos como aparece en la siguiente imagen
En la siguiente sección FRP
Luego podemos darle a submit, la plantilla se guarda a medida que ajustamos los campos
En las sección de limites no vamos a cambiar nada, el tiempo de duración de la instancia es una hora en segundos
Ahora vamos a descargar una imagen de un contenedor vulnerable, crearemos el reto y la iniciaremos
En el admin panel vamos a ir a la sección de challenges
Procedemos a darle click al botón de más para crear un challenge tipo docker
Llenamos los datos
Ahora en la sección de docker abajo, para el puerto de redirección es el puerto que usa el Docker, de manera dinámica se le asignara un puerto en el rango que colocamos en el docker-compose.yml
Colocamos los puntos y clickeamos en el botón create
Tendremos un mensaje que podemos llenar de la siguiente manera y pinchamos en Finish
Creamos un equipo y vamos al reto
Accedemos e iniciamos el contendor
Ahora nos debe salir el contenedor iniciado
Podemos ir al reto, recordemos que este reto es web asi que debemos acceder es por http://ctfd.toadsec.xyz:28072
Nota: cloudflare no permitia acceder por dominio al reto asi que tuvimos que entrar por el ip:puerto
Podemos ir a la sección de Instancia del plugin Whale y ver la instancia corriendo
De igual manera podemos verificar en el servidor el contenedor que fue iniciado, lo identificamos por el nombre del contenedor
6. Conclusiones y recomendaciones
Ahora tenemos lo necesario para ir armando contenedores vulnerables ya prefabricados o crear los nuestros para cada equipo o de manera individual aislados, pueden ser destruidos o alargar la duración de la instancia sino hemos logrado resolver los retos, lo que es muy útil para poder incentivar la competencia de manera saludables.
Por mejorar tenemos lo siguientes puntos:
- Aunque la vm utilizada debe ser suficiente para llevar un reto con 100 equipos y 500 ips únicos, podemos ver de incluir en cluster de swarm un worker que permita repartir los contenedores.
- Necesitamos una herramienta que administre los contenedores de manera gráfica en el caso que el plugin de problemas.
- Podemos crear más aislamientos con los plugins de nsjail para evitar que rompan los contadores https://github.com/google/nsjail
- Para la denegación de servicio usamos cloudflare, pero para los retos igual tenemos que usar el ip real por tanto, debemos modificar esa parte para que no hagan DDOS o poner un tope de peticiones (rate limit).
- Para los retos de pwn podemos implementar Xinet https://medium.com/shellpwn/hosting-your-own-ctf-765607dbe06f
- No creemos que usar Kubernates nos facilite dado que es un ctf que queremos realizar esporadicamente.
- Utilizar otros plugins para exportar los retos y uno para notificaciones de first blood en los challenges
- Aumentar los workers de Gunicorn del ctfd .
- Separar la Base de datos y cambiar las credenciales por defecto, en este caso nosotros la dejamos de esa manera para la demostración.
- Para las imánese de docker notamos que debemos exponer los puertos que vamos a utilizar internamente de los contenedores, para que el proxy lo pueda redirigir correctamente.
- Utilizar los contenedores con usuario con menos privilegios en lugar del root, para evitar cualquier tema de seguridad con los usuarios si alguno escapa del aislamiento.
- Podemos instalar el proxy localmente en lugar del contenedor dado que a veces hay que builder la imagen nuevamente para que agarre algunas configuraciones.
Referencias
-
https://err0r.top/article/CTFD/
-
https://huaweicloud-csdn-net.translate.goog/63311505d3efff3090b51ab1.html?_x_tr_sl=zh-CN&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=sc
-
https://blog-kdxcxs-com.translate.goog/posts/server/ctfd%E8%B8%A9%E5%9D%91/?_x_tr_port=4433&_x_tr_sl=zh-CN&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=sc
-
https://www-zhaoj-in.translate.goog/read-6333.html/comment-page-1?_x_tr_sl=zh-CN&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=sc
-
https://blog.hz2016.com/2022/03/
-
https://blog.hz2016.com/2022/02/
-
http://www.boredhackerblog.info/2018/05/providing-shell-for-ctfs.html