Saltar al contenido
Nuevo

DeepSeek V4.1 Flash gratis por 7 días: 10M tokens al día tras cualquier recarga. Hasta el 30/11.

Cómo funciona

Lo básico de OWASP ZAP contra tu Juice Shop local

Ejecuta el escaneo pasivo de ZAP contra Juice Shop en tu máquina, lee el informe y verifica una alerta a mano antes de confiar en ella.

Principiante13 min de lecturaPor NotHoly

Qué es ZAP y dónde se sitúa

El Zed Attack Proxy, al que casi todos llaman ZAP, es una herramienta gratuita para encontrar problemas en aplicaciones web. Hace dos cosas que importan para esta guía. Funciona como un proxy interceptor, de modo que puedes leer cada petición que envía tu navegador y cada respuesta que recibe. Además, ejecuta escáneres que revisan ese tráfico en busca de patrones débiles.

Un proxy es un intermediario. Normalmente, el navegador envía la petición directamente a 127.0.0.1:3000. Cuando pasa por ZAP, el navegador envía la petición a ZAP. ZAP la reenvía a la aplicación, recibe la respuesta, guarda el par en su historial y devuelve la respuesta al navegador. La aplicación no nota ningún cambio. Tú solo obtienes una copia de la conversación.

Esa posición es lo que hace útil la herramienta. Un escáner ve lo que el servidor devuelve. Un proxy te muestra lo que el navegador hizo en realidad, incluidas las cabeceras, las cookies, las redirecciones y el JavaScript que cargó la página. Muchos problemas web solo aparecen cuando comparas esas dos vistas.

Escaneo pasivo y activo

ZAP tiene dos tipos de escaneo, y la diferencia determina qué puedes ejecutar y dónde.

El escaneo pasivo lee el tráfico que ya pasó por ZAP. Cada respuesta se revisa en busca de cosas como la cabecera Content-Security-Policy ausente, cookies sin la marca HttpOnly o trazas de pila en páginas de error. Las reglas pasivas no envían cadenas de ataque. Solo observan lo que regresó.

El escaneo activo envía peticiones nuevas, diseñadas para provocar una reacción. Cambia parámetros, envía entradas inusuales y observa cómo responde la aplicación. La mayoría de los hallazgos serios salen de este tipo de prueba, y también es el tipo que se comporta como un ataque. Ejecútalo solo contra un laboratorio tuyo, o contra un sistema donde tengas permiso por escrito y un alcance definido.

El baseline que usa esta guía es pasivo. La documentación de ZAP lo describe como un script que ejecuta el spider contra el objetivo durante un minuto por defecto, espera a que terminen las reglas pasivas y luego genera el informe. No lanza cargas de ataque, así que es un primer paso razonable en tu laboratorio.

Por qué una alerta es una pista, no una prueba

Cada alerta de ZAP es una regla que coincidió. Una regla puede encontrar una cabecera que falta, un patrón en el cuerpo de la respuesta, o una combinación de ambos. No sabe para qué sirve tu aplicación, qué datos protege ni si un atacante puede llegar a la ruta que importa. Una alerta te dice dónde mirar. No confirma que una debilidad se pueda explotar.

Hay dos tipos de error que aparecen todo el tiempo. Algunas alertas son falsos positivos: la regla coincidió con algo inofensivo. Otros problemas reales pasan desapercibidos, porque el escáner nunca llegó a la página o nunca envió la entrada que dispara el fallo. Lo que convierte una pista en un hallazgo es un paso que puedas repetir tú mismo: una petición que enviaste, la respuesta que viste y una explicación sencilla de por qué importa. Mientras no tengas las tres cosas, la alerta se queda en una lista marcada como "por verificar".

Ten esa regla presente durante el resto de la guía. El escáner te da un punto de partida, y el trabajo de verificación es tuyo.

Antes de empezar

Necesitas cuatro cosas:

  • Docker. Docker Desktop en Windows o macOS sirve, y Docker Engine en Linux también. Confírmalo con docker version.
  • Una máquina tuya, donde ejecutes solo el laboratorio que se describe aquí.
  • Una terminal. Los ejemplos usan una shell de tipo POSIX. En Windows, yo ejecuté los comandos en Git Bash.
  • Un navegador, si quieres el paso opcional con ZAP de escritorio.

El laboratorio se queda en tu máquina. Publica su puerto solo en 127.0.0.1. Un puerto publicado sin ese prefijo puede escuchar en todas las interfaces de red, y Juice Shop está hecho para que lo rompan.

Paso 1: levantar el laboratorio

Inicia Juice Shop en segundo plano, con un nombre que puedas usar después:

bash
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shop

