Skip to main content

Command Palette

Search for a command to run...

Montando un WAF en 10 minutos: atacando una app antes y después de Aegis

SQLi, XSS y path traversal contra la misma app: 4/4 explotan sin WAF, 4/4 bloqueados con Aegis — con evidencia y repo para probarlo.

Updated
•9 min read•View as Markdown
Montando un WAF en 10 minutos: atacando una app antes y después de Aegis
S

I have a bachelor's degree in Technology from the University of Palermo, a Master in Information Security from the University of Murcia and different certifications such as CISSP | CISM | CDPSE | CCSK | CSX | MCSA | SMAC™️ | DSOE | DEPC | CSFPC | CSFPC | 5x AWS Certified.

He is currently CISO at Klar, a Mexican Fintech. He was fortunate to be awarded as CISO of the Year in Argentina in 2021 and was among the Top 100 CISO's in the World in 2022.

A lover of new technologies, he has developed a career in DevSecOps and Cloud Security at Eko Party, the largest security conference in Latin America.

Todos sabemos en teoría qué hace un Web Application Firewall: se pone delante de tu aplicación y filtra el tráfico malicioso antes de que llegue al backend. Pero "en teoría" no convence a nadie. En este post lo vamos a ver: levantamos una app deliberadamente vulnerable, la atacamos con SQL injection, XSS y path traversal, y después pusimos Aegis —un WAF open source escrito en Go— delante de ella y repetimos exactamente los mismos ataques.

Spoiler: el antes y el después se explican solos.


⚠️ Disclaimer. Todo esto corre en un laboratorio local, contra una app de juguete que escribí para este post. Atacar sistemas que no son tuyos es ilegal. Esto es material educativo de defensa.


Qué vamos a montar

La arquitectura de la POC es simple. La misma app vulnerable, expuesta por dos caminos:

Arquitectura del laboratorio: la misma app, dos caminos — acceso directo vs. detrás del WAF

Aegis es un reverse proxy + WAF que trae el OWASP Core Rule Set con anomaly scoring. Es decir, no bloquea por una sola coincidencia tonta: va sumando un puntaje de amenaza según las reglas que matchean y recién bloquea al pasar un umbral. Eso reduce los falsos positivos sobre tráfico legítimo.

La app vulnerable la escribí en Python (Flask), con cuatro agujeros a propósito:

Endpoint Vulnerabilidad
/login SQL Injection (bypass de autenticación)
/search SQL Injection (UNION, exfiltración de datos)
/greet XSS reflejado
/download Path Traversal
Home de DemoShop con los cuatro formularios vulnerables

Paso 1 — Levantar el laboratorio

Todo el stack vive en un docker-compose.yml. Un solo comando:

docker compose up -d

Esto levanta cuatro contenedores: la app vulnerable, Aegis, y las dos dependencias que este build de Aegis necesita (PostgreSQL para configuración, ClickHouse para analytics en tiempo real).

Esperá ~20 segundos a que las bases pasen el healthcheck y Aegis termine de arrancar.

docker compose ps: los cuatro contenedores del lab, bases healthy

Paso 2 — Configurar la ruta en Aegis

