Índice / Módulo 1 — Mentalidad y Entorno

1.6 Docker

Docker cambió la forma de montar laboratorios de seguridad. En segundos tienes una aplicación vulnerable funcionando, aislada, reproducible y descartable. Sin instalar dependencias, sin romper el sistema, sin snapshotear máquinas de 20 GB. Aprende a usarlo bien desde el principio.

1.6.1 ¿Por qué Docker y no una VM? Contenedores vs. virtualización

La pregunta tiene trampa: Docker y las máquinas virtuales no compiten directamente. Son herramientas con propósitos distintos y en un laboratorio serio acabarás usando las dos. Pero para entender cuándo usar cada una, primero tienes que entender qué hace diferente a Docker por dentro.

Comparativa arquitectura VM vs contenedor Docker

Cómo funciona Docker por dentro

Una máquina virtual emula hardware completo. Tiene su propio kernel, sus propios drivers, su propio sistema de archivos arrancando desde cero. El hypervisor gestiona la ilusión de que es hardware real. Eso cuesta recursos: arrancar una VM de Ubuntu tarda entre 30 segundos y 2 minutos, y ocupa al menos 1-2 GB de RAM solo para estar encendida.

Docker no virtualiza hardware. Usa dos características del kernel de Linux — namespaces y cgroups — para aislar procesos dentro del mismo sistema operativo:

  • Namespaces: crean vistas aisladas de recursos del sistema — el contenedor tiene su propio PID namespace (no ve los procesos del anfitrión), su propio network namespace (su propia interfaz de red, su propia tabla de rutas), su propio mount namespace (su propio sistema de archivos), etc.
  • cgroups (control groups): limitan y miden el uso de recursos — cuánta CPU puede usar el contenedor, cuánta RAM, cuánto ancho de banda de disco y red. Sin cgroups, un contenedor podría consumir todos los recursos del anfitrión.

El resultado: un contenedor arranca en milisegundos, ocupa solo la memoria que necesita el proceso que ejecuta, y comparte el kernel del anfitrión. DVWA en un contenedor Docker puede estar funcionando en menos de 5 segundos desde que ejecutas el comando.

La tabla que necesitas

Característica Máquina Virtual Contenedor Docker
Tiempo de arranque 30 seg — 2 min Milisegundos
Uso de RAM mínimo 512 MB — 2 GB por VM Solo lo que usa el proceso
Aislamiento Completo (kernel propio) Parcial (kernel compartido)
Portabilidad Imagen de disco pesada (GBs) Imagen ligera (MBs), en Docker Hub
SO invitado Cualquiera (Windows, Linux, BSD) Solo Linux (en anfitrión Linux)
Persistencia El disco persiste por defecto Efímero por defecto (datos se pierden al borrar el contenedor)
Reproducibilidad Alta (snapshot) Máxima (Dockerfile define el estado exacto)
Escenarios de hacking Windows ✅ Sí ❌ No (sin Linux kernel)

Cuándo usar cada uno

  • Usa Docker para: aplicaciones web vulnerables (DVWA, Juice Shop, WebGoat), servicios de red (MySQL, Redis, MongoDB expuestos), herramientas que tienen dependencias conflictivas, entornos que necesitas levantar y destruir rápido
  • Usa VMs para: objetivos Windows (Active Directory, máquinas CTF de Windows), análisis de malware (necesitas kernel aislado), laboratorios de red completos (cada máquina necesita su propia pila de red), cuando el objetivo requiere un SO diferente al anfitrión
  • Usa ambos juntos: Kali en VM atacando contenedores Docker de aplicaciones vulnerables — lo mejor de los dos mundos
⚠️
Docker en Linux vs Docker Desktop en Windows/macOS: En Linux, Docker corre directamente sobre el kernel del anfitrión — el aislamiento y el rendimiento son los esperados. En Windows y macOS, Docker Desktop ejecuta una VM Linux ligera por debajo (WSL2 en Windows, HyperKit en macOS), así que hay una capa de virtualización adicional. Para laboratorios de hacking lo ideal es trabajar en Linux directamente.

