Saltar al contenido

Temas

SSRF e IMDSv2

Cómo un SSRF hace que el servidor pida la URL del atacante, por qué en AWS puede robar las credenciales del rol vía 169.254.169.254, y cómo IMDSv2 y el egress lo cortan.

IntermedioEn progreso 6 min de lectura1 oct 2026#ssrf#imdsv2#metadatos#aws#ec2#cloud

De todas las vulnerabilidades web, el SSRFSSRFFallo en el que un atacante hace que el servidor emita peticiones a una URL que él controla, alcanzando sistemas internos que él no podría tocar. es la que mejor conecta la aplicación con la nube. Un fallo que en un servidor tradicional sería “molesto” —hacer que el servidor pida una URL que tú eliges— en AWS puede convertirse en el robo de las credenciales del rol de la instancia y, a partir de ahí, en el compromiso de la cuenta. Es el caso que hizo famoso a IMDSv2, y la razón por la que esta nota es la pieza clave del tema. Como siempre, todo lo que sigue es para defender: pruébalo solo en tu laboratorio.

Qué es SSRF#

Server-Side Request Forgery (A10 en el OWASP Top 10) ocurre cuando una aplicación toma una URL —o parte de ella— de la entrada del usuario y la solicita desde el servidor. Muchas funciones legítimas lo hacen: previsualizar un enlace, importar una imagen desde una URL, un webhook, un conversor de documentos. Si el servidor pide lo que le digan sin restricción, el atacante lo usa como proxy hacia sitios que él no alcanza directamente:

  • Servicios en subredes privadas (bases de datos, paneles internos) que no están expuestos a Internet.
  • localhost y puertos locales del propio servidor (http://127.0.0.1:8080/admin).
  • En la nube, el servicio de metadatos de la instancia.

La clave es el cambio de perspectiva: la petición ya no sale del navegador del atacante, sale del servidor, desde dentro de la red y con su identidad.

SSRF contra el servicio de metadatos (y cómo IMDSv2 lo corta)
VPC CITAS · SUBRED PRIVADAAtacanteAPI Citasfetch(url)EgressIMDS169.254.169.254?url=169.254.169.254AccessKeyId · Secret · Token✗ actúa con el rolIMDSv2: sin token → 401solo destinos permitidosurl=169.254…GET /iam/…AKIA…+tokenAKIA…+tokenGET (sin token)GET 169.254…

Paso 1 de 5La petición

La app de Citas tiene una función que descarga una URL que le pasa el usuario (previsualizar un adjunto). El atacante le pasa la IP del servicio de metadatos.

Por qué SSRF y nube van de la mano#

Toda instancia EC2 puede consultar un servicio interno de metadatos (Instance Metadata Service, IMDS) en la dirección link-local 169.254.169.254 (en IPv6, fd00:ec2::254). Ese servicio expone datos de la instancia y, lo más sensible, las credenciales temporales del rol IAM asociado, en la ruta:

http://169.254.169.254/latest/meta-data/iam/security-credentials/<nombre-del-rol>

Esa ruta devuelve un AccessKeyId, un SecretAccessKey y un Token de sesión. Quien los lea puede actuar como la instancia frente a la API de AWS, con todos los permisos de su rol. La dirección es link-local: no se enruta, no sale de la máquina, y por eso durante años se consideró “segura” sin autenticación. El SSRF rompe ese supuesto, porque la petición la hace el propio servidor.

Con IMDSv1, cualquier GET bastaba. Un SSRF tan simple como ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ leía las credenciales y las devolvía al atacante. Es, en esencia, lo que ocurrió en la brecha de Capital One (2019): un SSRF contra un componente mal configurado en EC2 alcanzó el servicio de metadatos, obtuvo las credenciales del rol y las usó para leer datos en S3. AWS lanzó IMDSv2 poco después, en parte como respuesta.

IMDSv2: por qué existe y cómo funciona#

IMDSv2 convierte el acceso a los metadatos en una sesión que un SSRF simple no puede completar. En vez de un GET directo, exige dos pasos:

  1. Obtener un token con una petición PUT y una cabecera especial. El token dura lo que indiques, hasta un máximo de 6 horas (21 600 s):

    Terminal
    TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
  2. Usar el token en cada GET, en la cabecera X-aws-ec2-metadata-token:

    Terminal
    curl -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/iam/security-credentials/rol-de-citas

Esto frena el SSRF por varios motivos a la vez:

  • Exige un PUT. La mayoría de primitivas SSRF solo saben hacer GET (el atacante controla una URL, no el método). Sin el PUT inicial no hay token, y sin token el GET recibe 401 Unauthorized.
  • Rechaza cabeceras de proxy. El servicio rechaza los PUT que llevan una cabecera X-Forwarded-For, típica cuando la petición pasa por un proxy o se reenvía —justo el patrón de muchos SSRF.
  • Límite de saltos (hop limit). La respuesta al PUT tiene por defecto un hop limit de 1 a nivel IP: el token no puede reenviarse más allá de la propia instancia. (Para contenedores que añaden un salto de red, a veces hace falta subirlo a 2.)

Cómo forzar IMDSv2#

Que IMDSv2 exista no basta: hay que exigirlo para que IMDSv1 deje de responder. Se hace con HttpTokens=required en las opciones de metadatos, al lanzar la instancia o modificándola:

ec2.tf
resource "aws_instance" "worker" {
# ...
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # exige IMDSv2; IMDSv1 deja de funcionar
http_put_response_hop_limit = 1 # 2 si corres contenedores
}
}
Terminal
# Exigir IMDSv2 en una instancia existente
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-tokens required \
--http-endpoint enabled
# Encontrar instancias que todavía permiten IMDSv1 (optional)
aws ec2 describe-instances \
--query 'Reservations[].Instances[?MetadataOptions.HttpTokens==`optional`].InstanceId' \
--output text

