// 執筆:ポー・ニング・チェン
目次
ここ数年、セキュリティ研究者は SSRF(Server-Side Request Forgery)という攻撃手法への関心を高めています。SSRF を使えば、攻撃者はリクエストのターゲットを内部サービスに移し、ネットワーク内の暗黙の信頼を悪用できます。脅威がエスカレートして重大な脆弱性につながることも多く、2021 年には、Open Web Application Security Project によってウェブ アプリケーションのセキュリティ リスク トップ 10 に数えられています。Dropbox では、アプリケーション セキュリティ チームが責任を持って、スケーラブルな方法で SSRF からの保護にあたり、できる限り操作性に影響しない安全な製品をエンジニアが提供できるようにしています。
2021 年 2 月 19 日、HackerOne ユーザーのクマール・サウラブ氏から、バグ発見奨励金プログラムを通じて、SSRF の重大な脆弱性に関する報告がありました。この脆弱性を利用すると、攻撃者は本番環境内の内部エンドポイントに HTTP GET リクエストを送り、レスポンスを読み取ることができます。脆弱性を再現した後、直ちに社内セキュリティ インシデントを宣言し、セキュリティ ホールを塞ぐための応急措置を施し、約 8 時間で本番環境に修正を適用しました(この脆弱性が実際に悪用されたと考える理由はなく、リスクにさらされたユーザー データもありませんでした)。
Dropbox 内部のサービスのほとんどは、認証メカニズムが組み込まれた最新の RPC フレームワークである gRPC を使用しているため、プレーンな GET による通信は不可能です。つまり、攻撃者のアクセスには限界があります。それでも、潜在的な影響に関する調査に基づき、奨励金を 27,000 ドル相当と計算しました。
このブログでは、この脆弱性が実際にどのような働きをしたのか、また、今後の SSRF 攻撃のリスクを大幅に減らすために私たちがどのように対応したのかをご紹介します。
1. バグの内容
Dropbox にはブランデッド共有という機能があります。この機能により、チーム用 Dropbox ユーザーと Pro ユーザーは組織のロゴや背景画像をアップロードして、すべての共有リンクと Dropbox 内から送信する共有メールに表示できます。
この機能を設定する場合、クライアントはまず img_url
引数を付けて /team/admin/team_logo/save
または /team/admin/team_background/save
にリクエストを送信します。次に、これらのエンドポイントは、curl
を呼び出して指定された URL から画像データを取得します。指定された img_url
のドメインは dl-web.dropbox.com
のはずです。Dropbox はこの URL の証明機関を検証しますが、使用する 2 つのライブラリ間の URL 解析の相違と、URL API の検証の遅れにより、悪用される余地が残ります。
実際、curl
コマンドをだまして、内部ネットワークを含む任意の URL にリクエストを送信させ、結果のデータをデータベースに保存させることができます。その後、https://www.dropbox.com/team/team_logo/[dbtid]
または https://www.dropbox.com/team/team_background/[dbtid]
から保存したデータを読み出すことが可能です。
以下は、脆弱性のあるコードの実例です。
parsed_url = URI.parse(img_url)
if BLOCK_CLUSTER != parsed_url.authority:
raise HttpStatusBadRequestException()
conn, url = CurlConnection.build_connection_url(img_url)
response = conn.perform("GET", url)
URI
は、URI の解析、変更、構築、シリアル化を処理し、いくつかの追加検証を行うために構築されたライブラリです。ブラウザとの互換性を維持するために若干の変更を加えた RFC 3986 Appendix B に従い、正規表現を使用して URI を解析します。CurlConnection
は、pycurl
でリクエストを行うために構築したもう 1 つのヘルパー ライブラリです。build_connection_url
の内部では、Python の標準ライブラリである urllib.parse
を使用して URL を解析しています。
URL の解析に一貫性がないことから、2017 年の Black Hat のこの講演で解説されているような SSRF の脆弱性を引き起こす恐れがありました。ペイロードの例は https://dl-web.dropbox.com\@<host>:<port>
です。これを URI
ライブラリで解析すると、次のように \@
の前の部分が証明機関として返され、チェックに合格します。
In [1]: URI.parse('https://dl-web.dropbox.com\@127.0.0.1:8080').authority
Out[1]: 'dl-web.dropbox.com'
ところが、これを urlsplit
で解析すると、次のように \@
の後ろがホスト名として扱われ、リクエストが攻撃者の指定したアドレスに誘導されます。
In [1]: urlsplit("https://dl-web.dropbox.com\@127.0.0.1:8080").hostname
Out[1]: '127.0.0.1'
2. 修正方法
URI
クラスの主な目的は正規化です。したがって、parsed_url
に str
を適用すると、シリアル化の間に URI
が検証を実施して、一部の一般的でないパターンを拒否するので、悪意のあるペイロードを実際にブロックできます。最も簡単な修正方法は次のとおりです。
parsed_url = URI.parse(img_url)
+try:
+ safe_url = str(parsed_url)
+except Exception as e:
+ raise HttpStatusBadRequestException()
if BLOCK_CLUSTER != parsed_url.authority:
raise HttpStatusBadRequestException()
-conn, url = CurlConnection.build_connection_url(img_url)
+conn, url = CurlConnection.build_connection_url(safe_url)
しかし、もう少し良い解決策は、ユーザー入力に有効なドメインが含まれるかどうかを検証するのではなく、意図したドメインで URL を構築することです。そうすれば、ユーザーが指定した生の URL にリクエストを送信しなくなります。この解決策は、次のようになります。
try:
safe_uri = str(
URI(
scheme="https",
authority=BLOCK_CLUSTER,
path=args.path,
query=args.query,
)
)
conn, url = CurlConnection.build_connection_url(safe_url)
except Exception as e:
raise HttpStatusBadRequestException()
3. セキュア バイ デフォルト」の安全性
差し迫った脆弱性の問題を解決した後、事後分析のためにいくつかのチームが集まり、SSRF の脆弱性に関するリスクを Dropbox 全体でさらに減らすにはどうすればよいかを話し合いました。そして、デフォルトで安全なウェブ リクエストを行うためのフレームワークを構築することにしました。
最終的な目標は、送信 HTTP リクエストが誤って内部のプライベート アドレスに到達するのを防ぐことです。理論的には、URL を解決してプライベート アドレス空間に属しているかどうかを確認できますが、いくつか注意すべき点があります。
- 一貫性のない URL の解析:検証と実際のリクエスト ライブラリで異なる URL パーサーを採用すると、仕様が曖昧であるために URL の解釈が異なる可能性があります。攻撃者は、複数の言語として解釈可能な URL を作成して検証をバイパスできます。
- HTTP リダイレクト:最初のリクエストのみを検証している場合、攻撃者はそれを内部アドレスにリダイレクトし、その後のリクエストの検証をバイパスできます。
- DNS の再バインド:検証と実際のリクエスト ライブラリがそれぞれ独自に URL を IP に解決しているとします。その場合、攻撃者は最初の DNS 参照で安全な IP を返し、2 回目の参照でプライベート IP を返すことで、検証をバイパスできます。
これらの問題に対処するには、URL の解析、IP アドレスの検証、実際のリクエストを一貫して行う、バイパスできない一元的な場所を確保する必要があります。
Advocate など一部のライブラリは、これをアプリケーション レイヤーで実装しています。Dropbox では、以下の理由から、より下層のネットワーク/プロキシ レイヤーに軽減策を導入しています。
- Dropbox では一元的な構成が可能であり、Envoy と HTTP RBAC フィルターを使用してそれを実現しています。
- サポートする言語ごとに解決策を見つけ出す必要はありません。
- Dropbox がコントロールできないサードパーティのライブラリやバイナリを対象とするものです。
- プログラミング言語の URL パーサーとリクエスト ライブラリの URL パーサーとの間の不一致を心配する必要はありません。
- 目的に応じてカスタマイズした、よりきめ細かな ACL の構成が容易になります。たとえば、Dropbox は自社の企業ネットワークへのリクエストが許可された少数のターゲットを登録したプロキシを構成しています。また、一部の Dropbox アセットが Webhook リクエストを受信できないようにブロックしています。
- 直接の送信リクエストをブロックすることで、プロキシの利用を強制できます。
最後に注意すべき点は、特別なプロトコルです。検証が HTTP リクエストに対してのみ行われる場合、攻撃者は file://
や gopher://
などの URL スキームを攻撃に利用できます(古いライブラリの中には、これらの特別なプロトコルへの HTTP リダイレクトに従うものがあるため、最初のリクエストのみの検証では、やはり不十分な場合があります)。
1 つの解決策は、HTTP/HTTPS 以外のプロトコルをすべて無効化することです。Dropbox は curl
ベースのライブラリを使用しているため、CURLOPT_PROTOCOLS
や CURLOPT_REDIR_PROTOCOLS
などのオプションの設定によってそれを実現することも可能ですが、代わりに libcurl
を再コンパイルして、他のプロトコルのサポートを削除することを選びました。その方がより防御が堅く、システムへのインテグレーションが容易だからです。
この防御策は、Webhook リクエストに対してはすでに採用されていましたが、このインシデントの後はすべての送信リクエストに範囲を拡げ、互換性のないユースケースをすべて移行しました。現在は、SSRF 攻撃が機密性の高い内部ネットワークをターゲットにすることはデフォルトで不可能です。
4. 保護のさらなる強化
これまで述べた SSRF 対策は社内ネットワーク全体を隔離するには有効ですが、Dropbox では、もう一歩踏み込んで本番環境ネットワークでも攻撃対象領域を減らしたいと考えていました。弊社の製品サービスのほとんどは gRPC を使用しており、アクセス制御をデフォルトで拒否するポリシーが適用されているため、SSRF 攻撃による悪用は困難です。しかし、誤った HTTP リスナーが機密データにアクセス可能な場合など、隙間に入り込む可能性のあるものを捕らえるセーフティ ネットを提供するため、セキュア バイ デフォルトの枠組みに沿った総合的なソリューションを目指しました。
考案した 「Auth Everywhere」というプロジェクトでは、本番環境のすべての接続を認証、承認し、監査可能にすることを義務付けました。これを実現するため、HTTP サービスに加えてサイドカー プロキシを導入し、ACL 内のクライアントからの mTLS 接続のみを受け付けるようにしました。Auth Everywhere では、SSRF の脆弱性を処理するために多層防護のアプローチをとり、最小権限の原則に基づいて本番環境のインフラストラクチャを強化し、稼働しているホストが侵害された場合の影響を一般的に制限しています。
5. まとめ
最近の講演でいくつかの斬新な技法に触れて以来、SSRF は最大の関心事の 1 つになっています。バグ発見奨励金プログラムを通じてセキュリティ研究者の協力を得たことで、この記事で詳述した重大な脆弱性を攻撃者に悪用される前に修正できました。また、この SSRF のケースを解決する過程で、この機会を捉えてシステムを幅広く強化し、今後の SSRF のリスクを大幅に減らしました。
脆弱性を見つけて報酬を得ることに興味がある方は、ぜひバグ発見奨励金プログラムをご確認ください。