1.6.2 Instalación y primeros comandos

Docker se instala en segundos y los primeros comandos son suficientes para el 90% de lo que necesitas en un laboratorio. Vamos por partes.

Instalación en Debian/Ubuntu/Kali

⚠️
La barra ivertida "\" al final de la línea es como decir le a linux que el comando sigue en la siguiente linea, por ejemplo:

curl -fsSL https://download.docker.com/linux/debian/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg


Es lo mismo que hacer:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Se usa por comodidad cuando la línea es muy larga.
bash — instalación Docker en Debian/Ubuntu/Kali
# 1. Eliminar versiones antiguas si las hay
sudo apt remove docker docker-engine docker.io containerd runc

# 2. Instalar dependencias
sudo apt update
sudo apt install -y ca-certificates curl gnupg

# 3. Añadir la clave GPG oficial de Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
    sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 4. Añadir el repositorio de Docker
# Según el linux que estes usando deberas adecuar el nombre de la distribucion, por ejemplo este es para debian:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 5. Instalar Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 6. Verificar instalación
sudo docker run hello-world

# 7. (Opcional) Usar Docker sin sudo — añadir tu usuario al grupo docker
sudo usermod -aG docker $USER
newgrp docker
# Cierra sesión y vuelve a entrar para que el cambio de grupo surta efecto
⚠️
El grupo docker equivale a root: Añadir un usuario al grupo docker le da control total sobre el daemon de Docker, lo que efectivamente equivale a privilegios de root. En un sistema de producción es un riesgo. En un laboratorio personal es conveniente para no escribir sudo en cada comando.

Conceptos clave antes de los comandos

  • Imagen: plantilla de solo lectura que define el contenido del contenedor — el sistema de archivos base, las dependencias instaladas, el comando que se ejecuta al arrancar. Se descarga de Docker Hub o se construye con un Dockerfile.
  • Contenedor: instancia en ejecución de una imagen. Puedes tener diez contenedores del mismo DVWA corriendo a la vez, cada uno independiente.
  • Dockerfile: archivo de texto con instrucciones para construir una imagen personalizada.
  • Docker Hub: registro público de imágenes. docker pull ubuntu descarga la imagen oficial de Ubuntu desde ahí.
  • Volumen: mecanismo para persistir datos fuera del contenedor — si el contenedor se destruye, los datos en el volumen sobreviven.
  • Red Docker: red virtual que conecta contenedores entre sí y con el anfitrión.

Comandos esenciales

bash — comandos Docker esenciales
## IMÁGENES

# Buscar una imagen en Docker Hub
docker search dvwa

# Descargar una imagen sin ejecutarla
docker pull ubuntu:22.04

# Listar imágenes descargadas localmente
docker images

# Eliminar una imagen
docker rmi ubuntu:22.04

## CONTENEDORES

# Ejecutar un contenedor interactivo (entra directamente a la shell)
docker run -it ubuntu:22.04 bash

# Ejecutar en segundo plano (detached)
docker run -d --name mi_contenedor nginx

# Mapear puerto del anfitrión → puerto del contenedor
docker run -d -p 8080:80 nginx
# Accesible en http://localhost:8080

# Listar contenedores en ejecución
docker ps

# Listar todos (incluidos los parados)
docker ps -a

# Ver los logs de un contenedor
docker logs mi_contenedor
docker logs -f mi_contenedor   # -f: seguir en tiempo real

# Abrir una shell en un contenedor ya en ejecución
docker exec -it mi_contenedor bash

# Parar, iniciar y eliminar un contenedor
docker stop mi_contenedor
docker start mi_contenedor
docker rm mi_contenedor

# Eliminar contenedor automáticamente al pararlo (útil en pruebas)
docker run --rm -it ubuntu:22.04 bash

## LIMPIEZA

# Eliminar todos los contenedores parados
docker container prune

# Eliminar imágenes sin usar, contenedores parados y redes sin usar
docker system prune

