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

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

  • Plugin Ctfd Docker

    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

    3

    Probamos luego otro plugin llamado CTFd-Docker-Plugin el cual si logramos conectar con el api de docker pero solo llegamos hasta ese punto

    4

    En los retos si logramos ver las imágenes de los contenedores

    5

    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 

6

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

7

Probamos si le llegamos al manager con ping

8

Iniciamos el docker swarm

docker swarm init --advertise-addr  10.1.0.4

9

Configuramos la etiqueta que utilizara el FRP (fast reverse proxy)

docker node update --label-add='name=linux-1' $(docker node ls -q)

10

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

11

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

12

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 

13

Demorara un tiempo, cuando termine debemos ver los siguiente

14

Procedemos a iniciar el docker-compose.yml

docker-compose up

15

Podemos comprobar que todos los contenedores están arriba

docker-compose ps
docker ps 

16

Ahora podemos ir a nuestro sitio y configurar el usuario admin https://ctfd.toadsec.xyz, procedemos a ir a Admin Panel –> Whale –>Docker

17

Y Ajustamos como aparece en la siguiente imagen

18

En la siguiente sección FRP

19

Luego podemos darle a submit, la plantilla se guarda a medida que ajustamos los campos

22

En las sección de limites no vamos a cambiar nada, el tiempo de duración de la instancia es una hora en segundos

23

Ahora vamos a descargar una imagen de un contenedor vulnerable, crearemos el reto y la iniciaremos

24

En el admin panel vamos a ir a la sección de challenges

25

Procedemos a darle click al botón de más para crear un challenge tipo docker

26

Llenamos los datos

27

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

28

Colocamos los puntos y clickeamos en el botón create

29

Tendremos un mensaje que podemos llenar de la siguiente manera y pinchamos en Finish

30

Creamos un equipo y vamos al reto

31

Accedemos e iniciamos el contendor

32

Ahora nos debe salir el contenedor iniciado

33

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

34

Podemos ir a la sección de Instancia del plugin Whale y ver la instancia corriendo

35

De igual manera podemos verificar en el servidor el contenedor que fue iniciado, lo identificamos por el nombre del contenedor

36

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:

  1. 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.
  2. Necesitamos una herramienta que administre los contenedores de manera gráfica en el caso que el plugin de problemas.
  3. Podemos crear más aislamientos con los plugins de nsjail para evitar que rompan los contadores https://github.com/google/nsjail
  4. 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).
  5. Para los retos de pwn podemos implementar Xinet https://medium.com/shellpwn/hosting-your-own-ctf-765607dbe06f
  6. No creemos que usar Kubernates nos facilite dado que es un ctf que queremos realizar esporadicamente.
  7. Utilizar otros plugins para exportar los retos y uno para notificaciones de first blood en los challenges
  8. Aumentar los workers de Gunicorn del ctfd .
  9. Separar la Base de datos y cambiar las credenciales por defecto, en este caso nosotros la dejamos de esa manera para la demostración.
  10. 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.
  11. 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.
  12. 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