Aprende › Vulnerabilidades web

SSRF (Server-Side Request Forgery): qué es, cómo se explota y cómo se corrige

· 15 min de lectura

Aprende qué es el Server-Side Request Forgery, cómo un servidor puede ser obligado a pedir servicios internos o metadatos de la nube, cómo se detecta en un laboratorio autorizado (incluida la SSRF ciega) y cómo se corrige con listas blancas, validación de la IP resuelta, segmentación e IMDSv2.

Muchas aplicaciones web necesitan pedir cosas a otros servidores: descargar una imagen desde una URL, generar la vista previa de un enlace, llamar a un webhook o importar un archivo remoto. Son funciones útiles y muy comunes. El problema aparece cuando el usuario decide a qué dirección se hace esa petición y la aplicación no lo controla.

Eso es el Server-Side Request Forgery (SSRF): el atacante consigue que el servidor haga peticiones en su nombre. Y como el servidor vive dentro de la red de la empresa, puede llegar a sitios a los que el atacante nunca llegaría desde Internet: paneles de administración internos, bases de datos sin contraseña o el servicio de metadatos de la nube, donde a veces hay credenciales. En esta lección verás qué es, por qué ocurre, cómo se detecta y se explota en un laboratorio autorizado y cómo se corrige de verdad.

Practica estas técnicas solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Usar un servidor ajeno para explorar su red interna sin permiso es un delito.

Qué es un SSRF#

Un SSRF ocurre cuando una aplicación toma una URL (o parte de ella) del usuario y hace una petición a esa dirección desde el servidor, sin comprobar adónde apunta realmente.

Cómo un SSRF convierte al servidor web en un intermediario hacia la red interna
Cómo un SSRF convierte al servidor web en un intermediario hacia la red interna

La clave está en el punto de vista. Desde tu equipo, la red interna de la empresa está detrás de un firewall y no la ves. Pero el servidor web está dentro de esa red: puede hablar con 127.0.0.1 (él mismo), con otras máquinas de la red privada y, si está en la nube, con el servicio de metadatos de la instancia. Si tú controlas la URL que pide, heredas su posición en la red. El firewall no ayuda, porque la petición no viene de fuera: nace dentro.

Además, muchos servicios internos se diseñaron pensando que «nadie de fuera puede llegar», así que no piden autenticación. Un SSRF rompe justo esa suposición.

Por qué ocurre: la causa raíz#

Imagina una función para previsualizar enlaces escrita en PHP:

// preview.php?url=https://ejemplo.com/articulo
$url = $_GET['url'];
$html = file_get_contents($url);
echo extraerTituloYDescripcion($html);

Con una URL normal, el servidor descarga la página y muestra su título. Pero nadie comprueba a qué host apunta la URL. Si alguien envía una dirección interna, el servidor la pide igual:

preview.php?url=http://127.0.0.1:8080/admin
preview.php?url=http://10.0.0.5:6379/
preview.php?url=http://169.254.169.254/latest/meta-data/

En Python, el mismo error tiene este aspecto:

import requests
from flask import request

@app.route("/importar")
def importar():
    url = request.args["url"]
    r = requests.get(url, timeout=5)   # pide lo que diga el usuario
    return r.text

El patrón es siempre el mismo: un dato del usuario decide el destino de una petición del servidor. Igual que en la inyección SQL se mezcla dato y código, aquí se mezcla dato y destino.

Tipos de SSRF#

TipoQué ve el atacanteCómo se confirmaGravedad típica
SSRF con respuestaLa respuesta completa del servicio internoLeyendo directamente el contenido devueltoAlta a crítica
SSRF parcialSolo el código de estado, el tamaño o un errorComparando respuestas entre puertos y hostsMedia a alta
SSRF ciegaNada en la respuestaCon una petición saliente a un servidor que controlas o por diferencias de tiempoMedia, a veces alta

SSRF con respuesta#

La aplicación devuelve el contenido de lo que pidió. Es el caso más directo: puedes leer páginas internas, respuestas de APIs y documentos como si estuvieras dentro de la red.

SSRF ciega#

La aplicación hace la petición pero no te enseña el resultado. Pasa mucho en webhooks, en funciones que procesan una URL en segundo plano o en analizadores que solo guardan «correcto» o «error». Se detecta haciendo que el servidor pida una dirección que tú controlas y comprobando si la petición llega. Aunque no veas la respuesta, sigue siendo útil para descubrir hosts y puertos internos por tiempo o por mensajes de error, y puede encadenarse con servicios internos que actúan solo con recibir una petición.

Otros esquemas además de HTTP#