La opción -d ejecuta el contenedor en segundo plano. Dejé fuera --rm a propósito, para que el contenedor siga existiendo hasta que lo detengas y lo elimines por nombre. El prefijo 127.0.0.1: mantiene el puerto fuera de tu red.

Comprueba que responde:

bash
curl -i http://127.0.0.1:3000/

En mi máquina, las primeras líneas de la respuesta fueron estas. Quité la barra de progreso que curl muestra cuando no se usa -s:

text
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Feature-Policy: payment 'self'
X-Recruiting: /#/jobs
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Sun, 08 Nov 2026 09:06:18 GMT
ETag: W/"2700-1a1ba6896a5"
Content-Type: text/html; charset=UTF-8
Content-Length: 9984

Tus cabeceras y fechas serán distintas. Anota también la versión de Juice Shop, porque la aplicación cambia entre versiones. Este endpoint la devuelve:

bash
curl -s http://127.0.0.1:3000/rest/admin/application-version

En mi máquina imprimió {"version":"20.3.0"}. Guarda ese número en tus notas, junto a los resultados del escaneo.

Paso 2: ejecutar el escaneo baseline

Lanza el escáner desde la carpeta donde quieres que aparezca el informe. Este es el comando para Docker Desktop:

bash
docker run --rm -t -v "$(pwd)":/zap/wrk ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html

Cada parte cumple una función:

  • --rm elimina el contenedor del escáner cuando termina el escaneo.
  • -v "$(pwd)":/zap/wrk monta tu carpeta actual dentro del contenedor, para que el informe se escriba en tu disco.
  • -t http://host.docker.internal:3000 es el objetivo. Desde dentro de un contenedor, host.docker.internal apunta a tu computadora, donde escucha el laboratorio.
  • -r zap-report.html escribe el informe HTML completo en la carpeta montada.

En Linux, el nombre host.docker.internal no existe por defecto en Docker. Agrega un mapeo de host al mismo comando:

bash
docker run --rm -t --add-host=host.docker.internal:host-gateway -v "$(pwd)":/zap/wrk ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html

Yo solo probé la forma de Docker Desktop. En Linux, un puerto publicado en 127.0.0.1 puede no aceptar conexiones que llegan por la dirección del puente de Docker. Si el escáner muestra errores de conexión, lee las primeras líneas de su salida antes de cambiar cualquier otra cosa.

Dos problemas con los que me topé

El primer intento falló sin escanear nada. La consola mostró A file based option has been specified but the directory '/zap/wrk' is not mounted, luego imprimió el texto de uso y salió con código 3. El montaje no funcionó en mi sesión de Git Bash, y no averigüé por qué. Ejecutar el comando con MSYS_NO_PATHCONV=1 y la ruta completa de Windows en la opción -v lo resolvió:

bash
MSYS_NO_PATHCONV=1 docker run --rm -t -v "D:/ruta/a/tu/carpeta:/zap/wrk" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html

Reemplaza la ruta por tu carpeta. La primera ejecución también descarga la imagen de ZAP, y en mi conexión tardó casi doce minutos. Las siguientes empiezan en segundos.

Cómo leer la salida de la consola

El escáner imprime una línea por regla. Esta es la parte que importa, de mi ejecución, con la larga lista de reglas aprobadas acortada:

text
Using the Automation Framework
Total of 116 URLs
PASS: Vulnerable JS Library (Powered by Retire.js) [10003]
PASS: Cookie No HttpOnly Flag [10010]
...
PASS: CSP [10055]
...
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 5
WARN-NEW: Cross-Domain Misconfiguration [10098] x 5
WARN-NEW: Modern Web Application [10109] x 5
FAIL-NEW: 0	FAIL-INPROG: 0	WARN-NEW: 3	WARN-INPROG: 0	INFO: 0	IGNORE: 0	PASS: 59

WARN-NEW significa que una regla marcó algo que el escaneo todavía no había visto. PASS significa que la regla se ejecutó y no encontró nada. El número después del nombre de la regla es su identificador en ZAP, que puedes buscar en la documentación.

El comando salió con código 2 en mi ejecución. La documentación del baseline lista los códigos de salida: 0 es éxito, 1 es al menos un FAIL, 2 es al menos un WARN y ningún FAIL, y 3 es cualquier otro fallo. Un código 2 es normal aquí. Solo indica que el escáner encontró algo que leer.

Paso 3: leer el informe

