← Back to Writeups
CUSTOMEasyMisc

Скоростные Пазлы – 1 (Fast Puzzles – 1)

XESXOR9/2/20263 min read
#misc#custom#easy#api_reverse_engineering#automated_puzzle_solving#mse_pixel_matching

Скоростные Пазлы – 1 (Fast Puzzles – 1)

Platform: bug-makers | Category: Misc | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-25 | Status: Solved Techniques: api_reverse_engineering, automated_puzzle_solving, mse_pixel_matching

Summary

Task: web-based 4×4 jigsaw puzzle that must be solved within 60 seconds; API provides original image and shuffled pieces as base64 JPEG. Solution: reverse-engineered the API, matched pieces to grid positions using MSE pixel comparison, submitted correct order automatically.

Скоростные Пазлы – 1 (Fast Puzzles – 1) — bug-makers

Description

Чтобы получить флаг, нужно успеть собрать весь пазл за 1 минуту. Количество пазлов: 16 шт. Адрес: https://fast-puzzles-1.t1.bug-makers.ru

English summary: A web-based jigsaw puzzle game where the player must correctly assemble a 4×4 grid (16 pieces) within 60 seconds to receive the flag.

Analysis

The challenge presents a drag-and-drop puzzle interface in the browser. Inspecting the page source and JavaScript (script.js) reveals two API endpoints: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. GET /api/new-game — Starts a new game session. Returns JSON containing:

    • originalImage: base64-encoded JPEG of the complete, unshuffled image
    • puzzle: array of 16 objects, each with id (integer) and image (base64-encoded JPEG of that piece)
    • gridSize: 4 (the grid is 4×4)
    • pieceSize: 137 (each piece is 137×137 pixels)
    • success: boolean
  2. POST /api/check-solution — Accepts {"solution": [array of piece IDs in correct grid order]}. Returns:

    • success: boolean
    • flag: the flag string (if correct)
    • time: time taken in seconds
    • timeUp: boolean (true if over 60 seconds) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The key insight is that the API provides the original unshuffled image alongside the shuffled pieces. This means we don't need to solve the puzzle visually — we can programmatically match each piece to its correct position by comparing pixel data.

Solution

Step 1: Reverse-engineer the API

Using browser DevTools and curl, the two endpoints were identified. The /api/new-game response conveniently includes the full original image, making automated solving trivial.

Step 2: Automate piece matching

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The approach:

  1. Call GET /api/new-game to obtain the original image and all 16 shuffled pieces.
  2. Decode all images from base64 into numpy arrays.
  3. For each grid position (row, col), extract the corresponding 137×137 pixel region from the original image.
  4. Compare each unmatched piece against the region using Mean Squared Error (MSE) — the piece with the lowest MSE is the correct match.
  5. Build the solution array of piece IDs in grid order (left-to-right, top-to-bottom).
  6. Submit via POST /api/check-solution. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Submit and get the flag

The script solved the puzzle in approximately 1.29 seconds, well within the 60-second time limit.

python
#!/usr/bin/env python3
"""Solve the puzzle by matching pieces to the original image."""

import requests
import base64
import json
import numpy as np
from io import BytesIO
from PIL import Image

BASE_URL = "https://fast-puzzles-1.t1.bug-makers.ru"

# Start a new game
session = requests.Session()
resp = session.get(f"{BASE_URL}/api/new-game")
data = resp.json()

grid_size = data["gridSize"]
piece_size = data["pieceSize"]
pieces = data["puzzle"]
original_b64 = data["originalImage"]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Decode original image
orig_data = base64.b64decode(original_b64.split(",")[1])
orig_img = Image.open(BytesIO(orig_data)).convert("RGB")
orig_arr = np.array(orig_img)

# Decode each piece
piece_images = {}
for p in pieces:
    p_data = base64.b64decode(p["image"].split(",")[1])
    p_img = Image.open(BytesIO(p_data)).convert("RGB")
    piece_images[p["id"]] = np.array(p_img)

# For each grid position, find the best matching piece via MSE
correct_order = []
used_pieces = set()

for row in range(grid_size):
    for col in range(grid_size):
        y_start = row * piece_size
        x_start = col * piece_size
        region = orig_arr[y_start:y_start+piece_size, x_start:x_start+piece_size]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        best_id = None
        best_score = float('inf')

        for pid, p_arr in piece_images.items():
            if pid in used_pieces:
                continue
            h, w = region.shape[:2]
            ph, pw = p_arr.shape[:2]
            if h != ph or w != pw:
                p_resized = np.array(Image.fromarray(p_arr).resize((w, h)))
            else:
                p_resized = p_arr
            diff = region.astype(float) - p_resized.astype(float)
            mse = np.mean(diff ** 2)
            if mse < best_score:
                best_score = mse
                best_id = pid
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        correct_order.append(best_id)
        used_pieces.add(best_id)

# Submit solution
resp = session.post(
    f"{BASE_URL}/api/check-solution",
    json={"solution": correct_order},
    headers={"Content-Type": "application/json"}
)
result = resp.json()
print(json.dumps(result, indent=2, ensure_ascii=False))

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