# Limpieza total: incluye también volúmenes
docker system prune --volumes

## INSPECCIÓN

# Ver detalles de un contenedor (IP, volúmenes, configuración)
docker inspect mi_contenedor

# Ver la IP asignada a un contenedor
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mi_contenedor

# Ver uso de recursos en tiempo real
docker stats

1.6.3 Desplegando DVWA con Docker

DVWA (Damn Vulnerable Web Application) es la aplicación web vulnerable por excelencia para aprender y practicar las vulnerabilidades más comunes: SQLi, XSS, CSRF, File Upload, Command Injection y más. En Docker tienes todo funcionando en un comando.

Panel de DVWA mostrando las categorías de vulnerabilidades

Despliegue rápido

bash — DVWA en un comando
# Desplegar DVWA con la imagen oficial
docker run -d --name dvwa -p 80:80 vulnerables/web-dvwa

# Verificar que está corriendo
docker ps

# Acceder en el navegador
# http://localhost:80
# Usuario: admin | Contraseña: password

# Ver los logs si algo falla
docker logs dvwa

# Parar y eliminar cuando termines
docker stop dvwa && docker rm dvwa
💡
Primera vez que entras: DVWA necesita inicializar la base de datos. En la primera visita verás un botón "Create / Reset Database" — pulsa en él antes de hacer login. Luego entra con admin / password y en la parte inferior del menú izquierdo puedes ajustar el nivel de seguridad (Low para empezar).

Si el puerto 80 ya está en uso

bash — cambiar puerto de DVWA
# Mapear el puerto 80 del contenedor al 8080 del anfitrión
docker run -d --name dvwa -p 8080:80 vulnerables/web-dvwa
# Acceder en: http://localhost:8080

# Ver qué puertos están ocupados en el anfitrión
ss -tlnp

Acceder desde Kali en VM

Si DVWA corre en el anfitrión y Kali está en una VM, no puedes usar localhost desde Kali. Necesitas la IP del anfitrión en la red compartida.

bash — encontrar la IP del anfitrión desde Kali
# Desde Kali, ver la puerta de enlace (es la IP del anfitrión en Host-Only o NAT)
ip route show
# La IP del gateway es la del anfitrión

# Verificar que el servicio es accesible
curl -s http://192.168.56.1:8080 | grep "title"

# Escanear con nmap para confirmar
nmap -p 8080 192.168.56.1

1.6.4 Juice Shop: el reto moderno (Node.js, JWT, GraphQL)

OWASP Juice Shop es la evolución de DVWA. Mientras DVWA simula una aplicación web antigua con vulnerabilidades clásicas, Juice Shop simula una aplicación moderna — Node.js, API REST, GraphQL, JWT, SPA con Angular. Es el estándar de referencia para practicar vulnerabilidades en arquitecturas actuales y tiene más de 100 retos catalogados por dificultad.

OWASP Juice Shop tienda y scoreboard de retos

Despliegue

bash — Juice Shop en Docker
# Desplegar Juice Shop
docker run -d --name juiceshop -p 3000:3000 bkimminich/juice-shop

# Acceder en: http://localhost:3000

# Ver los logs de arranque (Node.js tarda unos segundos)
docker logs -f juiceshop
                    

Qué tiene Juice Shop que no tiene DVWA

Categoría DVWA Juice Shop
Tecnología PHP + MySQL (stack antiguo) Node.js + SQLite + Angular (stack moderno)
API Sin API REST API REST completa + GraphQL
Autenticación Sesiones clásicas JWT (JSON Web Tokens)
Número de retos ~10 categorías 100+ retos catalogados
Dificultad Principiante — intermedio Principiante — avanzado
Scoreboard No Sí (es un reto encontrarlo)
💡
El primer reto: Juice Shop no te dice directamente dónde está el scoreboard — encontrarlo es el primer reto. Pista: está en una ruta que no aparece en el menú. Inspecciona el JavaScript de la aplicación buscando rutas internas. Cuando lo encuentres, tendrás acceso al mapa completo de los 100+ retos organizados por categoría y dificultad.

