Panorama#

Los tres capítulos web anteriores atacan el web clásico que documentó The Web Application Hacker’s Handbook en 2011: una aplicación monolítica, un navegador, un servidor, y una same-origin policy que aísla un sitio de otro. Ese modelo tenía fronteras de confianza nítidas —el login separa anónimo de autenticado, el origen separa un dominio de otro, el intérprete separa dato de código—, y las clases de 3.2 a 3.4 son maneras de romperlas. Pero el web dejó de ser monolítico. Las aplicaciones modernas delegan la identidad a un tercero, descentralizan el estado en tokens autocontenidos, hablan cross-origin por diseño, empujan lógica y datos al cliente, y procesan input rico a través de motores muy potentes.

Bajo la variedad de clases de este capítulo hay una sola idea: cada comodidad del web moderno reubica una frontera de confianza, y la vulnerabilidad vive en la frontera que se dejó de validar. Donde antes la aplicación validaba, ahora hay un límite nuevo —y a menudo nadie lo custodia—. Las clases se agrupan en dos familias que son los sucesores directos de los capítulos previos:

  1. Identidad y confianza delegadas (el sucesor de 3.3 y 3.4). La app ya no autentica ni guarda la sesión por sí sola: delega el login a Google o Facebook (OAuth), lleva el estado en un token autocontenido que el servidor no recuerda (JWT), y abre el origen para conversar con otros dominios (CORS, postMessage, WebSockets). Cada delegación crea una frontera nueva —el redirect_uri, la firma del token, el Origin— que es el punto exacto del ataque.
  2. Input rico y motores potentes (el sucesor de 3.2). La misma tesis de la inyección —romper la frontera entre dato y código— pero el intérprete ya no es una base de datos SQL: es un motor de plantillas (SSTI), un deserializador (object injection), un parser de archivos (file upload) o el propio stack HTTP del servidor convertido en cliente (SSRF). El resultado, casi siempre, es RCE directo en el servidor.
flowchart TD
  MOD["Web moderno:\ncada comodidad mueve\nuna frontera de confianza"] --> F1["Familia A —\nidentidad/confianza delegada"]
  MOD --> F2["Familia B —\ninput rico a motores potentes"]
  F1 --> OAUTH["OAuth: frontera = redirect_uri"]
  F1 --> JWT["JWT: frontera = la firma"]
  F1 --> CORS["CORS / postMessage / WS:\nfrontera = el Origin"]
  F2 --> SSTI["SSTI: motor de plantillas"]
  F2 --> DESER["Deserialización: unserialize()"]
  F2 --> UP["File upload: parser de archivos"]
  F2 --> SSRF["SSRF: el server como cliente HTTP"]
  OAUTH --> ATO["Account takeover\n/ robo de token"]
  JWT --> ATO
  CORS --> ATO
  SSTI --> RCE["RCE server-side"]
  DESER --> RCE
  UP --> RCE
  SSRF --> RCE

El capítulo recorre las dos familias y cierra con la lente púrpura que las une. Todo se apoya en el mapa de superficie de 3.1 · Metodología: cada botón de “iniciar sesión con Google”, cada token en Authorization: Bearer, cada campo que acepta un URL o un archivo, y cada postMessage en el JavaScript del cliente es una frontera a probar.

Familia A · Identidad delegada: OAuth 2.0#

OAuth 2.0 (RFC 6749) resuelve un problema real del modelo clásico: para que una app de un tercero acceda a los datos de un usuario, antes había que darle la contraseña —que podía guardarla en claro, obtenía acceso total y no selectivo, y no había forma de revocarle acceso a uno sin revocárselo a todos—. OAuth lo reemplaza con cuatro roles: el resource owner (el usuario), el client (la app del tercero, con un client_id público y un client_secret privado), el resource server (donde viven los datos, protegidos por un access token) y el authorization server (que emite el token). El usuario concede a la app un acceso definido por scope, sin entregar su contraseña.

La frontera de confianza es el redirect_uri: la URI pre-configurada a la que el authorization server redirige al terminar el flow, entregando el token o el código. Robar esa redirección es robar el token. Y lo que la hace tan potente es una comodidad del protocolo: una vez que el usuario concedió acceso a una app, el authorization server no vuelve a mostrar el prompt de consentimiento en reintentos del client (salvo que cambien los scopes). Así, si se logra que el servidor acepte un redirect_uri controlado por el atacante para un client popular con muchos scopes ya concedidos, el token llega solo, sin interacción de la víctima.

