Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

GitHubの内部ネットワークにアクセス可能な脆弱性(SSRF)を報告した話

はじめに

こんにちは、株式会社Flatt SecurityでセキュリティエンジニアをやっているRyotaK (@ryotkak) です。

HackerOneのイベント (H1-512) に参加するためにテキサスに行った話で紹介したイベントにおいて報告したSSRF(サーバーサイドリクエストフォージェリ)に関して、脆弱性情報を公開する許可が得られたため、今回の記事ではその脆弱性に関して解説を行います。

なお、本記事で解説している脆弱性はGitHub Bug Bountyプログラムのセーフハーバーに則り行われた脆弱性調査の結果発見され、公開を行う許可を得たものであり、無許可の脆弱性診断行為を推奨することを意図したものではありません。 GitHubが開発するプロダクトやサービスに脆弱性を発見した場合は、GitHub Bug Bountyへ報告してください。

GitHub Enterprise Importerについて

今回解説する脆弱性は、当時パブリックベータ中だったGitHub Enterprise Importerと呼ばれる機能に存在しました。

この機能は以下の環境からGitHub Enterprise Cloudに対して、リポジトリデータだけでなくプルリクエストやそのコメントなど、様々なデータをインポートすることができるものであり、従来のデータ移行時には引き継ぐことができなかったデータをGitHub Enterprise Cloudに対して引き継ぐことができます。

  • Azure DevOps (ADO) クラウド
  • Bitbucket Server と Bitbucket Data Center 5.14 以降
  • GitHub.com
  • GitHub Enterprise Server (GHES) 3.4.1 以降

(引用元: GitHub Enterprise Importer について)

上記の図の通り、GitHub Enterprise Importerを使用することで、GitHubから各種連携先に対してリクエストを送信し、様々なデータを取得した上でGitHub上で使用可能な形へ変換した上でインポートすることができるという機能となっています。

この機能の内、Azure DevOpsと連携するためのコードにおいて脆弱性が存在し、GitHubの内部ネットワークに存在する任意のホストへリクエストを送信し、そのレスポンスを読み取ることができました。

脆弱性があったコード

GitHub Enterprise Importerにおいて、Azure DevOpsからデータを取得するためのライブラリとして、Faradayが用いられていました。1 (上記の図の①の部分) このライブラリはHTTPリクエストを送信するためのライブラリであり、コネクションオブジェクトを生成してそれをリクエスト毎に使い回す、という使用方法ができる仕様となっていました。

そして、GitHub Enterprise Importerにおいては以下のようにコネクションオブジェクトが生成されていました。

      def build
        connection = Faraday::Connection.new(
          url: url,
          builder: FaradayMiddleware::MiddlewareStack.default
        ) do |conn|
          conn.options.proxy = Rails.configuration.x.octoshift.octoshift_proxy
          [...]
        end

        connection.request(:basic_auth, "", access_token)
        connection
      end

このコードでは、conn.options.proxyに内部ネットワークにアクセスできないように構成されたOctoshiftと呼ばれるプロキシ(上記の図の②の部分)を設定し、内部ネットワークへのアクセスを遮断するような仕組みとなっていました。

しかしながら、このコードにはプロキシを回避し、内部ネットワークへアクセス可能な脆弱性が存在します。 どのようにしてプロキシを回避することができるのかわかりますか?

脆弱性の解説

このコードがなぜ脆弱であるのかを理解するためには、Faradayのコードを確認する必要があります。

Faradayは、Faraday:Connection.newが呼ばれた際に、以下のようなコードを実行します。

    def initialize(url = nil, options = nil)
      options = ConnectionOptions.from(options)

      if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
        options = Utils.deep_merge(options, url)
        url     = options.url
      end

      @parallel_manager = nil
      @headers = Utils::Headers.new
      @params  = Utils::ParamsHash.new
      @options = options.request
      @ssl = options.ssl
      @default_parallel_manager = options.parallel_manager
      @manual_proxy = nil

      @builder = options.builder || begin
        # pass an empty block to Builder so it doesn't assume default middleware
        options.new_builder(block_given? ? proc { |b| } : nil)
      end

      self.url_prefix = url || 'http:/'

      @params.update(options.params)   if options.params
      @headers.update(options.headers) if options.headers

      initialize_proxy(url, options)

      yield(self) if block_given?

      @headers[:user_agent] ||= USER_AGENT
    end

