White-Box Auditing & Săn Bug Thực Chiến
~15 phútRace Conditions & Concurrency ExploitsMiễn phí

Mini-challenge: Khai thác Limit Overrun trên Mock API Thanh toán

Thử thách độc lập: Sử dụng script Python bất đồng bộ kết hợp HTTP/2 để khai thác lỗ hổng rút tiền kép trên mock server và viết remediation diff vá lỗi.

TL;DR: Thử thách này cung cấp cho bạn một mock API thanh toán nội bộ giả lập hành vi của một cổng chuyển tiền ngân hàng. Tài khoản thử nghiệm của bạn có số dư ban đầu là 100 USD. Bằng cách viết một script Python tận dụng kỹ thuật gửi gói tin đồng bộ cao độ, mục tiêu của bạn là rút thành công số tiền 200 USD (Double Spending). Sau khi lấy được cờ thành công từ server, nhiệm vụ tiếp theo của bạn là viết một bản vá mã nguồn (Remediation Diff) sử dụng atomic database update để loại bỏ hoàn toàn cửa sổ chạy đua.

1. Giới thiệu kịch bản thử thách

Nhớ lại —

Nhớ lại mô hình Check-then-Act: Khi khoảng thời gian giữa kiểm tra số dư và cập nhật số dư có độ trễ (I/O, network), kẻ tấn công có thể chen nhiều request vào giữa để kích hoạt race condition.

Nhớ lại —

Nhớ lại kỹ thuật Single-Packet: Để vượt qua race window ngắn, các request cần được gửi gần như đồng thời để triệt tiêu network jitter.

Nhớ lại —

Nhớ lại chiến lược phòng thủ: Không dùng cờ in-memory trong môi trường đa tiến trình mà phải khóa atomic hoặc dùng cơ chế lock theo khóa định danh.

Bạn được giao nhiệm vụ đánh giá bảo mật cho một microservice xử lý rút ví điện tử (wallet-service). Dịch vụ này được viết bằng Python FastAPI kết hợp SQLite/PostgreSQL giả lập.

Dưới đây là mã nguồn của file server.py đang chạy tại cổng 8000:

Python
# server.py - Vulnerable Wallet Service
import asyncio
import time
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# In-memory mock database
db = {
    "user_1": {"balance": 100, "flag": None}
}

class WithdrawRequest(BaseModel):
    user_id: str
    amount: int

@app.post("/api/v1/wallet/withdraw")
async def withdraw(req: WithdrawRequest):
    account = db.get(req.user_id)
    if not account:
        raise HTTPException(status_code=404, detail="User not found")

    # Step 1: Check balance (TOCTOU check)
    current_balance = account["balance"]
    if current_balance < req.amount:
        raise HTTPException(status_code=400, detail="Insufficient funds")

    # Simulating complex business validation or network I/O
    await asyncio.sleep(0.05)  # 50ms race window!

    # Step 2: Deduct balance (TOCTOU update)
    account["balance"] = current_balance - req.amount

    # Reward flag if user successfully withdrew more than initial 100 USD
    if account["balance"] < 0:
        account["flag"] = "FLAG{R4C3_C0ND1T10N_D0UBL3_SP3ND_PWN3D}"

    return {
        "status": "success",
        "withdrawn": req.amount,
        "remaining_balance": account["balance"],
        "flag": account["flag"]
    }

@app.get("/api/v1/wallet/{user_id}")
async def get_balance(user_id: str):
    return db.get(user_id, {})

Nhìn vào withdraw(), bạn thấy rõ một race window dài 50ms giữa lệnh đọc current_balance và lệnh ghi account["balance"].

2. Nhiệm vụ của bạn

Nhiệm vụ 1: Dựng môi trường thử nghiệm

Lưu đoạn code trên vào file server.py và khởi động bằng lệnh:

Bash
pip install fastapi uvicorn httpx
uvicorn server:app --host 127.0.0.1 --port 8000 --workers 2

Nhiệm vụ 2: Viết script khai thác đồng bộ (Exploit Script)