Algunas librerías aceptan más esquemas que http y https. En un laboratorio verás dos clásicos:

  • file:// permite leer archivos locales del servidor si la librería lo admite, por ejemplo file:///etc/hostname. Convierte el SSRF en una lectura de archivos.
  • gopher:// es un protocolo antiguo que permite enviar bytes casi arbitrarios a un puerto TCP. Algunas librerías, como curl, lo soportan, y por eso se ha usado para hablar con servicios internos que no usan HTTP. Basta con saber que existe y que es la razón principal para permitir solo http y https.

Dónde aparece#

  • Previsualización de enlaces en chats, redes sociales y gestores de contenido.
  • Importar desde URL: fotos de perfil, documentos, feeds RSS o archivos de configuración.
  • Webhooks: el usuario indica una URL a la que el servidor avisará cuando ocurra algo.
  • Generadores de PDF o capturas que abren una página en un navegador del servidor. Si renderizan HTML del usuario, ese HTML puede pedir recursos internos.
  • Procesadores de documentos que resuelven referencias externas, como los XML con entidades externas (XXE), que a menudo terminan en un SSRF.
  • Proxies y APIs internas que reciben un parámetro host, endpoint o callback.
  • Cabeceras como Host o X-Forwarded-Host que algunas aplicaciones usan para construir URLs internas.

El objetivo estrella: los metadatos de la nube#

Los grandes proveedores de nube ofrecen a cada máquina virtual un servicio de metadatos en una dirección especial de tipo link-local, 169.254.169.254, solo accesible desde la propia instancia. Ahí la máquina consulta su configuración y, si tiene un rol asignado, credenciales temporales para usar otros servicios de la nube.

Si una aplicación en esa instancia tiene un SSRF, el atacante puede pedir esa dirección a través del servidor. En AWS, con la versión antigua del servicio (IMDSv1), basta una petición GET sencilla, que es justo lo que un SSRF típico permite hacer. Con unas credenciales del rol, el atacante pasa de la aplicación web a la cuenta de nube.

La defensa que diseñó AWS es IMDSv2: antes de leer los metadatos hay que pedir un token con una petición PUT y enviarlo después en una cabecera. Un SSRF que solo controla la URL de un GET normalmente no puede hacer ese paso, y además la respuesta del token tiene por defecto un límite de saltos de red pensado para que no salga de la instancia.

# IMDSv2: primero se pide un token con PUT, luego se usa en cada lectura
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/

Google Cloud y Azure tienen protecciones parecidas: exigen cabeceras concretas (Metadata-Flavor: Google y Metadata: true, respectivamente) que un SSRF básico no suele poder añadir. Aun así, la recomendación es la misma: configurar la versión que exige token y no depender solo de eso.

Cómo se detecta#

Pruebas manuales#

Busca cualquier parámetro que parezca una URL, un host, una ruta o un nombre de archivo remoto, y sigue estos pasos:

  1. Cambia la URL legítima por una dirección que controles dentro del laboratorio y mira si el servidor la visita.
  2. Si llega la petición, fíjate en desde qué IP viene y con qué cabecera User-Agent: te dice qué librería la hace.
  3. Prueba direcciones internas: 127.0.0.1, localhost y puertos típicos.
  4. Compara las respuestas: código, tamaño, mensaje de error y tiempo. Un puerto abierto y uno cerrado suelen responder distinto.
  5. Comprueba qué esquemas acepta (http, https, file) y si sigue redirecciones.
PruebaQué buscasQué indica
URL hacia tu servidor del laboratorioUna petición entranteEl servidor pide lo que tú le dices: hay SSRF
http://127.0.0.1/Contenido distinto al de fueraAcceso a servicios locales
Puertos 22, 80, 3306, 6379, 8080Diferencias de tiempo o de errorMapa de puertos internos
file:///etc/hostnameContenido del archivoLa librería acepta file://
Redirección desde tu servidorQue el servidor la sigaPuede saltarse una lista blanca

Herramientas#

  • Burp Suite: Repeater para iterar rápido e Intruder para recorrer puertos o rutas. Burp Collaborator da una dirección única que registra las peticiones DNS y HTTP, ideal para la SSRF ciega.
  • Un servidor propio en el laboratorio: python3 -m http.server o nc -lvnp 8000 bastan para ver si llega la petición.
  • ffuf: para enumerar rutas internas a través del parámetro vulnerable, siempre con un ritmo moderado.
  • Revisión de código: busca funciones que hacen peticiones con datos del usuario: file_get_contents, curl_exec, requests.get, urllib, fetch, HttpClient.

Explotación paso a paso en un laboratorio#

Vamos a recorrer un escenario típico, parecido al que encontrarás en los laboratorios JANUS e IN THE HOUSE: una aplicación con una función de «previsualizar URL» y servicios internos que no están expuestos.