Mantener el progreso entre sesiones

Por defecto, al reiniciar el contenedor de Juice Shop el progreso se pierde — la base de datos SQLite vive dentro del contenedor. Para persistirla:

bash — Juice Shop con persistencia de datos
# Crear un directorio local para los datos
mkdir -p ~/docker-data/juiceshop

# Montar el directorio como volumen en la ruta de la base de datos
docker run -d \
    --name juiceshop \
    -p 3000:3000 \
    -v ~/docker-data/juiceshop:/juice-shop/data \
    bkimminich/juice-shop

# Ahora puedes parar y reiniciar sin perder el progreso
docker stop juiceshop
docker start juiceshop

1.6.5 docker-compose.yml: estructura mínima para un laboratorio vulnerable

Ejecutar contenedores uno a uno con docker run funciona para pruebas rápidas. Para un laboratorio con varios servicios — una app web, su base de datos, un servicio de cache, un servidor de logs — necesitas orquestarlos de forma reproducible. Para eso existe Docker Compose.

Docker Compose lee un archivo docker-compose.yml y levanta todos los servicios definidos en él con un solo comando. El estado completo del laboratorio queda documentado en un archivo de texto que puedes versionar, compartir y reproducir en cualquier máquina.

Estructura del archivo docker-compose.yml

yaml — estructura mínima docker-compose.yml
# docker-compose.yml
services:

  # Cada bloque bajo "services" es un contenedor
  nombre_del_servicio:

    # Imagen a usar (de Docker Hub)
    image: nombre_imagen:tag

    # O construir desde un Dockerfile local
    build: ./ruta/al/directorio

    # Nombre del contenedor (opcional, por defecto es carpeta_servicio_1)
    container_name: mi_contenedor

    # Mapeo de puertos: anfitrión:contenedor
    ports:
      - "8080:80"

    # Variables de entorno
    environment:
      - MYSQL_ROOT_PASSWORD=secreto
      - MYSQL_DATABASE=dvwa

    # Volúmenes: anfitrión:contenedor
    volumes:
      - ./datos:/var/lib/mysql
      - datos_nombrado:/app/data

    # Red a la que pertenece este servicio
    networks:
      - red_laboratorio

    # Depende de otro servicio (espera a que esté iniciado)
    depends_on:
      - base_de_datos

    # Política de reinicio
    restart: unless-stopped

# Definición de redes personalizadas
networks:
  red_laboratorio:
    driver: bridge

# Definición de volúmenes nombrados
volumes:
  datos_nombrado:

Laboratorio DVWA completo con Compose

DVWA necesita PHP y MySQL. Con Compose los definimos como dos servicios separados que se comunican por red interna:

yaml — docker-compose.yml para DVWA con MySQL separado
services:

  dvwa:
    image: vulnerables/web-dvwa
    container_name: dvwa
    ports:
      - "8080:80"
    environment:
      - MYSQL_HOSTNAME=db
      - MYSQL_DATABASE=dvwa
      - MYSQL_USERNAME=dvwa
      - MYSQL_PASSWORD=p@ssw0rd
    networks:
      - lab
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: mysql:5.7
    container_name: dvwa_db
    environment:
      - MYSQL_ROOT_PASSWORD=rootpass
      - MYSQL_DATABASE=dvwa
      - MYSQL_USER=dvwa
      - MYSQL_PASSWORD=p@ssw0rd
    volumes:
      - dvwa_db_data:/var/lib/mysql
    networks:
      - lab
    restart: unless-stopped

networks:
  lab:
    driver: bridge

volumes:
  dvwa_db_data:

Comandos de Docker Compose

bash — comandos docker compose
# Levantar todos los servicios (en segundo plano)
docker compose up -d

# Ver los servicios en ejecución
docker compose ps

# Ver los logs de todos los servicios
docker compose logs -f

# Ver los logs de un servicio específico
docker compose logs -f dvwa

# Parar todos los servicios (sin eliminarlos)
docker compose stop

