Laboratorios de hacking
Cómo escribir un buen informe de pentest
Estructure un informe de pentest y escriba un hallazgo que otra persona pueda reproducir, con evidencia, impacto, vector CVSS v4.0 y remediación.
Crea una red Docker sin acceso a internet, ejecuta OWASP Juice Shop en ella y publica una copia para el navegador solo en 127.0.0.1.
Un laboratorio es tan seguro como los caminos que entran y salen de él. Si tu aplicación vulnerable puede llegar a internet, puede hacer llamadas a lugares que nunca quisiste tocar. Si escucha en todas las interfaces de tu máquina, cualquiera conectado a la misma red Wi-Fi puede alcanzarla. Docker te da control sobre los dos caminos con unos pocos comandos. Esta guía usa OWASP Juice Shop como objetivo y recorre cada uno.
Vas a crear una red sin salida, levantar Juice Shop en ella, llamar a la aplicación desde un segundo contenedor y comprobar que esa red no llega a internet. Después ejecutarás una copia aparte para tu navegador, publicada solo en la interfaz de loopback, para que nada de tu red local pueda verla. El aislamiento es el primer control de seguridad de cualquier laboratorio, y todos los ejercicios posteriores, del escaneo a la práctica de explotación, dependen de él.
Si todavía no has ejecutado Juice Shop, empieza por OWASP Juice Shop con Docker. Este artículo da por sentados los conceptos básicos de contenedores de esa guía.
Un laboratorio de seguridad ejecuta software hecho para romperse. Juice Shop tiene fallas intencionales, y de eso se trata. El riesgo no es la aplicación en sí. El riesgo es lo que pasa cuando una falla, una mala configuración o un error de tipeo le da a la aplicación, o a una herramienta que ejecutas contra ella, un camino hacia otro lugar.
Tres casos muestran cómo sale mal:
-p 3000:3000. Docker pasa a aceptar conexiones en todas las interfaces del host, así que otros dispositivos de tu red pueden alcanzar la aplicación vulnerable.El aislamiento cubre cada caso. Una red interna no tiene ruta hacia afuera. Un puerto atado al loopback no tiene camino por la LAN. Una red dedicada separa los contenedores del laboratorio de tus otros proyectos. Ninguno de estos pasos es difícil. Solo es fácil olvidarlos cuando tienes prisa, y por eso esta guía te pide verificar cada uno.
Necesitas tres ideas antes de que los comandos tengan sentido.
Docker conecta los contenedores a redes virtuales. El driver por defecto es bridge. Cada contenedor en una red bridge recibe su propia dirección IP en una subred privada. En Linux, el bridge vive en el propio host. En Windows y macOS, Docker Desktop ejecuta el motor dentro de una máquina virtual Linux, así que el bridge vive dentro de esa VM.
Los contenedores en el bridge por defecto se alcanzan por dirección IP, pero no por nombre. Una red bridge que creas tú con docker network create resuelve los nombres de los contenedores por DNS, con un resolvedor integrado en Docker. Por eso el contenedor de herramienta de esta guía puede llamar a http://juice:3000 sin depender de una IP que cambia después de un reinicio.
Cuando creas una red con --internal, Docker restringe el acceso externo a ella. La documentación de Docker lo describe así: los contenedores de una red interna pueden comunicarse entre sí, pero no con nada de afuera. Eso incluye internet. Lo verás en el paso 4, cuando la consulta de un host externo falle.
Por defecto, el puerto de un contenedor no se publica hacia otras máquinas. La opción -p o --publish cambia eso. Mapea una dirección y un puerto del host a un puerto del contenedor, y la dirección que elijas define qué interfaces aceptan conexiones.
-p 3000:3000 escucha en todas las interfaces del host, incluida la dirección de tu LAN. -p 127.0.0.1:3000:3000 escucha solo en loopback, así que solo los programas de la misma máquina pueden conectarse. La documentación de Docker sobre publicación de puertos muestra esta misma forma. La diferencia es un prefijo, y importa.
El laboratorio tiene cinco partes:
juicenet, una red interna sin ruta de salida.juice, Juice Shop conectado solo a juicenet, sin puerto publicado.curlimages/curl en juicenet que llama a juice por nombre.example.com, lo que debe fallar.juice-local, una segunda instancia de Juice Shop publicada en 127.0.0.1:3000 para tu navegador.El contenedor juice es el que aíslas. juice-local es una copia de comodidad para navegar. Corre en la red por defecto, así que sí llega a internet. Ten eso presente y no pongas credenciales reales ni datos personales en la aplicación.
Necesitas:
docker info debe mostrar los detalles del servidor sin errores. En la máquina que usé para esta guía, Docker Engine 29.8.2 corría en Windows 11 a través de Docker Desktop.-w entre comillas simples necesitan otra forma de entrecomillar.Revisa los nombres antes de elegirlos. Otros proyectos de tu máquina pueden usar ya nombres como juice o juicenet. Ejecuta esto primero:
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'Si alguna de las listas muestra juice, juice-local o juicenet, elige otros nombres o elimina antes los viejos. No borres nada que no hayas creado tú.
Revisa también la imagen que vas a ejecutar. Juice Shop publica etiquetas de versión en su imagen:
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'Este comando falla hasta que la imagen esté local, porque el primer docker run es el que la descarga. En la máquina que usé para esta guía, la imagen informó la versión 20.3.0. La tuya puede informar otra, así que anótala.
docker network create --internal juicenetDocker imprime el ID de la nueva red. El tuyo será distinto. Esto fue lo que imprimió en la máquina de prueba:
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9Confirma que la opción se aplicó:
docker network inspect juicenet --format '{{.Internal}}'trueSi la salida es false, creaste la red sin --internal. Elimínala con docker network rm juicenet y créala de nuevo. No sigas hasta ver true, porque todas las pruebas siguientes suponen que la opción está activa.
docker run -d --name juice --network juicenet bkimminich/juice-shopCada opción tiene su función. -d ejecuta el contenedor en segundo plano. --name juice le da un nombre DNS que los demás contenedores de juicenet pueden resolver. --network juicenet lo conecta solo a esa red interna. No hay -p, así que el contenedor no publica ningún puerto hacia el host.
Docker imprime el ID del contenedor:
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8La aplicación escucha en el puerto 3000. Juice Shop tarda en arrancar después de que el contenedor sube, así que no pruebes de inmediato. Usa un bucle que verifique si un contenedor de herramienta recibe HTTP 200:
for i in $(seq 1 30); do
s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
echo "try $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneMientras la aplicación arranca, el bucle imprime 000, que significa que curl no pudo conectarse. En la máquina de prueba imprimió 000 cuatro veces y luego 200 en el quinto intento. A ti puede tomarte más o menos intentos. El bucle se rinde tras 30 intentos. Si nunca ves un 200, revisa docker logs juice para ver los errores.
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000200Lee cada parte. --rm elimina el contenedor de herramienta cuando termina. --network juicenet pone la herramienta en la misma red interna que la aplicación. curlimages/curl es una imagen pequeña que contiene curl. http://juice:3000 usa el nombre del contenedor, que Docker resuelve dentro de juicenet. La opción -s oculta la barra de progreso, -o /dev/null descarta el cuerpo de la respuesta y -w imprime solo el código de estado.
Este es el patrón que vas a reutilizar. Un escáner, un proxy o un script entra en la red del laboratorio y apunta al contenedor por nombre. Nada en tu host necesita enrutar hacia la aplicación.
La etiqueta de la imagen aquí no está fijada, así que Docker descarga lo que sea latest hoy. Para un laboratorio que piensas repetir, fija la etiqueta una vez que la hayas probado.
Una prueba que pasa solo significa algo cuando a su lado hay una prueba que falla. El paso 3 mostró que el contenedor de herramienta llega a la aplicación, así que la red funciona. Ahora verifica el otro sentido:
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"curl: (28) Resolving timed out after 5000 milliseconds
exit=28La opción -m 5 limita la petición a cinco segundos. -sS oculta la barra de progreso pero deja visibles los errores. El código de salida 28 es el código de timeout de curl. El fallo ocurre en la resolución de nombres: la consulta de example.com nunca recibe respuesta, así que curl ni siquiera abre una conexión. Es lo esperable en una red sin ruta hacia afuera.
Este es el resultado que quieres. El laboratorio llega a sus propios contenedores y no llega al exterior. Guarda la salida de los pasos 3 y 4 en tus notas de laboratorio con la fecha y la versión de Docker. Ese registro ayuda cuando vuelvas y algo haya cambiado.
Tu navegador no resuelve el nombre juice, y el contenedor juice no publica ningún puerto, así que no puedes llegar a él desde el host. Para navegar, levanta una segunda copia y publícala en loopback:
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shopLa opción -p 127.0.0.1:3000:3000 mapea el puerto 3000 de la interfaz de loopback al puerto 3000 dentro del contenedor. Esta copia usa el bridge por defecto, no juicenet, así que no está aislada de internet. Es una concesión deliberada para una copia de navegación. Si necesitas algo más estricto, tendrás que diseñar y probar un montaje aparte.
La copia tarda un poco en arrancar. Usa el mismo tipo de bucle de antes, apuntando al host:
for i in $(seq 1 30); do
s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
echo "try $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneEn la máquina de prueba la copia respondió 200 en el décimo intento. Ahora confirma dónde está atado el puerto:
docker port juice-local3000/tcp -> 127.0.0.1:3000Después, revisa el lado de la LAN. Busca la dirección IPv4 de tu máquina en la red local con ipconfig en Windows o ip addr en Linux. Luego pide la aplicación en esa dirección:
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://TU-IP-DE-LAN:3000/Reemplaza TU-IP-DE-LAN por tu dirección. En la máquina de prueba esto imprimió 000 y curl salió con código 7, lo que significa que no pudo conectarse. La misma petición a 127.0.0.1 devolvió 200. Esa diferencia es todo el sentido de atar al loopback.
Ahora abre http://127.0.0.1:3000 en el navegador. Debes ver la página de inicio de Juice Shop. Usa la aplicación solo desde esa misma máquina.
Ejecuta docker ps para ver los dos contenedores lado a lado:
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'NAMES PORTS
juice-local 127.0.0.1:3000->3000/tcp
juice 3000/tcpLa fila de juice muestra 3000/tcp sin dirección del host. El puerto existe dentro de la red, pero no está mapeado al host. La fila de juice-local muestra el enlace con el host.
Detén y elimina el laboratorio. Los contenedores tienen que salir antes que la red, porque Docker se niega a eliminar una red que aún tiene contenedores conectados:
docker rm -f juice juice-local && docker network rm juicenetjuice
juice-local
juicenetDocker imprime los nombres que eliminó. Luego confirma que no quedó nada:
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'Ambos comandos deben imprimir vacío. Los filtros coinciden con parte del nombre, así que lee la salida antes de dar por hecho que está completa, y elimina solo lo que iniciaste tú. Además, --rm solo limpia los contenedores desechables de herramienta. Los contenedores iniciados con -d se quedan hasta que los elimines, y por eso existe este paso.
Publicar sin dirección. -p 3000:3000 escucha en todas las interfaces del host. Usa el prefijo 127.0.0.1: en cualquier aplicación del laboratorio que abras desde la misma máquina, y revisa el resultado con docker port.
Olvidar --internal. La red funciona sin la opción y nada te avisa. Ejecuta docker network inspect juicenet --format '{{.Internal}}' y busca true.
Conectar la aplicación a la red por defecto. Un contenedor iniciado sin --network entra al bridge por defecto, que llega al exterior. Si la aplicación debe quedar aislada, nombra la red cada vez.
Confiar en una prueba que falló. Un error en el paso 4 puede venir del DNS, de un proxy, de una VPN o de un error de tipeo. El paso 3 es el control que muestra que la prueba mide lo correcto.
Usar el nombre desde el host. El nombre juice solo se resuelve para contenedores de juicenet. Tu navegador no puede usarlo. Por eso existe la copia para el navegador.
Dejar contenedores en ejecución. Un contenedor en segundo plano sigue consumiendo recursos y mantiene el puerto ocupado. Termina cada sesión con el paso de limpieza y ejecuta docker ps después.
La opción de red interna es un control, y tiene límites.
No impide que una herramienta en juicenet llegue a todos los demás contenedores de esa red. Los contenedores de una misma red pueden hablar entre sí, y eso es lo que hace funcionar el paso 3. Mantén los servicios sin relación fuera de la red del laboratorio.
La publicación en loopback te protege de otras máquinas de tu LAN. No te protege de programas que ya corren en tu propia computadora, porque esos también pueden conectarse a 127.0.0.1.
La prueba verifica un nombre y un host externo. No demuestra que todos los caminos de salida estén cerrados. El comportamiento de Docker puede variar entre Docker Desktop, motores Linux y configuraciones rootless, así que repite las verificaciones en la máquina que realmente usas.
Por último, este laboratorio solo muestra que el aislamiento funciona. No muestra que Juice Shop sea seguro de exponer. No lo es, y esa es la razón para mantenerlo aislado.
Cuando el laboratorio funcione, unos pocos cambios pequeños lo hacen más útil:
juicenet, como una base de datos, y confirma que la aplicación lo alcanza por nombre. Así se arman los laboratorios de varios contenedores.juicenet es un buen primer objetivo, porque controlas todos sus contenedores.Laboratorios de hacking
Estructure un informe de pentest y escriba un hallazgo que otra persona pueda reproducir, con evidencia, impacto, vector CVSS v4.0 y remediación.
Laboratorios de hacking
Monta un laboratorio que puedes atacar: archivo de alcance, puertos de Docker en loopback, red aislada, cuaderno de notas y desmontaje limpio.
Laboratorios de hacking
Ejecuta OWASP Juice Shop en un contenedor Docker limitado a tu equipo, comprueba que responde y detenlo sin dejar nada en ejecución.