Otras defensas (no dependas solo de IMDSv2)#

IMDSv2 protege un objetivo concreto —los metadatos—, pero el SSRF puede apuntar a muchos otros. La defensa es en capas, empezando por la aplicación:

No confíes en la entrada para construir URLs#

El origen del problema es pedir lo que el usuario diga. En el lado de la app:

Vulnerable

Seguro

vulnerable.js
// Pide cualquier URL que llegue en el parámetro: SSRF directo
const res = await fetch(req.query.url);
return res.body;
seguro.js
// Allow-list de destinos + bloqueo de direcciones internas/link-local
const url = new URL(req.query.url);
if (url.protocol !== 'https:') throw new Forbidden();
if (!ALLOWED_HOSTS.has(url.hostname)) throw new Forbidden();
// Resuelve el DNS y rechaza rangos privados y 169.254.0.0/16 (anti-rebind)
await assertPublicAddress(url.hostname);
const res = await fetch(url, { redirect: 'manual' }); // no sigas redirecciones a ciegas

Puntos finos que marcan la diferencia: usa allow-list, no deny-list (es fácil olvidar un rango); resuelve el DNS y valida la IP resuelta, no solo el nombre (contra DNS rebinding); y no sigas redirecciones automáticamente, porque un 302 a 169.254.169.254 evade una validación que solo mire la URL original.

Egress allow-list y VPC endpoints#

Como segunda capa, controla la salida: si la tarea de Citas solo puede conectarse a los destinos que necesita (vía security groups de salida restringidos, un proxy de egress o VPC endpoints para hablar con AWS sin salir a Internet), una petición al rango de metadatos o a un host arbitrario no llega. Es reducir la superficie por el lado de la salida, y no depende de que la app esté bien escrita.

WAF y detección#

AWS WAF tiene reglas que bloquean patrones de SSRF conocidos (URLs con IPs internas), útil como filtro externo pero evadible: trátalo como detección. Y con A09 cubierto, vigila en los logs las peticiones salientes hacia 169.254.169.254 o rangos privados: en una app sana no deberían existir.

Tu turno

Audita el acceso a metadatos en tu entorno

Sobre una cuenta de AWS tuya o de tu laboratorio:

  • Lista tus instancias EC2 y comprueba cuáles tienen HttpTokens=optional (el comando de la pestaña AWS CLI). ¿Cuántas siguen permitiendo IMDSv1?
  • Desde una instancia tuya, reproduce el flujo IMDSv2 con los dos curl y observa la respuesta de credenciales. Luego pon http_tokens = "required" y comprueba que un GET sin token devuelve 401.
  • Revisa la salida a Internet de una de tus cargas: ¿podría alcanzar 169.254.169.254 o una subred interna? ¿Qué allow-list de egress lo impediría?
  • Busca en tu código funciones que hagan fetch/request con una URL que venga del usuario. ¿Validan el destino?

Reto

Tienes IMDSv2 obligatorio. ¿Puedes olvidarte del SSRF?

▸Mostrar solución

No. IMDSv2 protege los metadatos, pero el SSRF puede seguir alcanzando servicios internos, localhost, paneles de administración en subredes privadas o bases de datos. Y si subes el hop limit a 2 para contenedores sin más controles, reabres parte del riesgo. IMDSv2 es una capa crucial, no la única: hace falta validar la entrada en la app y controlar el egress.

Reto

¿Por qué exigir un PUT con una cabecera frena a casi todos los SSRF, si técnicamente un atacante podría hacer un PUT?

▸Mostrar solución

Porque la mayoría de primitivas SSRF dan control sobre una URL, no sobre el método HTTP ni las cabeceras. El atacante consigue que el servidor haga un GET url, pero no que haga un PUT con X-aws-ec2-metadata-token-ttl-seconds. Los pocos casos donde sí controla método y cabeceras (algunas inyecciones de petición completas) son mucho más raros, y ahí entran el hop limit y el rechazo de X-Forwarded-For. IMDSv2 no es infalible; sube el listón lo bastante para neutralizar el SSRF típico.

En resumen#

  • SSRF: la app pide una URL que controla el atacante, usando al servidor como proxy hacia lo que él no alcanza.
  • En la nube es crítico porque puede leer 169.254.169.254 y obtener las credenciales del rol de la instancia (así fue Capital One).
  • IMDSv2 convierte el acceso en una sesión: PUT para un token (máx. 6 h), GET con la cabecera del token, hop limit 1 y rechazo de X-Forwarded-For. Un SSRF simple no lo completa.
  • Exígelo con HttpTokens=required (Terraform, CLI o por defecto en la cuenta) y marca lo no conforme con Config o SCPs.
  • No dependas solo de IMDSv2: valida la entrada de URLs (allow-list, IP resuelta, sin redirecciones) y controla el egress.

Referencias#

Notas relacionadas

ESC
↑↓ para navegar↵ para abriresc para cerrar