DEF CON CTF 2020 Quals Writeup - pooot

試行錯誤をものすごくした途中経過が書いてあるので解法を知りたい人は一番下までスクロールしてください。

問題の調査

問題文に書かれたURLにアクセスするといわゆるWeb-based Proxyサービス(一昔前だとCGIプロキシと呼ばれていた気もする)であることがわかる。このサイトを使うと、指定したURLにサーバーがアクセスしてくれ、その結果を返してくれる。またfeedback機能として、管理者にURLを送信する機能もある。ガチャガチャして分かったことをまとめると次の通り。

  • /sourceソースコードを見ることができる
  • コードを読むとPythonのrequestsで指定されたURLをGETし、htmlを少し編集し返すだけということがわかる
    • 取得するページはhttps://pooot.challenges.ooo/{Domain}/{Path}の形式で指定する
  • サーバーは、リクエストするURLのホスト部とそれを名前解決した結果が同じ場合はhttpとしてリクエストし、異なる場合はhttpsとしてリクエストする
  • リクエストするURLのホスト部が172.25.から始まる場合において、リクエスト元が172.25.0.11でない場合は処理を拒否する
    • リクエストヘッダにX-Forwarded-For: 172.25.0.11を指定することでこの制約をバイパスできる
  • /feedbackで管理者にURLを報告できる
    • 管理者は指定されてURLに対して、今回の問題のサービスを使ってアクセスする。この際、headless chromeでアクセスしてくれるのでhttps://pooot.challenges.ooo/をオリジンとしてJavaScriptを動かしたりすることができる。

ソースコードを保存し忘れたので、記憶を元に書いており微妙に違う部分があるかも)

解答の方針

問題を見て以下の2パターンを思いついた

  1. 今回のサービスはSSRFができる(というかSSRFをするためのサービス)ため、クラウドサービスのメタデータエンドポイント(169.254.169.254など)から情報を読み出して攻略する
  2. feedback機能で管理者のブラウザを操作して情報を窃取する

前者は現実にありそうな状況*1でWebだけではなく、クラウドサービスに関する知識も求められそうで非常に面白そうだし、後者もWeb-based Proxyサービスの特殊性(全てのサイトのオリジンが同じになるため、CookieやLocalStorage、パーミッションなどが共通となる)を悪用するというのはセキュリティの技術として面白そうだと思って、久しぶりに面白いWeb問なんじゃないかという予感がした。

1. の方針で試したこと

https://pooot.challenges.ooo/169.254.169.254にアクセスするとこのサーバーがGCPで動いていることがわかる。https://pooot.challenges.ooo/169.254.169.254/computeMetadata/v1beta1/instance/service-accounts/default/tokenからaccess_tokenを取得できる。与えられたscopeを見るとdevstorageのreadが(たぶん)付与されていることがわかる。また今回のサーバーがhackpack-ctf-2020(!?)というプロジェクトに属するということがわかる(使い回し?)。

GCPのdevstorageのread権限はCloud Storageのread権限なので、何か面白いオブジェクトがないか探してみる。まずこのaccess_tokenでアクセスできるバケットを調べるとctf2020-vm-imagesというバケットが見つかった。しかし中を覗いても空。何の情報も得られなかった。

今回のサービスはjob queueにRedisを使っているのでGCPのMemorystoreから何か情報が得られないか試行錯誤したり、Dockerで動いている可能性が高いと考え*2、GCRからaccess_tokenを使ってイメージを落とせないか試行錯誤するなど相当の時間を費やした。

2. の方針で試したこと

管理者が管理者用のページに今回のサービスを使ってアクセスしていた場合、管理者ページのオリジンもhttps://pooot.challenges.ooo/となるため何か痕跡が残っているのではないかと考えた。CookieやLocalStorage、SessionStorageなど情報が保存されそうな箇所の中身を、用意したサーバーに送信させるページを用意し管理者にアクセスさせた。しかしこれらの領域は全て空だった。

今回のスコアサーバーはZoomを激しくインスパイアしたものだったので、実は管理者のブラウザにビデオカメラが接続されていてそこにflagが映っているかもしれないと考えたが、カメラなどのデバイスは繋がっていないようだった。

(強引な)解法

172.25.から始まるIPアドレスに対してアクセス制限がかかっていることが気になり、172.25.0.0/24の80と8080番ポートをスキャンしてみることにした。具体的にはhttps://pooot.challenges.ooo/172.25.0.0などにアクセスしまくった。https://pooot.challenges.ooo/172.25.0.103:8080からhttps://pooot.challenges.ooo/の内容が返ってきたが、それ以外のIPアドレスからはアクセスの失敗の応答が返ってきた。ほとんどのIPアドレスでアクセス失敗の応答まで1秒ほどかかっていたが、いくつかのIPアドレスからは0.1秒程度でアクセス失敗の応答が返ってきたので、失敗の応答が早かったIPアドレスにはホストが存在していると考え、これらのIPアドレスに対して1 - 9999番ポートまでスキャンをした。

すると172.25.0.102:3000からflagを含む応答が返ってきた。

ちなみに172.25.0.112, 113, 114あたりでheadless chromeが動いているようで、タイミングによっては9222/TCPでheadless chromeデバッグ接続をすることができた。

きちんとした解法

チームのメンバーから、下記ページにある通り、Service Workerを使うとページを遷移した後でもリクエストを補足できることを利用してflagが書かれたURLを入手できるらしいと教えてもらった。なるほど。

medium.com

チームのメンバーからはこの問題の出典(といえばいいのか?)が下記の論文であるということも教えてもらった。

www.ndss-symposium.org

*1:例えば有名どころではShopifyの事例: https://hackerone.com/reports/341876

*2:Metadata endpointによるとサーバーのIPアドレスは10.x.x.xであるが、172.25.0.103:8080からpooot.challenges.oooの内容が返ってきた。つまりコンテナのIPアドレスは172.25.0.103。