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.06 億人分の S3 データが露出したと主張しています。トークンを必須にし、ロールを制限します。[10], [2]

💡 わかりやすい解説 (ELI5)

DOJ の刑事告訴状が主張した経路は次のように例えられます。指定した住所から書類を取ってくる受付係を想像してください。リクエストの中継により、誰かが受付係を社内の部屋へ誘導できてしまいます。そこで受付係はワークロード用の一時的な入館証を取得できます。その証で入れるのは、関連付けられたロールが許可する部屋だけです。設計者は社員しかその部屋に行けないと思い込んでいましたが、受付係をそこへ誘導できました。[10], [8]

主要な概念と専門用語

SSRF
サーバーサイドリクエストフォージェリ(CWE-918)。攻撃者が選んだリクエストをサーバーに送らせ、そのリクエストにサーバーのネットワーク上の位置と信頼を持たせる手法です。[9]
Web アプリケーションファイアウォール
Web アプリケーションの前段に置くフィルター。刑事告訴状は、Capital One の WAF の設定ミスにより内部アドレスへの要求を中継できたと主張しています。[10]
IMDSv1
IMDSv1 が有効な場合、メタデータ要求にトークンは不要です。IAM ロールのセキュリティ認証情報パスは、そのロールに関連付けられた一時認証情報を返します。[7], [8]
IMDSv2
IMDSv2 は PUT リクエストで取得するセッショントークンを使います。HttpTokens=required に設定すると、トークンなしの IMDSv1 要求を拒否できます。単純な 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 の設定ではありません。許可ホストリストがアプリケーション側の主な制御です。必須の送信プロキシは接続時に解決後のアドレスへポリシーを適用し、インスタンスメタデータを含むプライベートおよび 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

エンジニアリング&システム堅牢化チェックリスト

参考資料