# Parar y eliminar contenedores, redes (los volúmenes persisten)
docker compose down

# Parar, eliminar todo incluyendo volúmenes (reset total)
docker compose down -v

# Reconstruir las imágenes antes de levantar
docker compose up -d --build

# Ejecutar un comando en un servicio específico
docker compose exec dvwa bash

# Escalar un servicio (varias instancias)
docker compose up -d --scale dvwa=3
💡
docker-compose vs docker compose: El comando antiguo era docker-compose (con guion, plugin separado). En versiones modernas de Docker (v2+) está integrado como subcomando: docker compose (sin guion). Ambos funcionan igual, pero el nuevo es el estándar actual. Si tienes Docker instalado según las instrucciones del apartado 1.6.2, ya tienes el nuevo.

1.6.6 Redes internas con Docker Compose: aislamiento de clientes ficticios

El poder real de Docker Compose en un laboratorio de seguridad no es solo levantar servicios — es controlar exactamente qué puede ver quién. Con redes Docker personalizadas puedes crear topologías que simulan entornos corporativos reales: una zona DMZ, una red interna, servicios que solo son accesibles desde ciertos contenedores.

Topología de redes Docker con DMZ y red interna separadas

Cómo funcionan las redes en Docker Compose

Por defecto, Docker Compose crea una red bridge única para todos los servicios del archivo — todos se ven entre sí. Para segmentar, defines múltiples redes y asignas cada servicio a las que necesita.

Los contenedores en la misma red se resuelven por nombre de servicio. Si tienes un servicio llamado db, cualquier contenedor en la misma red puede conectarse a él con el hostname db — sin necesidad de conocer la IP.

Laboratorio con DMZ y red interna

El siguiente compose simula una arquitectura realista: un proxy nginx en la DMZ que recibe conexiones del exterior, una aplicación web en la red interna que solo es accesible desde el proxy, y una base de datos completamente aislada que solo ve la aplicación.

⚠️
Antes de levantar este laboratorio — DVWA requiere inicializar su base de datos manualmente tras el primer arranque. Accede a http://localhost/setup.php y pulsa Create / Reset Database. Sin este paso la aplicación mostrará un error de conexión. Usa la imagen ghcr.io/digininja/dvwa si la imagen vulnerables/web-dvwa da problemas de pull en tu sistema.
yaml — laboratorio con DMZ y red interna aislada
services:

  # PROXY — accesible desde el exterior, tiene acceso a la DMZ
  proxy:
    image: nginx:alpine
    container_name: proxy
    ports:
      - "80:80"        # El único servicio expuesto al anfitrión
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro  # Configuración del proxy inverso
    networks:
      - dmz            # Solo tiene acceso a la DMZ
    depends_on:
      - webapp

  # APP WEB — solo accesible desde la DMZ, no desde el exterior
  webapp:
    image: ghcr.io/digininja/dvwa
    container_name: webapp
    # Sin "ports" — no está expuesta al anfitrión
    networks:
      - dmz            # Puede recibir tráfico del proxy
      - interna        # Puede acceder a la base de datos
    depends_on:
      - db

  # BASE DE DATOS — solo accesible desde la red interna
  db:
    image: mysql:8.0
    container_name: base_datos
    environment:
      - MYSQL_ROOT_PASSWORD=rootpass
      - MYSQL_DATABASE=app
      - MYSQL_USER=appuser
      - MYSQL_PASSWORD=apppass
    networks:
      - interna        # Solo accesible desde la red interna
    volumes:
      - db_data:/var/lib/mysql

  # CLIENTE FICTICIO — simula un usuario en la red interna
  cliente:
    image: alpine
    container_name: cliente
    command: sleep infinity    # Mantenerlo vivo para conectarse manualmente
    networks:
      - interna        # Solo ve la red interna

networks:
  dmz:
    driver: bridge
  interna:
    driver: bridge
    internal: true   # Sin acceso al exterior ni al anfitrión

volumes:
  db_data:
