FlawOpen

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

● Capital One · CWE-918 · July 2019 · Crítica
Pesquisa · FlawOpen

SSRF na Capital One: roubo de credenciais AWS IMDSv1

A denúncia federal alegou que um WAF mal configurado permitiu SSRF até o IMDSv1 da AWS e expôs credenciais temporárias do papel e dados S3 de cerca de 106 milhões. Exija tokens e limite papéis.[10], [2]

💡 Explicação em Linguagem Simples (ELI5)

A denúncia criminal do DOJ descreve uma cadeia como esta: Imagine uma recepcionista que busca documentos nos endereços que você informa. Um serviço de encaminhamento pode permitir que alguém direcione essa recepcionista a uma sala interna. Lá, ela pode obter um crachá temporário da carga de trabalho; o crachá só abre as salas permitidas pelo papel associado à carga. O erro foi presumir que apenas funcionários chegariam à sala, embora a recepcionista pudesse ser direcionada até lá.[10], [8]

Conceitos Centrais e Termos

SSRF
Server-side request forgery (CWE-918): fazer um servidor enviar uma requisição escolhida pelo atacante, de modo que ela carregue a posição de rede e a confiança do servidor.[9]
Web application firewall
Um filtro diante de uma aplicação web. A denúncia criminal alegou que o WAF mal configurado da Capital One podia encaminhar requisições a endereços internos.[10]
IMDSv1
Quando o IMDSv1 está habilitado, as requisições de metadados não exigem token. O caminho de credenciais de segurança do papel IAM devolve credenciais temporárias associadas a esse papel.[7], [8]
IMDSv2
O IMDSv2 usa um token de sessão obtido com uma requisição PUT. Defina HttpTokens=required para rejeitar requisições IMDSv1 sem token; isso bloqueia muitos repasses SSRF que só conseguem enviar um GET simples, mas não elimina todo o risco de SSRF.[7], [6]
Credenciais de papel IAM
Chaves temporárias da AWS emitidas para uma instância. Tudo o que o papel pode fazer, quem tiver as chaves também pode, até elas expirarem.[8]

Mapeamentos de frameworks

Linha do Tempo do Incidente

2019-03-22

Segundo a denúncia criminal, entre 22 e 23 de março a intrusa usou SSRF para obter as credenciais do papel do firewall e copiar dados S3 da Capital One.[10], [2]

2019-07-17

Um pesquisador externo relata os dados expostos pelo programa de divulgação responsável da Capital One.[2]

2019-07-29

A Capital One anuncia o incidente e o FBI prende uma suspeita em Seattle.[1]

Fluxo de Ataque Passo a Paso

Passo 1

O firewall repassa uma requisição forjada

A denúncia criminal alega que o WAF mal configurado podia ser induzido a enviar requisições para endereços escolhidos pelo atacante, inclusive internos.[10]

Passo 2

O serviço de metadados entrega credenciais

Segundo a denúncia, o encaminhamento alcançou o caminho de credenciais do papel IAM no serviço de metadados da instância. Como o IMDSv1 não exigia token de sessão, essa requisição podia devolver credenciais temporárias do papel associado.[10], [8]

Passo 3

O papel pode ler demais

A denúncia alega que as credenciais tinham as permissões do papel, inclusive listar e ler buckets S3 com dados de clientes.[10], [8]

Passo 4

Os dados saem em massa

Foram copiados dados de pedidos de cartão de crédito de 2005 ao início de 2019: cerca de 100 milhões de pessoas nos EUA e 6 milhões no Canadá, incluindo uns 140 mil números de Seguro Social americanos e 80 mil números de contas bancárias vinculadas.[2]

Código-Fonte: Vulnerável vs. Seguro

Código Flask ilustrativo, não é a configuração da Capital One. A allowlist de hosts é o controle principal da aplicação. O proxy de saída obrigatório deve aplicar a política ao endereço resolvido no momento da conexão, bloquear faixas privadas e link-local, inclusive o serviço de metadados, e repetir os controles em cada requisição e nova tentativa; verifique esse comportamento na configuração do proxy. Redirecionamentos não são seguidos. Uma verificação separada de resolução antes da conexão deixaria uma condição de corrida de DNS rebinding.[9], [5], [6]

IMPLEMENTAÇÃO VULNERÁVEL
import requests
from flask import Flask, request

app = Flask(__name__)


@app.get("/fetch")
def fetch():
    # O servidor busca qualquer URL fornecida pelo chamador.
    url = request.args["target"]
    resp = requests.get(url, timeout=5)
    return resp.text
PATCH SEGURO E 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 a política de destino após a resolução 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 Verificação de Segurança para Engenharia

Fontes