World Cup 2026 Penalty Shootout — Concise Solution
Category: Misc / Scripting
Service: nc task.ctf.uz 13001
Challenge
Uzbekistan reaches the 2026 World Cup final. The match is tied and the user
controls Uzbekistan's penalty directions: LEFT, CENTER, or RIGHT.
Brazil (and, on a later connection, Argentina) automatically takes its shots.
The goal is to win the shootout.
Observation
Choosing the same direction repeatedly eventually produces a miss, for example:
Abdukodir Khusanov shoots LEFT.
Alisson Becker jumps LEFT.
[-] MISSED
After choosing y to restart, the goalkeeper repeats exactly the same jump
sequence from penalty 1. The restart resets the score, but it does not reset
the sequence used for the goalkeeper's decisions.
This makes the service an oracle. A failed attempt reveals the goalkeeper's direction for each penalty reached. On the next attempt, choose any other direction for the known positions. If a new position is unknown, submit a trial direction, record its result, and restart only if it is saved.
Exploit strategy
- Start with
LEFTfor unknown penalties. - Parse the line
<goalkeeper> jumps <DIRECTION>. - Store a safe shot for that penalty:
- if the keeper jumps
LEFT, useCENTER; - otherwise use
LEFT.
- if the keeper jumps
- When Uzbekistan misses, answer
yand replay every stored safe shot. - Repeat until all required penalties are known.
The solver intentionally uses only LEFT and CENTER; it only needs one
direction that differs from the keeper's choice.
Solver
The following Python program speaks directly to the TCP service. It handles TCP data in a buffer because a goalkeeper line may be split between network reads.
import re
import socket
HOST, PORT = "task.ctf.uz", 13001
PROMPT = b"Choose your shot [LEFT, CENTER, RIGHT]: "
RESTART = b"try again [y/n]: "
def safe_shot(keeper_direction):
return "CENTER" if keeper_direction == "LEFT" else "LEFT"
with socket.create_connection((HOST, PORT), timeout=15) as sock:
buf, learned = b"", []
round_no, attempt = 0, 1
while True:
while PROMPT not in buf and RESTART not in buf:
block = sock.recv(65536)
if not block:
raise SystemExit("server closed")
buf += block
prompt_at, restart_at = buf.find(PROMPT), buf.find(RESTART)
is_prompt = prompt_at >= 0 and (restart_at < 0 or prompt_at < restart_at)
marker = PROMPT if is_prompt else RESTART
marker_at = prompt_at if is_prompt else restart_at
event, buf = buf[:marker_at], buf[marker_at + len(marker):]
jumps = re.findall(rb"jumps (LEFT|CENTER|RIGHT)\\.", event)
if jumps and round_no:
shot = safe_shot(jumps[-1].decode())
if len(learned) < round_no:
learned.append(shot)
else:
learned[round_no - 1] = shot
flag = re.search(rb"[A-Za-z0-9_]+\\{[^\\r\\n}]+\\}", event)
if flag:
print(flag.group().decode())
break
if is_prompt:
round_no += 1
shot = learned[round_no - 1] if round_no <= len(learned) else "LEFT"
sock.sendall((shot + "\\n").encode())
else:
attempt += 1
round_no = 0
sock.sendall(b"y\\n")
Result
The final replay reached penalty 100 without an Uzbekistan miss. Emiliano Martinez jumped left while Uzbekistan shot center, then Lionel Messi missed.
Score after penalty 100:
Uzbekistan 100 - 99 Argentina
UZBEKISTAN WINS THE WORLD CUP 2026!
Flag
CTF.UZ{W3_4r3_7h3_Ch4mp10n5_my_fr13nd!}