nginx — nginx.conf (necesario para el proxy inverso)
server {
    listen 80;

    location / {
        proxy_pass         http://webapp:80;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
    }
}
💡
¿Por qué hace falta nginx.conf? — Sin configuración, nginx sirve su página de bienvenida por defecto. El archivo de arriba le indica que actúe como proxy inverso y reenvíe todas las peticiones a webapp:80. Guárdalo en el mismo directorio que el compose.yaml. El sufijo :ro en el volumen lo monta en modo solo lectura dentro del contenedor.
💡
internal: true — esta opción en la definición de red hace que Docker no añada una ruta al exterior para esa red. Los contenedores en ella no pueden salir a internet ni al anfitrión. Es el equivalente a Internal Network de VirtualBox pero para contenedores.
⚠️
Límite del aislamiento en este modelowebapp está en ambas redes (dmz e interna), lo que significa que cliente puede conectarse directamente a webapp:80 sin pasar por el proxy. En un entorno real esto se controlaría con reglas de firewall (iptables/nftables). Para este laboratorio el objetivo es demostrar el aislamiento de red, no replicar una seguridad de producción completa.

Verificar el aislamiento

bash — verificar que el aislamiento funciona
# Levantar el laboratorio
docker compose up -d

# Desde el contenedor "proxy": puede ver webapp pero NO la db
docker compose exec proxy curl -I http://webapp        # ✅ funciona (proxy reenvía a webapp)
docker compose exec proxy nc -zv base_datos 3306     # ❌ falla (red interna)

# Desde el contenedor "webapp": puede ver db
# La imagen de DVWA no incluye nc — se usa /dev/tcp de bash directamente
docker compose exec webapp bash -c "echo open > /dev/tcp/base_datos/3306 && echo conectado"   # ✅ funciona

# Desde el contenedor "cliente": solo ve la red interna
docker compose exec cliente nc -zv base_datos 3306   # ✅ funciona
docker compose exec cliente ping -c 3 8.8.8.8          # ❌ falla (internal:true)

# Ver las redes creadas por Docker
docker network ls

# Ver qué contenedores están en una red específica
# NOTA: el prefijo depende del nombre del directorio del proyecto
docker network inspect $(docker network ls --filter name=interna --format '{{.Name}}')

# Alternativa: ver las interfaces de red desde dentro del contenedor
docker compose exec proxy cat /etc/hosts

Laboratorio de múltiples targets para practicar

Una vez dominas la gestión de redes en Compose, puedes montar un laboratorio completo con varios objetivos activos simultáneamente. Este es un ejemplo real con cuatro aplicaciones vulnerables en una sola red:

⚠️
Ejecuta esto solo en una máquina local o VM sin IP pública. El compose expone MySQL en el puerto 3306 del anfitrión con la contraseña toor. Si tu equipo tiene IP pública o está en una red no controlada, cualquiera podría acceder a esa base de datos. Es intencionalmente inseguro — ese es el objetivo del laboratorio.
yaml — laboratorio multi-target
services:

  dvwa:
    image: ghcr.io/digininja/dvwa
    container_name: dvwa
    ports:
      - "8080:80"
    networks:
      - lab

  juiceshop:
    image: bkimminich/juice-shop
    container_name: juiceshop
    ports:
      - "3000:3000"
    networks:
      - lab

  webgoat:
    image: webgoat/webgoat
    container_name: webgoat
    ports:
      - "9090:8080"
    networks:
      - lab

  mysql_exposed:
    image: mysql:8.0
    container_name: mysql_target
    environment:
      - MYSQL_ROOT_PASSWORD=toor
      - MYSQL_DATABASE=secretos
    ports:
      - "3306:3306"    # MySQL expuesto — objetivo para SQLi y credenciales débiles
    networks:
      - lab

networks:
  lab:
    driver: bridge
💡
Guarda este compose como plantilla: Con docker compose up -d tienes cuatro objetivos activos en menos de un minuto. Con docker compose down lo destruyes todo limpiamente. Es la forma más eficiente de gestionar un laboratorio de hacking web — mucho más rápido que cuatro VMs separadas.