Hay dos grants. El authorization code (response_type=code) devuelve un code que se cambia server-side por el token en un POST que incluye el client_secret (token = code + client_id + client_secret + redirect_uri). El implicit (response_type=token) devuelve el token directo en el fragmento de la URL (redirect_uri#access_token=...), sin secret —lo que lo hace más frágil: robar la redirección entrega el token sin que falte ningún secreto—.

Las derrotas del redirect_uri#

  • Open redirect por grant link malformado. Un grant link con valores basura en scope o client_id hace que algunos authorization servers, en vez de mostrar el error en su propio dominio, redirijan al redirect_uri con parámetros de error. Es un open redirect encubierto que se dispara desde un dominio confiable (api.trusted.com) hacia el del atacante —LinkedIn era vulnerable—.
  • Hijack del redirect_uri. Si la app permite solo http://example.com/token/callback, hay trucos para colar uno propio: directory traversal (.../callback/../../our/path, con encoding y CRLF para evadir el chequeo), naked domain (si el permitido es el dominio pelado, algunas implementaciones aceptan subdominios → controlledsub.example.com), y TLD suffix confusion (dejar el dominio intacto y agregar un sufijo válido: example.comexample.com.mx; hallado en Instagram y Slack).
  • Covert Redirect. Si el dominio permitido tiene un open redirect propio (/exit/redirect.php?u=...), se usa como redirect_uri: pasa la validación —es el dominio permitido— pero reenvía el token al atacante por un 302. Especialmente potente en implicit grant.
  • Force-install por clickjacking. Como conceder es solo clickear “allow”, si el grant prompt no trae X-Frame-Options, el clickjacking de 3.3 fuerza ese clic y concede scopes arbitrarios sin que el usuario lo note.
OAuth mal implementado es “el login con Google/Facebook roto”: es el eslabón de autenticación de 3.4 delegado a un tercero. El account takeover no requiere tocar la contraseña de la víctima —basta que el authorization server emita un token hacia un redirect_uri controlado—.

Familia A · Estado autocontenido: JWT#

El JWT (JSON Web Token) lleva la sesión al extremo del token con significado de 3.4: en vez de un identificador opaco que apunta a un estado server-side, el token contiene el estado. Son tres partes base64url separadas por puntos: header.payload.signature. El header declara el alg (algoritmo de firma), el payload lleva los claims que la app usa para autorizar (username, isAdmin, exp), y la firma se calcula hasheando header.payload con una clave. Como el payload es solo base64url —codificado, no cifrado—, cualquiera lo lee y lo modifica; cambiar "isAdmin":false a true es trivial. La firma es la única barrera, y todos los ataques apuntan a ella:

  • Firma no verificada. La app usa el decode()/unverified de la librería en vez de verify(): se modifican los claims sin re-firmar y el token pasa.
  • alg:none. La especificación admite tokens unsecured con "alg":"none" y firma vacía (el token termina en un punto). Si la app respeta el alg del header sin whitelist, acepta un token sin firma con "isAdmin":true. El filtro que rechaza none se evade con variaciones de caso (NoNe, nOnE).
  • Secreto HMAC débil. Si usa HS256 con un secreto por defecto o de tutorial (secret, changeme), se crackea offline: el atacante tiene header.payload y la firma, y prueba candidatos hasta que el HMAC coincida (hashcat -m 16500, jwt_tool -C). Con el secreto, firma cualquier token.
  • Inyección de clave en el header (jwk/jku/kid). El jwk embebe la clave pública propia en el header → el atacante firma con su privada y el server verifica con la embebida. El jku apunta a un JWK Set en un dominio del atacante (whitelist de host evadible con discrepancias de parsing: https://trusted.com@attacker.com). El kid selecciona la clave; si construye una ruta de archivo, un path traversal a /dev/null (contenido vacío)
    • firmar con secreto vacío coincide; si alimenta una query, se inyecta SQL.
  • Algorithm confusion RS256→HS256. El más elegante: en RS256 el server firma con su clave privada y verifica con la pública (que es pública por diseño). Si el server acepta también HS256 y usa “la clave” para verificar sin distinguir el algoritmo, el atacante firma el token con HMAC-SHA256 usando la clave pública RSA como secreto —el server la toma como secreto HMAC y coincide—.
# alg:none con bypass de filtro por caso {"alg":"nOnE","typ":"JWT"} # token: header.payload. (firma vacía) # crackear secreto HMAC débil y re-firmar hashcat -a 0 -m 16500 <jwt> jwt.secrets.list python3 jwt_tool.py <jwt> -S hs256 -p 'secret123' -T # algorithm confusion: la clave pública como secreto HMAC python3 jwt_tool.py <jwt> -X k -pk public.pem -T

A esto se suman claims sin exp (replay indefinido de un token robado), sin aud (reuso cross-service), y datos sensibles en el payload (legibles con un simple decode de base64). La defensa transversal: fijar y validar el algoritmo esperado server-side (mata alg:none y algorithm confusion), secretos aleatorios ≥256 bits, no procesar jwk/jku/kid sin whitelist estricta, exp corto + aud, y nunca secretos en el payload.

Familia A · El origen abierto por diseño: CORS, postMessage, WebSockets, storage#

La same-origin policy que 3.3 trabajaba para eludir, el web moderno la relaja a propósito para que dominios distintos colaboren. Cada mecanismo de relajación traslada la frontera a una validación de origen que a menudo se implementa mal.

  • CORS. Permite requests cross-domain vía XMLHttpRequest: el navegador manda Origin y el servidor responde Access-Control-Allow-Origin (ACAO) indicando quién puede leer la respuesta, con un preflight OPTIONS para los requests no simples y Access-Control-Allow-Credentials para las credenciales. Las misconfigs peligrosas: ACAO: * (todos permitidos), o reflejar el Origin recibido sin validar combinado con Allow-Credentials: true —eso reproduce el desastre del crossdomain.xml con comodín que se anticipó en 3.3: cualquier sitio del atacante lee datos autenticados de la víctima cross-domain—.
  • Web Messaging (postMessage). Comunicación cross-document. El receptor debe verificar event.origin con una whitelist exacta (event.origin === 'chat.example.com'); el error clásico es e.origin.indexOf(".owasp.org"), que www.owasp.org.attacker.com evade por substring. Y event.data es input no confiable aun de un dominio confiable: meterlo en innerHTML produce DOM-based XSS. El emisor debe fijar el target origin explícito, nunca * (con *, una redirección filtra la data a un host desconocido).
  • WebSockets. Un canal full-duplex (ws:///wss://) que hace un handshake de upgrade sobre HTTP. Es responsabilidad del servidor validar el Origin del handshake; si no lo hace, acepta conexiones de cualquier origen → Cross-Site WebSocket Hijacking (CSWSH), un CSRF sobre el canal. Los WebSockets no traen autenticación ni autorización propias: valen los tests normales de sesión, control de acceso e IDOR de 3.4.
  • Local Storage. localStorage/sessionStorage guardan pares clave/valor por dominio (~5 MB) que nunca viajan al servidor pero son legibles desde JavaScript: un solo XSS los vacía enteros. Por eso guardar session IDs o datos sensibles ahí es un error —una cookie HttpOnly los protege de JS; localStorage no—.

A este cluster se suman los vectores DOM puros que completan la superficie client-side moderna: DOM clobbering (HTML “seguro” como <img name="getElementById"> sobrescribe métodos del DOM y paraliza el JS de la página —el caso GitHub Gist—), RPO (Relative Path Overwrite: abusar cómo el navegador resuelve la ruta relativa de un CSS para que la propia página se importe como stylesheet → CSS injection y, en IE quirks mode, XSS por expression()), client-side resource manipulation (un location.hash que llega al src de un <script> → XSS), y CSS injection (con selectores de atributo se lee el DOM carácter a carácter, exfiltrando un token CSRF). Todos convergen en el mismo sink del XSS del lado cliente: innerHTML en vez de textContent.

Familia B · Input rico y motores potentes → RCE server-side#

Esta familia es la tesis de la inyección de 3.2 —romper la frontera entre dato y código— con intérpretes que 2011 no atacaba. El desenlace ya no es leer una base de datos: es ejecución de código en el servidor.

SSTI (Server-Side Template Injection)#

Ocurre cuando input del usuario se embebe en una plantilla de forma insegura —concatenado dentro del string de la plantilla en vez de pasado como dato de contexto— y el motor lo evalúa como código. La causa raíz es tratar la plantilla como string: render_template_string("Hola " + name) en lugar de render_template("hola.html", name=name). A diferencia del XSS, que ejecuta en el navegador, el SSTI ejecuta en el servidor, con acceso al filesystem, las variables de entorno y, vía los globales del lenguaje, a comandos. La metodología tiene tres pasos:

flowchart TD
  IN["Inyectar {{7*7}} / ${7*7} /\n<%= 7*7 %> / #{7*7}"] --> EVAL{"¿Devuelve 49?"}
  EVAL -->|"no"| SAFE["Sin SSTI (o dato inerte)"]
  EVAL -->|"sí"| FP{"Fingerprint del motor"}
  FP -->|"{{7*'7'}} → 7777777"| JINJA["Jinja2 (Python)"]
  FP -->|"{{7*'7'}} → 49"| TWIG["Twig (PHP)"]
  FP -->|"${7*7} evalúa"| FREE["Freemarker / Mako (Java/Python)"]
  FP -->|"<%= 7*7 %>"| ERB["ERB (Ruby)"]
  JINJA --> RCE["Escapar a los globales del lenguaje\n→ os/subprocess/Runtime → RCE"]
  TWIG --> RCE
  FREE --> RCE
  ERB --> RCE

Detectado el motor, la explotación escapa del sandbox del motor a las primitivas de ejecución del lenguaje host. En Jinja2, se navega la jerarquía de clases de Python hasta os:

{{config.__class__.__init__.__globals__['os'].popen('id').read()}} # Jinja2 (Python/Flask) {{cycler.__init__.__globals__.os.popen('id').read()}} # variante que no depende de config {{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id")}} # Twig (PHP) <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} # Freemarker (Java) <%= system('id') %> # ERB (Ruby)

Aun donde el sandbox bloquea los comandos, el SSTI vuelca secretos: en Jinja2, {{config}} expone toda la configuración de Flask (incluida la SECRET_KEY —que reabre la forja de sesión de 3.4—) y las variables de entorno filtran API keys y credenciales de base de datos. tplmap es el “sqlmap del SSTI” (detección, fingerprint y --os-shell).

Deserialización: object injection y POP chains#

El serialize()/unserialize() de PHP convierte objetos a un formato de texto legible (O:5:"Packt":1:{s:4:"name";...}) y los reinstancia. El peligro: pasar input del usuario a unserialize() instancia objetos arbitrarios con valores controlados. Solo instanciar no daña; el daño viene de encadenar con las magic functions —métodos que se ejecutan solos: __construct, __destruct, __wakeup—. Si en el código existe una clase con una magic function peligrosa (un gadget), el atacante la instancia vía unserialize() y esa función se dispara. El ejemplo canónico: una clase LogWriter cuyo __destruct() hace file_put_contents($this->logfile, $this->logdata) —inyectando el objeto serializado con logfile = una ruta en el webroot y logdata = <?php system($_GET['cmd']); ?>, al terminar el script se escribe un webshell—. Encadenar magic functions de gadgets presentes para lograr RCE o traversal es la técnica de las POP chains (property-oriented programming), automatizable con phpggc. El mismo patrón existe en Java (readObject, gadgets de ysoserial), .NET y Python (pickle).

File upload → RCE#

Cuando una app permite subir archivos y no filtra bien, subir y ejecutar un script server-side da RCE. El flujo: probar ejecución con <?php echo phpversion(); ?>, plantar un one-liner de comandos (<?php passthru($_GET['cmd']); ?> —con un nombre de parámetro largo y absurdo para que nadie más lo abuse—), escalar a un web shell multifuncional (b374k, con file manager, command shell, acceso a base de datos y protección por contraseña) y de ahí a una reverse shell por Netcat para pivotar. Ante filtros, la escalera de bypasses:

phpinfo.PHP # blacklist case-sensitive (no normaliza mayúsculas) Content-Type: image/gif # falsificar el MIME de la parte multipart en Burp # .htaccess de Apache: forzar a ejecutar extensiones benignas como PHP AddType application/x-httpd-php .lol # polyglot: PHP embebido en el EXIF de un JPG válido, renombrado a .php (vence getimagesize()) strings exploit.jpg | head -4 && mv exploit.jpg exploit.php

Donde solo se permiten imágenes, formatos como SVG (que es XML y admite <script>) y SWF (ActionScript compilado con mtasc, getURL('javascript:...')) convierten un “subir avatar” en un XSS almacenado. Y una librería de procesamiento de imágenes vulnerable habilita DoS por agotamiento de memoria: pixel flood (JPEG con dimensiones EXIF de 65000×65000), frame flood (GIF con decenas de miles de frames) o una zlib-bomb en el zTXT de un PNG.

SSRF (Server-Side Request Forgery)#

La app hace un request hacia un URL que controla el atacante y devuelve parte del resultado, usando al servidor como proxy para saltar el firewall y alcanzar servicios internos, el loopback o la LAN. El código típico es un “visor” que hace file_get_contents($_POST['url']). Usos: port-scan interno (por diferencia de error entre puerto abierto y cerrado, a veces con banner), lectura de archivos (file://), y un abanico de protocol handlers (gopher:// para forjar payloads TCP arbitrarios contra Redis o una base de datos interna, dict, ftp, ldap). El salto más rentable del web moderno es a la nube:

# robar credenciales IAM del metadata de la instancia (AWS) url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ # port-scan interno por diferencia de error url=http://scanme.internal:22/ → banner SSH-2.0-OpenSSH → puerto abierto

Apuntar el SSRF al metadata endpoint 169.254.169.254 roba las credenciales IAM de la instancia —el puente directo de una vulnerabilidad web a comprometer la cuenta cloud entera—. El SSRF también nace del XXE de 3.2 (una entidad externa que apunta a un servicio interno), y la IDOR moderna (?id=101 sin chequeo de permiso) —tan prevalente hoy que supera al XSS, porque los frameworks filtran el XSS solos pero los permisos no se automatizan— cierra el círculo con el control de acceso de 3.4.

Las cuatro clases de esta familia convergen en una reverse shell o un webshell en el servidor. Ese es el punto de entrada al resto de la Parte 3: una vez con ejecución, la escalada de privilegios de 3.9 toma el sistema, y la reverse shell se profesionaliza en el C2 de 3.10. El bloque web no es un fin: es un foothold.

API: REST, GraphQL y la autorización que no viaja#

Hay una comodidad del web moderno que reordena todas las anteriores y merece sección propia: la aplicación dejó de ser una página que el servidor arma y pasó a ser un cliente rico —una single-page application o una app móvil— que consume una interfaz de programación (API). El servidor ya no devuelve HTML: devuelve datos, casi siempre en JSON, a través de una colección de endpoints. Ese cambio arquitectónico mueve una frontera crítica —la del control de acceso— a un lugar donde a menudo nadie la reconstruye, y por eso la clase de vulnerabilidad dominante de las API no es una novedad exótica sino la vieja falla de autorización de 3.4 multiplicada por la cantidad de endpoints. El marco de referencia es el OWASP API Security Top 10, y sus primeras posiciones son variantes de un mismo problema: la autorización que la interfaz clásica resolvía en un lugar ahora tiene que resolverse en cada uno de cientos de endpoints, y basta que uno la omita.

La falla número uno es la autorización rota a nivel de objeto (Broken Object Level Authorization, BOLA): es la IDOR de 3.4 a escala de API. Un endpoint como GET /api/pedidos/{id} recibe el identificador del objeto en la petición y devuelve el pedido —pero si no verifica que ese pedido pertenece al usuario autenticado, cualquiera itera los identificadores y lee los datos de todos—. Es tan prevalente porque la lógica de la API invita a confiar en el identificador que el cliente envía, y porque probar la propiedad objeto por objeto es tedioso y se omite. Su hermana es la autorización rota a nivel de función (Broken Function Level Authorization, BFLA): llamar a funciones para las que no se tiene rol —adivinar el endpoint administrativo (/api/admin/usuarios), o cambiar el método HTTP (GET que se permite, DELETE que no se controla)— porque el control de rol se aplicó en la interfaz de usuario pero no en el endpoint.

Dos fallas más completan el núcleo. La asignación masiva (mass assignment) ocurre cuando la API vincula automáticamente el JSON que recibe a los campos de un objeto interno: si el cliente agrega un campo que la interfaz no mostraba —{"nombre":"...","rol":"admin"} o {"saldo":999999}—, y el servidor lo asigna sin una lista explícita de qué campos son modificables, el cliente escribe propiedades que no le corresponden. Es la contracara de escritura de la BOLA. Y la exposición excesiva de datos (excessive data exposure) es su versión de lectura: la API devuelve el objeto completo —con campos internos, otros usuarios, banderas privadas— confiando en que el cliente muestre solo lo público; el atacante, que lee la respuesta cruda, obtiene todo lo que el servidor mandó de más.

GraphQL agrega su propia superficie sobre este fondo. Es un lenguaje de consulta donde el cliente pide exactamente los datos que quiere en una sola petición, y esa flexibilidad crea vectores específicos. La introspección (introspection) es la capacidad del esquema de describirse a sí mismo: una consulta de introspección devuelve el catálogo completo de tipos, campos y operaciones, entregándole al atacante el mapa entero de la API —enormemente útil para el reconocimiento, y peligroso si queda habilitado en producción—. La profundidad y complejidad de consulta habilitan una denegación de servicio: como las relaciones se pueden anidar, una consulta profunda (autor → libros → autor → libros …) obliga al servidor a resolver un árbol exponencial con una sola petición pequeña. Y el procesamiento por lotes (batching) —enviar muchas operaciones en una sola petición— permite evadir los límites de tasa y las protecciones contra fuerza bruta que cuentan peticiones, no operaciones. Bajo toda esa superficie nueva, sin embargo, la BOLA y la BFLA siguen mandando: los resolvers de GraphQL tienen que verificar la autorización objeto por objeto igual que cualquier endpoint REST, y con frecuencia no lo hacen.

Conviene notar que las clases de inyección de 3.2 no desaparecen en las API: un parámetro que llega por JSON y termina en una consulta a la base de datos es tan inyectable como el de un formulario. La API cambia el envoltorio y el modelo de autorización, no las leyes de la inyección.

Defensa y detección: la lente púrpura#

La defensa de todo el capítulo es una sola idea: reconstruir la frontera de confianza que la comodidad movió. Cada mecanismo moderno tiene un punto donde se debe volver a validar lo que antes validaba el monolito.

Endurecimiento (reconstruir la frontera)#

ClaseControl primario
OAuthMatching exacto y estricto del redirect_uri (comparación completa, sin prefijo/traversal/subdominios/sufijos TLD); errores en el dominio del authorization server (nunca redirigir); parámetro state (anti-CSRF del flow) + PKCE; preferir code + PKCE sobre implicit; X-Frame-Options en el grant prompt; sin open redirects en el client (mata el Covert Redirect)
JWTFijar y validar el algoritmo esperado server-side (nunca leer alg del header para decidir simétrico/asimétrico); secretos HMAC aleatorios ≥256 bits; no procesar jwk/jku/kid sin whitelist estricta; exp corto validado + aud; nunca datos sensibles en el payload
CORSNunca * con credenciales; whitelist de orígenes validada exactamente (no reflejar el Origin); Vary: Origin
postMessage / WSVerificar event.origin/Origin con comparación exacta (no indexOf); event.data con textContent, no innerHTML; target origin explícito; validar el Origin del handshake WS + anti-CSRF por conexión
Local StorageNo guardar session IDs ni sensibles (usar cookie HttpOnly/Secure/SameSite); sanitizar lo que salga del storage al DOM
SSTINunca concatenar input en la plantilla (pasarlo como variable de contexto); si el usuario provee plantillas, motor con sandbox (sabiendo que se han evadido) o lógica-menos (Mustache); least-privilege del proceso
DeserializaciónNunca unserialize()/pickle/readObject sobre input del usuario (usar JSON); allowed_classes en PHP 7+
File uploadWhitelist de extensiones con normalización + magic bytes + re-encodear la imagen (destruye polyglots); servir uploads desde un dominio/directorio sin ejecución (AllowOverride None); nombres aleatorios; Content-Disposition: attachment para SVG/SWF
SSRFWhitelist de destinos + resolver y validar el IP (bloquear rangos privados/link-local/loopback); deshabilitar handlers no-HTTP; egress filtering; en cloud, IMDSv2 (token) para que el SSRF no lea el metadata
API (BOLA/BFLA)Autorización a nivel de objeto en cada endpoint (verificar propiedad/rol server-side, default-deny, no confiar en el identificador del cliente); control de rol en la función además de la interfaz; probar cada objeto y método
API (mass assignment / exposición)Lista explícita de campos vinculables (allowlist, nunca bind automático del JSON completo); devolver solo los campos necesarios (no filtrar en el cliente)
GraphQLDeshabilitar la introspección en producción; límites de profundidad y complejidad de consulta; rate limiting que cuente operaciones, no peticiones (mata el batching); autorización en cada resolver

Detección (la señal en la telemetría)#

  • La firma común de la Familia B es el RCE server-side: un proceso hijo anómalo del servidor de aplicación (python/java/php-fpm/w3wpsh/cmd/id/whoami) es la señal más fuerte de un SSTI, una deserialización o un upload explotados —Event ID 4688 + Sysmon 1, relación padre-hijo—. El upload deja además un archivo nuevo en el webroot seguido de GET con un parámetro de comando en la query.
  • SSRF: conexiones salientes anómalas del servidor —a 169.254.169.254, a puertos internos secuenciales, o beaconing— en los logs de firewall/proxy.
  • OAuth/JWT: redirects OAuth hacia dominios externos inesperados (redirect_uri/Location fuera del client); un token JWT con alg:none, alg en caso raro, o jku/kid anómalos; y —la señal de oro del token forjado— una acción privilegiada de un usuario cuyo token declara un rol que no coincide con el registro en la base de datos, o un mismo token usado desde IPs/geografías nuevas (replay).
  • CORS/postMessage/WS: conexiones WebSocket con Origin externo y requests CORS con Origin anómalo; la exfiltración resultante aparece como conexión saliente del navegador a un dominio del atacante.
  • API (BOLA/BFLA): una sesión que itera identificadores de objeto secuenciales en un endpoint (/api/pedidos/101, 102, 103…) —la misma firma de harvesting de 3.4 llevada a la API—, peticiones a endpoints administrativos desde cuentas sin rol, y un volumen anómalo de respuestas exitosas sobre objetos ajenos; en GraphQL, consultas de introspección desde clientes de producción y consultas de profundidad inusual.

El emparejamiento púrpura es el mismo del resto del bloque web: el atacante opera sobre una frontera que la aplicación dejó de custodiar, con requests que a menudo parecen legítimos, así que el defensor lo caza por la anomalía —el proceso hijo que no debería existir, la conexión saliente inesperada, el claim que no coincide con el registro, el Origin externo—. La correlación de esas señales a escala de SIEM se desarrolla del lado azul en la Parte 5, con la misma lógica de 4.9 · Defensa de AD aplicada a la capa web. Con este capítulo se cierra el bloque web de la Parte 3 (3.2→3.5); lo que sigue es la explotación de binarios y la post-explotación, donde el foothold web se convierte en control del sistema.

Referencias#

  • red-web/mmwp/cap-06OAuth 2.0 Security: los cuatro roles, el redirect_uri como frontera crítica, code vs implicit grant, y las derrotas de la redirección (open redirect por grant malformado, traversal/naked domain/TLD suffix confusion, Covert Redirect, force-install por clickjacking).
  • red-web/mmwp/cap-03File Upload Vulnerabilities: de phpversion() al webshell y la reverse shell, la escalera de bypasses (case, MIME, .htaccess, polyglot EXIF), XSS por SVG/SWF y DoS por imagen malformada.
  • red-web/mmwp/cap-05Emerging Attack Vectors: SSRF (port-scan, file://, protocol handlers, MailChimp), IDOR (Flipkart/HackerOne), DOM clobbering (GitHub Gist), RPO/PRSSI, clickjacking y PHP Object Injection (unserialize
    • magic functions → POP chains).
  • red-web/web-moderno/cap-01Server-Side Template Injection: causa raíz (concatenar en la plantilla), detección ({{7*7}}/polyglot), fingerprint ({{7*'7'}}), RCE por motor (Jinja2/Twig/Freemarker/ERB/Mako) y fuga de secretos ({{config}}), con tplmap.
  • red-web/web-moderno/cap-02JWT Attacks: estructura y firma como única barrera, firma no verificada, alg:none, secreto HMAC débil (hashcat -m 16500), inyección jwk/jku/kid, algorithm confusion RS256→HS256, y exp/aud/datos sensibles.
  • red-web/owasp-tg4/cap-02Client Side moderno: CORS (ACAO */reflejo del Origin), Web Messaging (event.origin con whitelist exacta), WebSockets (CSWSH), Local Storage, CSS injection y Cross Site Flashing.
  • OWASP — SSTI (WSTG-INPV-18), Deserialization, File Upload, SSRF y OAuth 2.0 Cheat Sheets; PortSwigger JWT y OAuth.
  • MITRE — CWE-1336 SSTI, CWE-502 Deserialization, CWE-434 Unrestricted Upload, CWE-918 SSRF y CWE-347 Improper Verification of Signature.