Writeup by bylal for 2x2 Furious

misc

December 21, 2025

Challenge description

Subject in french: Un cube. Une horloge qui tourne. 10 secondes pour rétablir l’ordre. Peux-tu suivre le rythme ?

Pense vite. Tourne intelligemment.

Which translates to: A cube. A ticking clock. 10 seconds to restore order. Can you keep up?

Think fast. Spin smart.

First thoughts

This is what you see when you connect to the server:

Initial ANSI-art prompt

then press enter to be presented with a rubiks cube 2*2 random mix:

Find a solver online

So my first idea was to look for a solver online, preferably in python and I found this github:

py222

Solving steps

So then solving it becomes straightforward:

Approach overview

  1. Capture ANSI art - connect to the server with pwntools
  2. Parse colors → state[24]
  3. Reindex + normalize (fixed D-L-B corner)
  4. Run solver.solveCube
  5. Map WCA commands to CTF commands
  6. Send and retrieve flag

Implementation

So the first struggle was to get the grid (because the colour are displayed in your terminal, I wasn’t sure of the actual bytes received).

I changed the: context.log_level to debug to see exactly the received bytes to make the parsing easier.

After translating we must no forget to normalise using py222.normFC. That is because py222.normFC “rotates” (via a simple relabeling) any raw sticker state so that the D-L-B corner is always in the same fixed spot with the same face ordering. That normalization is mandatory because everything downstream in Py222 is keyed to that fixed-corner frame.

Note: the py222 gives multiples solution, for an easier solution I just put them all in a list and send them all at once, so the first output solution from it is the one the server will use to solve the rubiks cube.

This is why sometimes you will see a long solution when it in reality any 2*2 cube can be solved in 11 moves maximum.

Finally, the complete script looks like this:

#!/usr/bin/env python3
import re
import sys
import io
import contextlib
import numpy as np
from pwn import *

import py222    # from Py222 github
import solver   # from Py222 github

HOST = '192.168.1.108'
PORT = 4002

#context.log_level = "debug"

ANSI_BG = {41:'R',42:'G',43:'Y',44:'B',45:'O',46:'W',47:'W'}
LETTER2COLOR = {'Y':0,'R':1,'G':2,'W':3,'O':4,'B':5}
INDEX_MAP = [
     0, 1, 2, 3,
    16,17, 8, 9,
     4, 5,20,21,
    18,19,10,11,
     6, 7,22,23,
    12,13,14,15
]

# mapping WCA → CTF commands
CTF_MAP = {
    'R':  ['r'],      "R'": ["R"],     'R2': ['r','r'],
    'U':  ['u'],      "U'": ["U"],     'U2': ['u','u'],
    'F':  ['f'],      "F'": ["F"],     'F2': ['f','f'],
    'L':  ['l'],      "L'": ["L"],     'L2': ['l','l'],
    'D':  ['d'],      "D'": ["D"],     'D2': ['d','d'],
    'B':  ['b'],      "B'": ["B"],     'B2': ['b','b'],
}

def parse_net(buf: bytes) -> np.ndarray:
    tiles = re.findall(
        r'\x1b\[\d{3}m\x1b\[(\d{3})m {2}\x1b\[0m',
        buf.decode('latin1')
    )
    if len(tiles) != 24:
        print("DEBUG: found", len(tiles), "tiles:", tiles, file=sys.stderr)
        raise ValueError("Expected 24 tiles ANSI")
    faces2 = [LETTER2COLOR[ANSI_BG[int(c)]] for c in tiles]
    arr = np.empty(24, dtype=int)
    for i, col in enumerate(faces2):
        arr[INDEX_MAP[i]] = col
    return arr

def main():
    conn = remote(HOST, PORT)

    # 1) Kickoff scramble
    conn.recvuntil(b"Press Enter to scramble")
    conn.sendline(b"")

    # 2) Read until first prompt
    data = conn.recvuntil(b"Your move:")

    # 3) Parse ANSI → Py222 state
    state = parse_net(data)

    # 4) Normalize for fixed-corner
    state = py222.normFC(state)

    # 5) Capture solver output
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        solver.solveCube(state)
    out = buf.getvalue()

    # 6) Extract WCA‐style algorithm line(s)
    alg_lines = [
        line.strip()
        for line in out.splitlines()
        if re.match(r"^[URFDLB][2']?(?:\s+[URFDLB][2']?)*$", line.strip())
    ]
    if not alg_lines:
        print("Error : no output captured.", file=sys.stderr)
        print("=== SOLVER OUTPUT ===\n", out, file=sys.stderr)
        sys.exit(1)

    # 7) Flatten to individual WCA moves
    moves_wca = " ".join(alg_lines).split()
    print("[*] Moves WCA →", moves_wca)

    # 8) Translate to CTF commands
    moves_ctf = []
    for m in moves_wca:
        if m not in CTF_MAP:
            print(f"Error : move unexpected {m}", file=sys.stderr)
            sys.exit(1)
        moves_ctf.extend(CTF_MAP[m])
    print("[*] Moves CTF →", moves_ctf)

    # 9) Send CTF moves
    for cmd in moves_ctf:
        conn.sendline(cmd.encode())

    # 10) Get the flag
    print(conn.recvall(timeout=2).decode(errors='ignore'))

if __name__=="__main__":
    main()

And the output of the program looks like this:

(note: the screen were taken on the docker version run locally)

Flag

AMSI{RUB1K5_5P33D_MA5T3R!}