Viết file exploit.py sử dụng thư viện httpx (hỗ trợ HTTP/2 và async) hoặc asyncio để:

  1. Gửi đồng thời 5 request rút 80 USD cho user_1 (vốn chỉ có 100 USD).
  2. Yêu cầu các request này xuất phát đồng thời nhất có thể để vượt qua bước kiểm tra số dư.
  3. Mục tiêu: Rút thành công ít nhất 2 lần (tổng tiền rút 160 USD > 100 USD ban đầu), khiến số dư bị âm và máy chủ trả về flag.

Nhiệm vụ 3: Viết bản vá Remediation Diff

Sửa lại hàm withdraw() trong server.py sao cho:

  1. Ngăn chặn triệt để hiện tượng Double Spending.
  2. Dù kẻ tấn công có gửi 100 requests cùng 1 nano-giây thì tổng số tiền rút cũng không bao giờ vượt quá 100 USD.
  3. Không làm nghẽn toàn bộ server khi có nhiều user khác nhau cùng giao dịch.

3. Không gian nháp và gợi ý hướng đi

Trước khi xem đáp án mẫu bên dưới, hãy tự đặt câu hỏi:

  • Tại sao dùng asyncio.gather(*tasks) với httpx.AsyncClient(http2=True) lại có thể kích hoạt race condition nội bộ dễ dàng hơn so với requests.post chạy tuần tự trong vòng lặp for?
  • Ở tầng cơ sở dữ liệu thật, giải pháp nào là tối ưu nhất giữa SELECT FOR UPDATE và UPDATE ... WHERE balance >= amount?

4. AI Copilot Slot: Prompt hỗ trợ phân tích và debug race condition script

AI Copilot Prompt Slot
Role:Exploit Developer
Task:Viết script asyncio bất đồng bộ kiểm thử Race Window trên API REST
Context:Endpoint POST /api/v1/wallet/withdraw có độ trễ 50ms giữa khâu xác thực và cập nhật số dư
Output:Mã nguồn Python sử dụng httpx.AsyncClient và asyncio.gather kèm logic bắt Flag
Tôi đang giải một bài lab mini-challenge về Race Condition.
Target: POST http://127.0.0.1:8000/api/v1/wallet/withdraw
Payload: {"user_id": "user_1", "amount": 80}
Số dư ban đầu: 100 USD.

Hãy viết script Python bất đồng bộ hoàn chỉnh bằng thư viện httpx:
1. Tạo 5 coroutine độc lập gửi request rút tiền.
2. Bắn cả 5 coroutine đồng thời bằng asyncio.gather.
3. Kiểm tra response trả về, in ra số dư cuối cùng và trích xuất Flag nếu thành công.

5. Bẫy tư duy thường gặp

  • Chạy request trong vòng lặp tuần tự: Rất nhiều bạn viết vòng lặp for i in range(5): requests.post(...). Khi request 1 hoàn tất thì request 2 mới bắt đầu gửi, do đó không bao giờ xuất hiện race condition. Bắt buộc phải dùng asyncio, threading hoặc HTTP/2 multiplexing.
  • Vá lỗi bằng cách dùng asyncio.Lock() toàn cục: Việc này sẽ biến toàn bộ server thành đơn luồng tuần tự (single-threaded bottleneck). Nếu User A đang rút tiền thì User B cũng phải xếp hàng chờ, làm giảm hiệu năng hệ thống thảm hại. Hãy dùng khóa theo từng User ID hoặc khóa atomic ở tầng database!

6. Liên hệ các bài khác

Tự kiểm tra
0/3 câu đã trả lời
  1. Q1
    Đáp án mẫu mã khai thác (Nhiệm vụ 2): Script Python asyncio giải bài lab này như thế nào?
  2. Q2
    Đáp án mẫu bản vá (Nhiệm vụ 3): Cách sửa hàm withdraw() an toàn bằng khóa theo user hoặc kiểm tra nguyên tử?
  3. Q3
    Tại sao uvicorn cần chạy với cờ --workers 2 trong kịch bản kiểm thử này?

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tổng kết module Race Conditions & Concurrency Exploits