FlawOpen

flawopen.com/Incidents/capital-one-ssrf-imds-july-2019

● Capital One · CWE-918 · July 2019 · Crítica
Investigación · FlawOpen

SSRF en Capital One: robo de credenciales AWS IMDSv1

La denuncia federal alegó que un WAF mal configurado permitió SSRF hasta IMDSv1 de AWS y expuso credenciales temporales del rol y datos S3 de unos 106 millones. Exige tokens y limita los roles.[10], [2]

💡 Explicación en Lenguaje Sencillo (ELI5)

La denuncia penal del DOJ describe una cadena como esta: Imagina una recepcionista que recoge documentos en las direcciones que le indicas. Un sistema de reenvío puede permitir que alguien la dirija a una oficina interna. Allí puede obtener una credencial temporal de acceso para la carga de trabajo; esa credencial solo abre las salas permitidas por el rol asociado. El error fue suponer que solo el personal llegaría a esa oficina, aunque se podía dirigir allí a la recepcionista.[10], [8]

Conceptos Clave y Términos

SSRF
Server-side request forgery (CWE-918): hacer que un servidor envíe una petición elegida por el atacante, de modo que lleve la posición de red y la confianza del servidor.[9]
Web application firewall
Un filtro delante de una aplicación web. La denuncia penal alegó que el WAF mal configurado de Capital One podía reenviar peticiones a direcciones internas.[10]
IMDSv1
Cuando IMDSv1 está habilitado, las peticiones de metadatos no exigen un token. La ruta de credenciales de seguridad del rol IAM devuelve credenciales temporales asociadas a ese rol.[7], [8]
IMDSv2
IMDSv2 usa un token de sesión que se obtiene con una petición PUT. Configura HttpTokens=required para rechazar las peticiones IMDSv1 sin token; esto bloquea muchos reenvíos SSRF que solo pueden enviar un GET simple, pero no elimina por completo el riesgo de SSRF.[7], [6]
Credenciales de rol IAM
Claves temporales de AWS emitidas a una instancia. Todo lo que el rol puede hacer lo puede hacer también quien tenga las claves hasta que caduquen.[8]

Mapeos de marcos

Cronología del Incidente

2019-03-22

Según la denuncia penal, entre el 22 y el 23 de marzo la intrusa usó SSRF para obtener las credenciales del rol del firewall y copiar datos S3 de Capital One.[10], [2]

2019-07-17

Un investigador externo avisa de los datos expuestos a través del programa de divulgación responsable de Capital One.[2]

2019-07-29

Capital One anuncia el incidente y el FBI detiene a una sospechosa en Seattle.[1]

Flujo de Ataque Paso a Paso

Paso 1

El firewall reenvía una petición manipulada

La denuncia penal alega que se podía inducir al WAF mal configurado a enviar peticiones a direcciones elegidas por el atacante, incluidas direcciones internas.[10]

Paso 2

El servicio de metadatos entrega credenciales

Según la denuncia, el reenvío alcanzó la ruta de credenciales del rol IAM del servicio de metadatos de la instancia. Como IMDSv1 no exigía un token de sesión, la petición podía devolver credenciales temporales del rol asociado.[10], [8]

Paso 3

El rol puede leer demasiado

La denuncia alega que las credenciales tenían los permisos del rol, incluidos listar y leer buckets S3 con datos de clientes.[10], [8]

Paso 4

Los datos salen en masa

Se copiaron datos de solicitudes de tarjetas de crédito desde 2005 hasta principios de 2019: unos 100 millones de personas en EE. UU. y 6 millones en Canadá, con unos 140.000 números de la Seguridad Social estadounidense y 80.000 números de cuentas bancarias vinculadas.[2]

Código Fuente: Vulnerable vs. Seguro

Código Flask ilustrativo; no es la configuración de Capital One. La lista permitida de hosts es el control principal de la aplicación. El proxy de salida obligatorio debe aplicar la política al destino resuelto en el momento de la conexión, bloquear rangos privados y link-local, incluido el servicio de metadatos, y repetir las comprobaciones en cada petición y reintento; verifica ese comportamiento en la configuración del proxy. No se siguen redirecciones. Una comprobación de resolución separada antes de conectar dejaría una condición de carrera de DNS rebinding.[9], [5], [6]

IMPLEMENTACIÓN VULNERABLE
import requests
from flask import Flask, request

app = Flask(__name__)


@app.get("/fetch")
def fetch():
    # El servidor obtiene cualquier URL que proporcione quien llama.
    url = request.args["target"]
    resp = requests.get(url, timeout=5)
    return resp.text
PARCHE SEGURO Y ROBUSTO
from urllib.parse import urlsplit

import requests
from flask import Flask, abort, request

app = Flask(__name__)
ALLOWED_HOSTS = {"images.example-cdn.com", "api.partner.example"}



EGRESS_PROXY = "http://egress-proxy.internal:3128"
# Configure este proxy para aplicar la política de destino después de resolver DNS.

@app.get("/fetch")
def fetch():
    parts = urlsplit(request.args.get("target", ""))
    if parts.scheme != "https" or parts.hostname not in ALLOWED_HOSTS:
        abort(400, "destination not allowed")
    if parts.port not in (None, 443) or parts.username or parts.password:
        abort(400, "destination not allowed")
    resp = requests.get(parts.geturl(), timeout=(2, 5), allow_redirects=False, proxies={"https": EGRESS_PROXY}, trust_env=False)
    if resp.is_redirect:
        abort(502, "upstream redirect refused")
    return resp.text

Lista de Verificación de Seguridad para Ingeniería

Fuentes