Abre zap-report.html en el navegador. El resumen de la parte superior de mi informe mostró High 0, Medium 2, Low 0 e Informational 1. Tres alertas formaban toda la lista:

  • Content Security Policy (CSP) Header Not Set, Medium. La respuesta no envía la cabecera Content-Security-Policy, que le indica al navegador de qué orígenes puede cargar scripts y frames.
  • Cross-Domain Misconfiguration, Medium. La respuesta incluye Access-Control-Allow-Origin: *, lo que permite que cualquier sitio lea la respuesta en el navegador.
  • Modern Web Application, Informational. ZAP notó que el sitio se comporta como una aplicación de página única. Esto describe la aplicación. No es un defecto.

Cada alerta tiene las mismas partes: una descripción, las URL donde se disparó, la evidencia que vio ZAP, algunas notas y una sugerencia de corrección. Lee primero la evidencia y la URL. La descripción explica qué significa la regla en general. La evidencia muestra qué pasó en esta respuesta.

No ordenes el informe solo por la etiqueta de riesgo. ZAP asigna el nivel según su regla, no según los datos de tu aplicación. Un Medium en una página sin datos sensibles puede importar menos que un Low en un endpoint que maneja cuentas.

Paso 4: verificar una alerta a mano

Elige la alerta de CSP. Su evidencia es una ausencia, así que la prueba es sencilla: pide la página, busca la cabecera y registra el resultado. Este comando imprime el número de líneas que coinciden:

bash
curl -s -i http://127.0.0.1:3000/ -o /dev/null -D - | grep -i -c '^content-security-policy'

En mi máquina imprimió 0. La lista completa de cabeceras que capturé antes tampoco tenía ninguna línea Content-Security-Policy. La alerta coincide con lo que envía el servidor.

Ahora la segunda alerta. La evidencia es la línea Access-Control-Allow-Origin: * que ya viste en la salida de curl -i. Tu verificación debe responder otra pregunta: ¿esa respuesta contiene algo que un sitio de terceros no debería leer? La página raíz del laboratorio es HTML público, así que la cabecera en sí no es el problema. El texto de ZAP dice lo mismo. Los navegadores no dejan que una página de terceros lea respuestas autenticadas de esa manera. La alerta se vuelve seria solo cuando un endpoint que devuelve datos privados también envía un origen comodín. Si encuentras un endpoint así, pruébalo con una cuenta de tu laboratorio y registra lo que devuelve.

La tercera alerta no tiene nada que demostrar. Describe el tipo de aplicación. Déjala en el informe como contexto y sigue adelante.

Registra cada resultado verificado en una tabla o en un archivo de texto con cuatro columnas: la alerta, la petición que enviaste, lo que observaste y tu veredicto. El veredicto puede ser "confirmado", "no reproducido" o "necesita más contexto". Esas tres respuestas cubren la mayor parte de lo que harás con la salida de un escáner.

Paso 5: pasar el navegador por ZAP

Este paso te permite observar peticiones reales, y es lo que hace útil al proxy más allá del escaneo. Puedes hacerlo con la aplicación de escritorio o con un contenedor que ejecute ZAP en modo daemon. Describo las dos. Yo ejecuté la versión daemon. No ejecuté el recorrido de escritorio, así que no hay salida de él abajo.

Versión de escritorio (no ejecutada por mí)

Instala ZAP desde el sitio oficial. La página de primeros pasos enlaza al instalador actual. Ábrelo y elige Manual Explore, que inicia un navegador que ya envía su tráfico por ZAP. Escribe http://127.0.0.1:3000 y navega por la página principal. Luego abre la pestaña History. Cada fila es un par de petición y respuesta, con el método, la URL y el código de estado. Compara esas filas con lo que ves en las herramientas de desarrollo de tu propio navegador. Las diferencias muestran lo que el navegador envió y no esperabas.

Versión daemon (lo que ejecuté)

Inicia ZAP como proxy en segundo plano, en el puerto 8080:

bash
docker run -d --name zap-proxy-lab -p 127.0.0.1:8080:8080 ghcr.io/zaproxy/zaproxy:stable zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.disablekey=true -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true

La opción api.disablekey=true desactiva la comprobación de clave de la API, para que los comandos de abajo funcionen sin configuración extra. Mantén el puerto publicado en 127.0.0.1 y elimina el contenedor cuando termines. Quien pueda llegar a la API puede controlar ZAP.

Confirma que la API responde:

bash
curl -s http://127.0.0.1:8080/JSON/core/view/version/

Salida en mi máquina:

text
{"version":"2.17.0"}

Envía dos peticiones a través del proxy. La opción -x le dice a curl que use ZAP. El nombre del objetivo es host.docker.internal porque ZAP corre dentro de Docker, y el nombre que usa para tu máquina es el que resuelve desde allí:

bash
curl -s -x http://127.0.0.1:8080 -o /dev/null -w 'proxied root http=%{http_code}\n' http://host.docker.internal:3000/
curl -s -x http://127.0.0.1:8080 -o /dev/null -w 'proxied api http=%{http_code}\n' "http://host.docker.internal:3000/rest/products/search?q=apple"

Salida:

text
proxied root http=200
proxied api http=200

Pregunta a ZAP cuántos mensajes ha registrado:

bash
curl -s "http://127.0.0.1:8080/JSON/core/view/numberOfMessages/"

En mi máquina devolvió {"numberOfMessages":"5"}. Luego lista los mensajes con un one-liner corto de Python que imprime la línea de la petición y la línea de estado de la respuesta:

bash
curl -s "http://127.0.0.1:8080/JSON/core/view/messages/?start=1&count=5" | python -c "import json,sys; d=json.load(sys.stdin); [print(m['requestHeader'].splitlines()[0], '->', m['responseHeader'].splitlines()[0]) for m in d['messages']]"

Salida:

text
GET http://host.docker.internal:3000/ HTTP/1.1 -> HTTP/1.1 200 OK
GET http://host.docker.internal:3000 HTTP/1.1 -> HTTP/1.0 0
GET http://host.docker.internal:3000/rest/products/search?q=apple HTTP/1.1 -> HTTP/1.1 200 OK
GET http://host.docker.internal:3000/rest HTTP/1.1 -> HTTP/1.0 0
GET http://host.docker.internal:3000/rest/products HTTP/1.1 -> HTTP/1.0 0

Envié dos peticiones a través del proxy, pero el historial tiene cinco entradas. Tres de ellas no tienen respuesta, lo que aparece como estado 0. No rastreé de dónde salieron. No las leas como evidencia sobre Juice Shop. Por eso conviene revisar el historial tú mismo en lugar de confiar en un conteo. Si tu lista es distinta, anota lo que ves y pregunta por qué antes de sacar una conclusión.

Detén y elimina el proxy cuando termines:

bash
docker rm -f zap-proxy-lab

Errores comunes

Apuntar el escáner a un objetivo que no te pertenece. La opción -t acepta cualquier URL, y el escáner enviará peticiones a ella. Mantén el objetivo en el nombre de tu propia máquina dentro del laboratorio.

Publicar el laboratorio en todas las interfaces. -p 3000:3000 sin dirección escucha en todas las interfaces. Escribe siempre 127.0.0.1:3000:3000.

Tratar las advertencias como una puntuación. Tres advertencias indican que coincidieron tres reglas. Verifica cada una antes de contarla.

Omitir la versión. Juice Shop cambia entre versiones, y las reglas de ZAP también. Escribe las dos versiones junto al informe. La versión que usé fue Juice Shop 20.3.0 y ZAP 2.17.0.

Dejar el proxy encendido. El daemon tiene desactivada la clave de la API. Elimina el contenedor al final de cada sesión.

Leer la columna de estado como prueba. Una fila con estado 0 significa que no se registró ninguna respuesta. No es un hallazgo.

Montar la carpeta equivocada. Si falta el informe, revisa primero el montaje de -v. El escáner puede ejecutarse con éxito y aun así no escribir nada donde esperas.

Cómo seguir

Mantén un archivo de notas para el laboratorio. Cada línea debe tener una alerta, la petición que la probó, lo que viste y tu veredicto. Después de unos cuantos escaneos, ese archivo se convierte en un registro de lo que la herramienta encontró, lo que confirmaste y lo que se equivocó.

Lee la guía de instalación de Juice Shop si aún no la leíste, porque este artículo supone el mismo laboratorio. El siguiente artículo usa ese mismo laboratorio para explicar la inyección SQL. Es un buen lugar para ver por qué una alerta de escáner necesita revisión manual: el fallo está en cómo la entrada llega a una consulta, y una herramienta solo puede sugerirlo.

La documentación de ZAP cubre contextos, autenticación y escaneo activo. Esos temas importan cuando ya puedes distinguir un problema confirmado de una regla que coincidió. Estúdialos en el laboratorio, con permiso, y no los lleves a otros sistemas.

El proyecto Juice Shop guarda la lista de desafíos y las notas de instalación en su repositorio de GitHub. Revisa su página de releases para conocer la versión actual antes de descargar la imagen.

Fuentes

Fuentes

  1. ZAP documentation
  2. ZAP Baseline Scan (Docker)
  3. ZAP getting started
  4. OWASP Juice Shop (GitHub)

Más en Herramientas de seguridad

Herramientas de seguridad

Nmap básico en tu propio laboratorio

Nmap lista los puertos abiertos de tu propia máquina. Revisa escaneos connect y SYN, detección de servicios y cómo ligar cada puerto a su proceso.

Principiante13 min de lectura