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.
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
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
curl -fsSL https://download.docker.com/linux/debian/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgEs lo mismo que hacer:
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgSe usa por comodidad cuando la línea es muy larga.
# 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
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 ubuntudescarga 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
## 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.
Despliegue rápido
# 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
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
# 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.
# 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.
Despliegue
# 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) |
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:
# 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
# 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:
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
# 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 (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.
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.
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.
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:
server {
listen 80;
location / {
proxy_pass http://webapp:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
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.
webapp 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
# 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:
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.
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
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.