Paso 1: confirmar que el servidor hace la petición#

En tu máquina de ataque, dentro de la red del laboratorio, levanta un receptor:

python3 -m http.server 8000

Envía la URL de tu receptor en el parámetro sospechoso:

POST /preview HTTP/1.1
Host: IP_DEL_LAB
Content-Type: application/x-www-form-urlencoded

url=http://IP_ATACANTE:8000/prueba-ssrf

Si en tu terminal aparece una petición GET /prueba-ssrf desde la IP del laboratorio, el SSRF está confirmado.

Paso 2: mirar hacia dentro#

Ahora apunta al propio servidor:

url=http://127.0.0.1/
url=http://127.0.0.1:8080/

Si alguna respuesta muestra algo que no ves desde fuera, como un panel o una API, acabas de llegar a un servicio interno.

Paso 3: descubrir puertos internos#

Con Intruder o con un bucle sencillo, recorre una lista corta de puertos y compara tamaño y tiempo de respuesta:

for p in 22 80 3000 5000 6379 8000 8080 9000; do
  printf "%s " "$p"
  curl -s -o /dev/null -w "%{http_code} %{size_download} %{time_total}\n" \
    -X POST "http://IP_DEL_LAB/preview" --data "url=http://127.0.0.1:$p/"
done

Las diferencias delatan qué puertos tienen algo escuchando.

Paso 4: si hay un filtro, entender cómo falla#

Muchos laboratorios ponen un filtro ingenuo que bloquea la cadena 127.0.0.1 o localhost. Estas son las ideas que debes conocer, porque explican por qué las listas negras no funcionan:

  • Otras representaciones de la misma IP: una dirección IPv4 puede escribirse en decimal entero, en octal o abreviada, y 127.0.0.0/8 entera es loopback, no solo 127.0.0.1. La IPv6 ::1 también es el propio equipo.
  • Nombres que resuelven a IP internas: un dominio cualquiera puede tener un registro DNS que apunte a 127.0.0.1.
  • Redirecciones: si la aplicación solo valida la primera URL y sigue redirecciones, basta con que un servidor permitido responda con un 302 hacia una dirección interna.
  • DNS rebinding (concepto): el dominio resuelve a una IP pública cuando la aplicación lo valida y a una interna un instante después, cuando hace la petición. Funciona porque se resuelve dos veces.

Prueba en el laboratorio qué variante deja pasar el filtro. Lo importante es entender la lección: validar texto no es validar destinos.

Paso 5: usar el acceso interno#

Con acceso a un servicio interno, el siguiente paso depende del escenario: leer un panel que expone credenciales, llamar a una API interna sin autenticación o encontrar un servicio con otra vulnerabilidad, como una inyección de comandos. En los laboratorios, ese suele ser el camino hacia la flag de usuario.

Fíjate en que el SSRF casi nunca es el final del ataque: es la puerta. Su gravedad depende de lo que haya detrás, y por eso en una red bien segmentada el mismo fallo hace mucho menos daño.

Qué puede lograr un atacante#

ImpactoEjemplo real
Acceso a servicios internosPaneles de administración o APIs que confían en la red interna
Robo de credenciales de nubeLeer credenciales temporales del servicio de metadatos con IMDSv1
Lectura de archivos localesSi la librería acepta file://
Mapa de la red internaDescubrir hosts y puertos abiertos por tiempo o por errores
Ataque a otros serviciosHablar con bases de datos, cachés o colas internas sin autenticación
Evasión de controles por IPUsar la IP confiable del servidor para saltarse listas de acceso

Cómo se corrige#

Las cuatro capas de defensa contra SSRF: lista blanca, validación de IP, conexión controlada y red
Las cuatro capas de defensa contra SSRF: lista blanca, validación de IP, conexión controlada y red

La regla de oro: el usuario nunca debería decidir libremente adónde se conecta tu servidor.

Si puedes, no aceptes URLs#

La mejor corrección es de diseño. Si la aplicación solo necesita hablar con tres proveedores, deja que el usuario elija un identificador (proveedor=2) y construye tú la URL en el servidor. Así no hay nada que validar.

Lista blanca de esquemas, hosts y puertos#

Cuando sí necesitas aceptar URLs, define qué está permitido y rechaza todo lo demás: solo https (o http y https), solo los dominios esperados y solo los puertos habituales. Nunca una lista negra.

Resolver el nombre y validar la IP#

La validación debe hacerse sobre la IP a la que de verdad te vas a conectar, no sobre el texto. Resuelve el nombre, comprueba que ninguna de las IP sea privada, de loopback, link-local o reservada, y conecta exactamente a esa IP:

import ipaddress, socket
from urllib.parse import urlparse

