Back to writeups
HTBmiscVery Easysolved

Lucky Dice

3 Jul 2026 · by 0xIDA

Solvers

Lucky Dice

Author: 0xIDA
Platform: HTB · Category: misc · Difficulty: Very Easy

Solvers

Summary

A speed-based dice game where we had to compute the round winner (highest dice sum) for 100 rounds with only 0.3 seconds to answer each round. Solved by pre-computing the winner while the server was still printing dice rolls.

Analysis

The challenge provided the full Python source (challenge.py). The server:

  1. Shows an ASCII dice banner, explains the rules, then asks if we're ready
  2. Runs 100 rounds
  3. Each round, player_nr (random 8–13) players roll (round_nr * 2 + 2) dice each
  4. All dice are 6-sided (random 1–6)
  5. Winner is the player with the highest sum; ties broken by higher player number (via Python's stable sort — sorted(dice_sum.items(), key=lambda x:x[1])[-1] picks the last equal element, which is the player who appeared later in the dict, i.e., higher player number)
  6. A 0.3-second timeout starts after printing "> "

The 0.3-second window is tight, but the server prints each player's dice with time.sleep(0.1) between them. This means we have plenty of time to sum dice and track the leader as the data streams in.

The Tie-Breaker Bug/Trick

result = sorted(dice_sum.items(), key=lambda x:x[1])[-1][0][1].split('_')[1]
  • sorted(..., key=lambda x:x[1]) sorts by sum ascending (ties keep dict order)
  • [-1] picks the last element — highest sum, or if tied, the last tied player in dict order
  • Dict order is player_1 → player_2 → … → player_N, so the highest-numbered player with the max sum wins

This matches the rule: "If there is a draw, the player who rolled the last dice wins the round."

Exploitation

The solve script uses pwntools to connect and stream-read:

  1. Connect and acknowledge readiness
  2. For each round: read lines until "Who wins this round?" appears
  3. Parse each Player X: d1 d2 d3... line, sum the dice, track the running max sum + player
  4. Consume the numbered options list + "> " prompt
  5. Respond with the winner's player number
  6. Repeat for all 100 rounds
Connecting → banner → send "1" → countdown "3...2...1...Go!"
  ┌── Round 1..100 ──────────────────────┐
  │  Read player lines (pre-compute max) │
  │  Skip options (recvuntil "> ")       │
  │  Send winner number                  │
  │  Verify "Yes.. Correct!"             │
  └──────────────────────────────────────┘
  → "Nice job!" → flag.txt

Key Findings

  • The 0.3s timeout is only triggered after input("> ") is called — pre-computing during the player display phase bypasses the speed requirement entirely
  • Tie-breaking by higher player number is a subtle detail that matters in every round with 8+ players
  • Round 100 has players rolling 200 dice each — the output lines are long, but parsing is still fast

Tools Used

  • pwntools — TCP socket interaction with server
  • re — regex parsing of player/dice lines

Flag

HTB{r0LL1ng-1n-t43_D33P-b0t_n3T-cRe4t10n}

Lessons Learned

  • When a challenge has a tight response window, check if data streams in early enough to pre-compute
  • Always read the source code carefully for tie-breaking logic — sorted(...)[-1] with stable sort favors highest-indexed equal elements
  • pwntools recvuntil() + recvline() is perfect for line-oriented streaming protocols
  • The "Very Easy" difficulty comes from having full source code and a straightforward computation — no crypto, no binary exploitation, just correct parsing under a timer