FlawOpen

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

● Capital One · CWE-918 · July 2019 · 심각
보안 연구 · FlawOpen

Capital One SSRF: AWS IMDSv1 자격 증명 탈취

연방 형사 고소장은 잘못 구성된 WAF가 SSRF로 AWS IMDSv1에 접근하게 해 임시 역할 자격 증명과 약 1억 600만 명의 S3 데이터를 노출했다고 주장했습니다. 토큰을 필수화하고 역할 권한을 제한하세요.[10], [2]

💡 알기 쉬운 설명 (ELI5)

DOJ 형사 고소장은 다음과 같은 경로를 주장했습니다: 말해 준 주소에서 서류를 가져오는 안내 직원을 떠올려 보세요. 요청 중계 기능 때문에 누군가 그 직원을 건물 안쪽 사무실로 보낼 수 있습니다. 그곳에서 직원은 워크로드용 임시 출입증을 받을 수 있으며, 출입증은 연결된 역할이 허용한 방만 열 수 있습니다. 설계자는 직원만 그 사무실에 갈 수 있다고 생각했지만, 안내 직원을 그곳으로 유도할 수 있었습니다.[10], [8]

핵심 개념 및 용어

SSRF
서버 측 요청 위조(CWE-918): 공격자가 고른 요청을 서버가 보내게 만들어, 그 요청이 서버의 네트워크 위치와 신뢰를 갖게 하는 것입니다.[9]
웹 애플리케이션 방화벽
웹 애플리케이션 앞단의 필터입니다. 형사 고소장은 Capital One의 잘못 구성된 WAF가 내부 주소로 요청을 중계할 수 있었다고 주장했습니다.[10]
IMDSv1
IMDSv1이 활성화되어 있으면 메타데이터 요청에 토큰이 필요하지 않습니다. IAM 역할의 보안 자격 증명 경로는 해당 역할에 연결된 임시 자격 증명을 반환합니다.[7], [8]
IMDSv2
IMDSv2는 PUT 요청으로 얻은 세션 토큰을 사용합니다. 토큰이 없는 IMDSv1 요청을 거부하도록 HttpTokens=required를 설정하세요. 이는 단순 GET만 보낼 수 있는 많은 SSRF 중계를 막지만 SSRF 전체를 차단하지는 않습니다.[7], [6]
IAM 역할 자격 증명
인스턴스에 발급되는 임시 AWS 키입니다. 역할이 할 수 있는 일은 키를 가진 사람도 만료될 때까지 모두 할 수 있습니다.[8]

프레임워크 매핑

사고 타임라인

2019-03-22

형사 고소장에 따르면 3월 22~23일 피고인은 SSRF를 사용해 방화벽 역할 자격 증명을 얻고 Capital One의 S3 데이터를 복사했습니다.[10], [2]

2019-07-17

외부 연구자가 Capital One의 책임 있는 공개 프로그램을 통해 노출된 데이터를 신고합니다.[2]

2019-07-29

Capital One이 사고를 발표하고 FBI가 시애틀에서 용의자를 체포합니다.[1]

단계별 공격 실행 흐름

단계 1

방화벽이 조작된 요청을 중계한다

형사 고소장은 잘못 구성된 WAF가 공격자가 고른 주소(내부 주소 포함)로 요청을 보내도록 만들 수 있었다고 주장합니다.[10]

단계 2

메타데이터 서비스가 자격 증명을 넘겨준다

고소장에 따르면 중계 요청은 인스턴스 메타데이터 서비스의 IAM 역할 자격 증명 경로에 도달했습니다. IMDSv1은 세션 토큰을 요구하지 않아 연결된 역할의 임시 자격 증명을 반환할 수 있었습니다.[10], [8]

단계 3

역할이 너무 많은 것을 읽을 수 있다

고소장은 자격 증명에 역할 권한이 포함되어 고객 데이터가 있는 S3 버킷을 나열하고 읽을 수 있었다고 주장합니다.[10], [8]

단계 4

데이터가 대량으로 빠져나간다

2005년부터 2019년 초까지의 신용카드 신청 데이터가 복사되었습니다. 미국 약 1억 명, 캐나다 약 600만 명 분량이며, 미국 사회보장번호 약 14만 건과 연결된 은행 계좌번호 8만 건이 포함되었습니다.[2]

소스 코드 비교: 취약한 구현 vs 보안 패치

이 Flask 코드는 설명용이며 Capital One의 설정이 아닙니다. 호스트 허용 목록은 애플리케이션의 기본 제어입니다. 필수 egress 프록시는 연결 시 DNS 확인 결과 주소에 정책을 적용하고, 인스턴스 메타데이터를 포함한 사설 및 link-local 범위를 차단하며, 모든 요청과 재시도에서 같은 검사를 반복해야 합니다. 프록시 설정에서 실제 동작을 확인하세요. 리디렉션은 따라가지 않습니다. 애플리케이션에서 먼저 DNS를 확인하고 나중에 연결하면 DNS rebinding 경쟁 조건이 남습니다.[9], [5], [6]

취약한 구현
import requests
from flask import Flask, request

app = Flask(__name__)


@app.get("/fetch")
def fetch():
    # 서버는 호출자가 제공한 URL을 가져옵니다.
    url = request.args["target"]
    resp = requests.get(url, timeout=5)
    return resp.text
보안 강화 패치
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"
# 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

엔지니어링 및 시스템 보안 강화 체크리스트

출처