PERMITIDOS = {"https"}

def ip_es_publica(ip: str) -> bool:
    d = ipaddress.ip_address(ip)
    return not (d.is_private or d.is_loopback or d.is_link_local
                or d.is_reserved or d.is_multicast or d.is_unspecified)

def validar(url: str) -> str:
    u = urlparse(url)
    if u.scheme not in PERMITIDOS or not u.hostname:
        raise ValueError("Esquema no permitido")
    ips = {info[4][0] for info in socket.getaddrinfo(u.hostname, u.port or 443)}
    if not ips or not all(ip_es_publica(ip) for ip in ips):
        raise ValueError("Destino interno no permitido")
    return ips.pop()   # conecta a ESTA IP, no vuelvas a resolver

Si conectas a la IP ya validada (manteniendo el nombre para TLS), el DNS rebinding pierde efecto, porque no hay una segunda resolución.

No seguir redirecciones (o validarlas todas)#

Desactiva las redirecciones automáticas. En requests es allow_redirects=False; en curl, no actives CURLOPT_FOLLOWLOCATION. Si tienes que seguirlas, vuelve a validar cada destino.

Limitar protocolos en la librería#

En PHP con curl, limita los protocolos permitidos para que file, gopher y otros no estén disponibles:

$ch = curl_init($urlValidada);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);

Defensa en la red y en la nube#

  • Segmentación y filtrado de salida: el servidor que hace peticiones externas no debería poder hablar con la red interna. Un firewall de salida que solo permite lo necesario reduce mucho el impacto.
  • IMDSv2 obligatorio en las instancias de AWS y, si la aplicación no necesita metadatos, bloquear el acceso a 169.254.169.254 desde el proceso.
  • Autenticación en los servicios internos: ningún servicio debería confiar en alguien solo porque viene de la red interna.
  • No devolver la respuesta cruda: si solo necesitas el título de una página, devuelve el título, no todo el contenido. Eso convierte un SSRF con respuesta en uno ciego.

Errores comunes al defenderse#

  • Bloquear la cadena localhost o 127.0.0.1. Hay muchas formas de escribir la misma dirección y todo el rango 127.0.0.0/8 es loopback.
  • Validar el texto de la URL con expresiones regulares. Los analizadores de URL de cada librería interpretan casos raros de forma distinta; valida la IP resuelta.
  • Resolver dos veces. Validar con una resolución y conectar con otra abre la puerta al DNS rebinding.
  • Olvidar las redirecciones. Una URL externa permitida puede redirigir a una interna.
  • Olvidar IPv6. ::1 y las direcciones IPv4 mapeadas en IPv6 también deben bloquearse.
  • Pensar que una SSRF ciega no importa. Sigue permitiendo mapear la red y atacar servicios que actúan con solo recibir una petición.

Preguntas frecuentes#

¿Qué diferencia hay entre SSRF y CSRF?#

Las dos falsifican peticiones, pero desde lugares distintos. En el SSRF quien hace la petición es el servidor, hacia su red interna. En el CSRF quien la hace es el navegador de la víctima, hacia un sitio donde tiene la sesión iniciada.

¿Un firewall perimetral evita el SSRF?#

No. El firewall perimetral filtra lo que entra desde Internet, y en un SSRF la petición sale del propio servidor. Lo que sí ayuda es un filtrado de salida que limite con quién puede hablar ese servidor.

¿Qué es IMDSv2 y por qué protege?#

Es la versión del servicio de metadatos de AWS que exige pedir primero un token con una petición PUT y enviarlo luego en una cabecera. Un SSRF típico solo controla la URL de un GET, así que no puede completar ese intercambio.

¿Por qué el SSRF está en el OWASP Top 10?#

En la edición 2021 del OWASP Top 10 apareció como categoría propia (A10) por su creciente impacto en entornos de nube y microservicios, donde los servicios internos confían en exceso en la red.

Solo en sistemas propios, en laboratorios o en programas de bug bounty que lo permitan expresamente. Explorar la red interna de una empresa a través de su servidor sin permiso es un acceso no autorizado.

Resumen#

  • Un SSRF ocurre cuando el usuario decide el destino de una petición que hace el servidor, y el servidor hereda su posición privilegiada en la red.
  • Puede ser con respuesta, parcial o ciega; la ciega se confirma con peticiones salientes hacia un servidor que controlas.
  • Los objetivos típicos son servicios internos, la red privada y los metadatos de la nube.
  • Las listas negras de texto se saltan con otras representaciones de IP, redirecciones o DNS rebinding.
  • Se corrige con listas blancas, validando la IP resuelta, conectando a esa IP sin seguir redirecciones, limitando protocolos, segmentando la red y exigiendo IMDSv2.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría