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:

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:
Solving steps
So then solving it becomes straightforward:
Approach overview
- Capture ANSI art - connect to the server with pwntools
- Parse colors →
state[24] - Reindex + normalize (fixed D-L-B corner)
- Run
solver.solveCube - Map WCA commands to CTF commands
- 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!}