Aegis todavía no sabe a quién tiene que proteger. Hay que decírselo — desde su consola web (http://localhost:8081, usuario admin, contraseña admin) o por su API, que es como lo hace este lab para que sea reproducible. Creá una ruta nueva:

  • Hostname / path: / (todo el tráfico)

  • Upstream: http://demoshop:5000

  • WAF: activado, modo bloqueo (no solo detección)

A partir de acá, lo que entra por :8080 pasa por el filtro antes de tocar la app. El modo bloqueo no es un toggle de la ruta: viene del config.yaml (sections.waf_core.mode: blocking), y lo que la consola muestra activo es el OWASP CRS con sus grupos de reglas:

Managed Rulesets: los grupos del OWASP CRS activos (Attack Protection, Protocol Enforcement, etc.)

La ruta creada en el paso anterior, vista desde Network → Routing:

Consola de Aegis: la ruta demoshop apuntando al origin, Enabled

Paso 3 — El "ANTES": atacar sin protección

Escribí un script en Python que lanza la misma batería de ataques contra ambos destinos y arma la comparación. Primero miremos solo la columna "sin WAF".

python attack.py

SQL Injection — bypass de login

El clásico. La app arma la query concatenando lo que le mandás a ciegas:

SELECT id, username, role FROM users
WHERE username = '{username}' AND password = '{password}'

Acá hay un detalle que a muchos les explota en la cara la primera vez: en SQL el AND se evalúa antes que el OR. Si ponés solo ' OR '1'='1 en el usuario y cualquier cosa en la password, la query queda:

WHERE username = '' OR ('1'='1' AND password = 'loquesea')

El '1'='1' solo "salva" la condición de la password — y como loquesea no es la password de nadie, no matchea nada y te come un "Credenciales inválidas". Estas son las tres formas que sí funcionan (probadas una por una contra :5000):

Opción 1 — comentar el resto de la query (la más limpia):

curl -s --get "http://localhost:5000/login" \
  --data-urlencode "username=' OR '1'='1'--" \
  --data-urlencode "password=loquesea"

El -- convierte todo lo que sigue en un comentario, así que la password deja de chequearse. La query efectiva es WHERE username = '' OR '1'='1', siempre verdadera. Entramos sin credenciales y la app nos lista todos los usuarios con su rol:

Bypass con ' OR '1'='1'--: Login OK listando a admin, alice y bob

Opción 2 — entrar directo como admin, sin password:

curl -s --get "http://localhost:5000/login" \
  --data-urlencode "username=admin'--" \
  --data-urlencode "password=loquesea"

Cierra el string en admin y comenta el chequeo de password: la query efectiva es WHERE username = 'admin'. Más discreto que la opción 1, porque devuelve una sola fila en vez de todas:

Bypass con admin'--: Login OK entrando directo como admin

Opción 3 — el truco en los dos campos:

curl -s --get "http://localhost:5000/login" \
  --data-urlencode "username=' OR '1'='1" \
  --data-urlencode "password=' OR '1'='1"

Al repetir el payload en la password, el OR final salva la query aunque falle el del medio. Es la versión que aguanta forms donde no sabés si el -- va a sobrevivir al viaje:

Bypass con ' OR '1'='1 en ambos campos: Login OK listando todos los usuarios

En las tres capturas fijate en el pie: la app misma te muestra la query completa que ejecutó. Eso es oro para el post, porque deja en evidencia que el problema no es el payload, es la concatenación.

SQL Injection — exfiltración con UNION

curl -s --get "http://localhost:5000/search" \
  --data-urlencode "q=x' UNION SELECT username, password FROM users-- -"

La app nos devuelve, mezclado con los productos, el usuario y la contraseña del admin. Datos que jamás deberían salir de la tabla users:

El buscador devolviendo admin — $S3cr3t-Admin-Pass! gracias al UNION

XSS reflejado

curl -s --get "http://localhost:5000/greet" \
  --data-urlencode "name=<script>alert('xss')</script>"

La app refleja el parámetro sin escaparlo, así que el navegador ejecuta el script. Lo comprobado con un navegador real (Playwright/Chromium): al entrar a /greet con ese payload, el navegador dispara el alert('xss'). Y con un payload visible en lugar del script, se ve claramente lo que una víctima vería — HTML arbitrario inyectado en la página:

HTML inyectado renderizado en /greet: "PWNED — XSS reflejado"

En un escenario real esto es robo de sesión, keylogging, defacement.

Path Traversal

curl -s --get "http://localhost:5000/download" \
  --data-urlencode "file=../../../../etc/passwd"

La app concatena el nombre del archivo sin sanitizar, así que nos salimos de su carpeta y leemos un archivo del sistema:

El /etc/passwd del contenedor servido por la app como text/plain

Cuatro de cuatro. La app está completamente a merced del atacante.


Paso 4 — El "DESPUÉS": los mismos ataques, ahora con Aegis

Ahora apuntamos el script al puerto del WAF (:8080) y repetimos exactamente lo mismo. No cambiamos ni una coma de los payloads.

python attack.py --md resultados.md

La salida de la corrida real:

attack.py: 4/4 explotan sin WAF, 4/4 bloqueados con Aegis, trafico legitimo pasa

El resultado de la tabla comparativa (generada por el propio script):

Ataque Categoría Sin WAF Con WAF (Aegis)
SQLi — bypass de login SQL Injection Explota ❌ (HTTP 200) Bloquea ✅ (HTTP 403)
SQLi — UNION en búsqueda SQL Injection Explota ❌ (HTTP 200) Bloquea ✅ (HTTP 403)
XSS reflejado Cross-Site Scripting Explota ❌ (HTTP 200) Bloquea ✅ (HTTP 403)
Path Traversal — /etc/passwd Path Traversal Explota ❌ (HTTP 200) Bloquea ✅ (HTTP 403)
Tráfico legítimo (control) Benigno Pasa ✅ Pasa ✅

La última fila es la clave y la que suele faltar en estas demos: el tráfico legítimo sigue pasando. Un WAF que bloquea todo no sirve; la gracia es cortar el ataque sin romper al usuario real. Buscá un producto normal y la respuesta llega perfecta a través del WAF.

Y lo mejor: el WAF no solo corta — ve todo. En la consola de Aegis, Traffic Events muestra el tráfico agregado en tiempo real (rutas, paths, distribución 2xx/4xx):

Panel Traffic Events de Aegis con el trafico del laboratorio en tiempo real

Y Threat Logs es la vista forense: cada request bloqueada con su IP, path, status y veredicto. Ojo al número de arriba: 11 eventos, 11 bloqueados, 100% de tasa de bloqueo:

Threat Logs: forensic stream con los ataques bloqueados, 100% block rate

Qué está pasando por debajo

Cuando mandás ' UNION SELECT ..., el OWASP CRS lo reconoce como un patrón de inyección SQL y le suma puntos al "anomaly score" de esa request. Al cruzar el umbral configurado, Aegis responde 403 Forbidden y la request nunca llega a tu app en Python. Tu código vulnerable sigue estando vulnerable — pero el atacante nunca lo alcanza.

Esa es la idea central del WAF como capa de defensa: no arregla tu código, pero te compra tiempo mientras lo arreglás, y te da visibilidad de quién te está atacando y cómo.

Importante: un WAF es defensa en profundidad, no un reemplazo de escribir código seguro. La solución real al SQLi es parametrizar las queries; al XSS, escapar la salida; al traversal, validar la ruta. El WAF es el cinturón de seguridad, no la excusa para manejar mal.


Cerrando

En diez minutos pasamos de una app indefensa a una app detrás de un WAF que frena los ataques más comunes del OWASP Top 10, sin tocar una línea del código vulnerable. El antes y el después hablan por sí solos.

El código de toda la POC (app vulnerable, compose y script de ataque) está en GitHub: docker compose up -d y en diez minutos tenés el mismo laboratorio corriendo. El WAF que usamos es Aegis: open source, escrito en Go, con el OWASP Core Rule Set integrado.