← Back to Writeups
HTBMediumCoding

Bejeweled

D3v0o0Nu119/2/20265 min read
#coding#htb#medium#adaptive_waiting#ansi_terminal_emulation#greedy_move_search#grid_parsing#sgr_mouse_click_injection

Bejeweled

Platform: Sekai2026 | Category: Coding | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-28 | Status: Solved Techniques: adaptive_waiting, ansi_terminal_emulation, greedy_move_search, grid_parsing, sgr_mouse_click_injection

Summary

Task: an interactive TCP Bejeweled terminal game using ANSI drawing and SGR mouse input. Solution: emulate the terminal screen, parse the 12x7 grid, greedily choose legal match-3 swaps, and click cells with adaptive waits.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: sekai2026 | ID: 20260628_sekai2026_bejeweled
  • Tags: automation, tcp_service, terminal_ui, ansi_escape, sgr_mouse, match3
  • Indicators: ANSI alternate-screen terminal UI, minimum terminal size 80x24, SGR mouse mode sequences, 12x7 match-3 grid, 45 second game timer
  • Source: 20260628_sekai2026_bejeweled.md

Foothold

Vulnerability / Misconfiguration

  1. adaptive_waiting
  2. ansi_terminal_emulation
  3. greedy_move_search
  4. grid_parsing
  5. sgr_mouse_click_injection
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
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.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • adaptive_waiting
  • ansi_terminal_emulation
  • greedy_move_search
  • grid_parsing
  • sgr_mouse_click_injection
  • Tags: automation, tcp_service, terminal_ui, ansi_escape, sgr_mouse, match3

Original Writeup

<details><summary>Click to expand original content</summary>

Description

The stage is yours. Clear the board before the final note fades. socat -,raw,echo=0 tcp:bejeweled.chals.sekai.team:1337

English summary: the service launches a terminal Bejeweled-style game over TCP. The goal is to automate valid mouse input quickly enough to clear the game before the timer expires.

Analysis

Connecting to the service shows an ANSI alternate-screen terminal UI, not a line-oriented protocol. The initial screen requires at least an 80x24 terminal and displays a [ Start ] button. The application enables mouse reporting and accepts SGR mouse events, so the start button can be pressed by sending:

text
ESC[<0;40;13M ESC[<0;40;13m

After starting, the game board is a 12x7 match-3 grid. The visible gem symbols are:

text
♣ ♢ ♡ ◼ ▲ ● ♠

The grid is drawn at stable terminal coordinates. For zero-based column c in [0..6] and row r in [0..11], the cell centers are:

python
x = 19 + 6*c   # terminal column
y = 2 + 2*r    # terminal row

This made it possible to ignore most of the UI and maintain a small virtual terminal screen from ANSI cursor-position writes. The solver parses CSI row;col H updates, ignores styling and mouse-mode escape sequences, and reads gem glyphs near each cell center.

Solution

The automation loop is:

  1. Connect to bejeweled.chals.sekai.team:1337.
  2. Parse incoming ANSI output into a virtual screen buffer.
  3. Send the SGR mouse down/up sequence for the [ Start ] button.
  4. Extract the 12x7 board from fixed cell coordinates.
  5. Enumerate every adjacent horizontal/vertical swap.
  6. For each swap, check whether it creates any horizontal or vertical run of 3+ identical gems.
  7. Pick the best greedy move, preferring more cleared cells and matches lower on the board.
  8. Send two SGR mouse clicks at the selected cell centers.
  9. Wait only until the score or parsed grid changes, then immediately compute the next move.

The timing optimization was important. A first version slept a fixed long delay after every swap and only reached score 9855, level 4 before the 45-second timeout. The successful version waits adaptively for a score or board change, which avoids wasting time and reaches level 8 within 43 seconds.

Core snippets from tasks/sekai2026/Bejeweled/solve.py:

python
GEMS = "♣♢♡◼▲●♠"
R, C = 12, 7
CELL_X = [19 + 6 * i for i in range(C)]
CELL_Y = [2 + 2 * i for i in range(R)]

def click(s, r, c):
    x, y = CELL_X[c], CELL_Y[r]
    s.sendall(f"\x1b[<0;{x};{y}M\x1b[<0;{x};{y}m".encode())

def matches(g):
    m = set()
    for r in range(R):
        c = 0
        while c < C:
            c2 = c + 1
            while c2 < C and g[r][c2] == g[r][c]:
                c2 += 1
            if c2 - c >= 3:
                m.update((r, k) for k in range(c, c2))
            c = c2
    for c in range(C):
        r = 0
        while r < R:
            r2 = r + 1
            while r2 < R and g[r2][c] == g[r][c]:
                r2 += 1
            if r2 - r >= 3:
                m.update((k, c) for k in range(r, r2))
            r = r2
    return m

def best_move(g):
    best = None
    for r in range(R):
        for c in range(C):
            for dr, dc in [(1, 0), (0, 1)]:
                rr, cc = r + dr, c + dc
                if rr >= R or cc >= C or g[r][c] == g[rr][cc]:
                    continue
                h = [row[:] for row in g]
                h[r][c], h[rr][cc] = h[rr][cc], h[r][c]
                mm = matches(h)
                if mm:
                    score = len(mm) * 100 + sum(x for x, y in mm)
                    if not best or score > best[0]:
                        best = (score, (r, c, rr, cc), len(mm))
    return None if best is None else best[1:]

The adaptive wait after each pair of clicks looks like this:

python
old_score = vals.get("score", -1)
old_grid = [row[:] for row in g]
click(s, r, c)
time.sleep(0.005)
click(s, rr, cc)

deadline = time.time() + 0.42
while time.time() < deadline:
    recv_some(s, term, 0.035)
    vals_now = term.score_level_time()
    gg = term.grid()
    if vals_now.get("score", old_score) != old_score or (gg and gg != old_grid):
        break

The successful run printed:

text
*** GAME CLEAR! ***
You reached level 8!
Final score: 17610
Time:        43s
SEKAI{REDACTED}
</details>

signed by XESXOR