ここで重要になってくるのは以下の2つの行です。

      initialize_proxy(url, options)

      yield(self) if block_given?

上記のコードからわかるように、FaradayはFaraday::Connection.newに渡されたブロック引数を実行する前にinitialize_proxyを実行しています。

    def initialize_proxy(url, options)
      @manual_proxy = !!options.proxy
      @proxy =
        if options.proxy
          ProxyOptions.from(options.proxy)
        else
          proxy_from_env(url)
        end
    end

この関数は名前の通り、プロキシ設定を行うためのもので、オプションで指定されたプロキシを設定する他、環境変数からプロキシ設定を読み取り、Faradayで使用するように設定しています。

ここで思い出してほしいのが、先程のコードです。

      initialize_proxy(url, options)

      yield(self) if block_given?

このコードでは、initialize_proxy関数をブロック引数を実行する前に実行しています。

つまり、ブロック引数内で実行されている以下のコードが実行される前にinitialize_proxy関数へ到達するのです。

conn.options.proxy = Rails.configuration.x.octoshift.octoshift_proxy

これにより、ここで設定されているプロキシ設定はFaraday側で尊重されず、結果としてこのプロキシにより達成されるはずだった内部ネットワークへのリクエスト制限を回避することが可能となります。

そして、Azure DevOpsからファイルを取得する際にこのコードが使用されていたため、添付ファイルのデータに細工を行うことで以下のようにGitHubの内部ネットワークへ到達することが可能となっていたのです。

GitHubの内部ネットワーク内に存在するSSRFをテストするためのサーバーからのレスポンス

なお、Faradayのドキュメントではプロキシ設定を以下のように設定しており、この場合は上記で解説した挙動の影響を受けずに正しくプロキシ設定を行うことができます。

conn = Faraday.new('http://www.example.com', proxy: 'http://proxy.com')

脆弱性による影響

この脆弱性により、GitHubの内部ネットワークへ任意のGETリクエストを送信し、そのレスポンスを読み取ることが可能となっていました。 しかしながら、GitHub側の規定の中に「内部ネットワークへのアクセスができた時点で報告を行い、それ以上の調査を行ってはいけない」というものがあるため、正確な影響に関しては今もわかっていません。

まとめ

本記事では、筆者がGitHubに報告した脆弱性の内容について解説しました。

一見適切にライブラリが構成されているように見えても、ライブラリ側の挙動によって想定した動作をせず、結果として脆弱性を作り込んでしまうという観点で、非常に面白い脆弱性となっているかと思います。

GitHubにおいてもこのような深刻度の高い脆弱性があるということで、定期的に脆弱性診断を実施する必要性や、バグバウンティなどを用いて継続的な脆弱性調査を行っていくことの必要性を感じていただけますと幸いです。

また、GitHub Bug BountyプログラムをホストしているバグバウンティプラットフォームであるHackerOneには、東京にHackerOne Tokyo Clubと呼ばれるコミュニティグループが存在しています。

HackerOneでバグバウンティをやっている方がいらっしゃいましたら招待いたしますので、ぜひお声がけください!

また、弊社Flatt Securityではセキュリティエンジニアを積極採用中です! 最近リニューアルした採用ページにさまざまな情報を掲載しておりますので、ご興味のある方はぜひご覧ください。

カジュアル面談も以下のフォームよりお申し込み可能です!

ここまでお読みいただきありがとうございました。


  1. なお、GitHubのコードは配布されている際に難読化されており、ライセンスにより明示的に難読化の解除や解析が禁止されていますが、GitHub Bug Bountyプログラムのセーフハーバーにより、脆弱性を調査するという目的に限りこの条項が免除されます。また、ある特定の手法を用いることで、難読化をほぼ完全に解除して難読化前の状態へ戻すことが可能であるため、本記事では難読化を解除した後のコードを用いて解説を行います。